Compare commits

...

563 Commits

Author SHA1 Message Date
Claire
5799d5d306 Merge pull request #3336 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 55a7b1ea58 into stable-4.5
2026-01-07 14:40:15 +01:00
Claire
b5d868018d Merge commit '55a7b1ea5820b2fa8d754108b6a948d4bd60d98b' into glitch-soc/merge-4.5 2026-01-07 14:25:37 +01:00
Claire
55a7b1ea58 Bump version to v4.5.4 (#37409) 2026-01-07 14:23:34 +01:00
Claire
c1fb6893c5 Merge commit from fork 2026-01-07 14:15:14 +01:00
Claire
71ae4cf2cf Merge commit from fork 2026-01-07 14:14:42 +01:00
Claire
2ffe03457d Merge pull request #3334 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to a846ed17ff into stable-4.5
2026-01-06 20:38:29 +01:00
Claire
c1f5a9db23 [Glitch] Fix custom emojis not being rendered in profile fields
Port b622f4c698 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2026-01-06 18:14:35 +01:00
Claire
7c0701d906 [Glitch] Fix outdated link target for “locked” warning
Port e8a49bd6ae to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2026-01-06 18:14:35 +01:00
Claire
b134c6a8ef Merge commit 'a846ed17ffb85087de658678b482495981f02770' into glitch-soc/merge-4.5 2026-01-06 18:12:51 +01:00
Claire
a846ed17ff Fix custom emojis not being rendered in profile fields (#37365) 2026-01-06 14:11:56 +01:00
Claire
3013039720 Fix serialization of context pages (#37376) 2026-01-06 14:11:56 +01:00
Claire
ad4ba5aa00 Fix quotes with CWs but no text not having fallback link (#37361) 2026-01-06 14:11:56 +01:00
Claire
1c5461fffe Fix outdated link target for “locked” warning (#37366) 2026-01-06 14:11:56 +01:00
Claire
725c1a159d Merge pull request #3324 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 3de59a9344 into stable-4.5
2025-12-28 19:47:25 +01:00
ChaosExAnima
b52efea5cb [Glitch] Remove rendering of custom emoji using the database
Port 3de59a9344 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-28 10:56:09 +01:00
Claire
a0bdfc46c7 [Glitch] Fix custom emojis not displaying in CWs and fav/boost notifications
Port 962ae88caf to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-28 10:55:34 +01:00
diondiondion
afcdc19730 [Glitch] Fix notifications page error in Tor browser
Port 7d9d3de972 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-28 10:55:10 +01:00
Echo
80aa3bc8ad [Glitch] Emojis: Show in embedded statuses
Port 546a95349e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-28 10:54:47 +01:00
Claire
92955f7e6e [Glitch] Fix hashtag autocomplete replacing suggestion's first characters with input
Port 8d1ea4c531 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-28 10:53:53 +01:00
Claire
b868e598bc Merge commit '3de59a93441367fbca0fd22818c8411adc73a967' into glitch-soc/merge-4.5 2025-12-28 10:51:43 +01:00
ChaosExAnima
3de59a9344 Remove rendering of custom emoji using the database (#37284) 2025-12-19 11:02:32 +01:00
Echo
32c3376d84 Fixes CDN domain loading (#37310) 2025-12-19 11:02:32 +01:00
Claire
962ae88caf Fix custom emojis not displaying in CWs and fav/boost notifications (#37306) 2025-12-19 11:02:32 +01:00
diondiondion
7d9d3de972 Fix notifications page error in Tor browser (#37285) 2025-12-19 11:02:32 +01:00
Echo
546a95349e Emojis: Show in embedded statuses (#37272) 2025-12-19 11:02:32 +01:00
Claire
df1ab0ab90 Fix default Admin role not including view_feeds permission (#37301) 2025-12-19 11:02:32 +01:00
Claire
8d1ea4c531 Fix hashtag autocomplete replacing suggestion's first characters with input (#37281) 2025-12-19 11:02:32 +01:00
Claire
8233295e3b Fix mentions of domain-blocked users being processed (#37257) 2025-12-19 11:02:32 +01:00
Claire
4eb0a506d3 Change HTTP Signature verification status from 401 to 503 on temporary failure to get remote actor (#37221) 2025-12-19 11:02:32 +01:00
Claire
75739a5a9b Change build-releases workflow to tag images latest based on latest stable-x.y branch (#37179)
Co-authored-by: emilweth <7402764+emilweth@users.noreply.github.com>
2025-12-19 11:02:32 +01:00
Claire
54e08a54e9 Merge pull request #3308 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 86cff1abca into stable-4.5
2025-12-08 17:20:59 +01:00
Claire
12ec21a95f [Glitch] Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases
Port 1ae3b4672b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:40:40 +01:00
Echo
fe9a71975c [Glitch] Fixes YouTube embeds
Port 9bc9ebc59e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:40:11 +01:00
Echo
e1f145973b [Glitch] Remove noreferrer from external links
Port 234990cc37 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:39:17 +01:00
Claire
66f25c6709 [Glitch] Fix error handling when re-fetching already-known statuses
Port edfbcfb3f5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:39:03 +01:00
diondiondion
e4e4ffb08d [Glitch] Fix post navigation in single-column mode when Advanced UI is enabled
Port f12f198f61 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:38:49 +01:00
Claire
e016e2a31e [Glitch] Fix compose autosuggest always lowercasing token
Port a26636ff1f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:38:31 +01:00
Claire
6c1e892dd7 Merge commit '86cff1abca6af7ffb1a6ca004ae308c0df6d45ba' into glitch-soc/merge-4.5 2025-12-08 16:37:11 +01:00
Claire
86cff1abca Bump version to v4.5.3 (#37142) 2025-12-08 16:20:15 +01:00
Claire
e6d2fc869b Merge commit from fork 2025-12-08 15:44:08 +01:00
github-actions[bot]
a9f8268a75 New Crowdin Translations for stable-4.5 (automated) (#37158)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-12-08 12:56:49 +01:00
Claire
dfe269439a Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases (#37140) 2025-12-05 16:50:07 +01:00
Echo
9bc9ebc59e Fixes YouTube embeds (#37126) 2025-12-05 11:15:16 +01:00
Claire
a6d31c0ccf Fix streamed quoted polls not being hydrated correctly (#37118) 2025-12-05 11:15:16 +01:00
David Roetzel
1e2cf6c964 Fix creation of duplicate conversations (#37108) 2025-12-05 11:15:16 +01:00
Echo
c42c71c90a Remove noreferrer from external links (#37107) 2025-12-05 11:15:16 +01:00
Claire
782e410719 Make settings-related database migrations more robust (#37079) 2025-12-05 11:15:16 +01:00
Claire
b0c141e658 Fix error handling when re-fetching already-known statuses (#37077) 2025-12-05 11:15:16 +01:00
diondiondion
1ef4bbd88d Fix post navigation in single-column mode when Advanced UI is enabled (#37044) 2025-12-05 11:15:16 +01:00
Claire
240d38b7c0 Fix tootctl status remove removing quoted posts and remote quotes of local posts (#37009) 2025-12-05 11:15:16 +01:00
Claire
770d1212bb Increase HTTP read timeout for expensive S3 batch delete operation (#37004) 2025-12-05 11:15:16 +01:00
Claire
86e463c0e8 Fix compose autosuggest always lowercasing token (#36995) 2025-12-05 11:15:16 +01:00
Matt Jankowski
a04a210e14 Suggest ES image version 7.17.29 in docker compose (#36972) 2025-12-05 11:15:16 +01:00
Claire
300d62f1c4 Merge pull request #3291 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 19588756ef into stable-4.5
2025-11-20 15:07:17 +01:00
Claire
27d33d1233 [Glitch] Fix statuses without text disappearing on reload
Port 05c624cfa7 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-20 14:49:45 +01:00
Claire
b35c691ab2 Merge commit '19588756ef62847a01486fa360d4e4ee00307948' into glitch-soc/merge-4.5 2025-11-20 14:49:19 +01:00
Claire
19588756ef Bump version to v4.5.2 (#36944) 2025-11-20 14:41:09 +01:00
github-actions[bot]
e398ff40b2 New Crowdin Translations for stable-4.5 (automated) (#36945)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-11-20 13:40:31 +01:00
Claire
96eb687524 Fix missing fallback link in CW-only quote posts (#36963) 2025-11-20 12:56:41 +01:00
Claire
05c624cfa7 Fix statuses without text disappearing on reload (#36962) 2025-11-20 12:56:41 +01:00
Claire
dba811952a Merge pull request #3289 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 5fe316b2e9 into stable-4.5
2025-11-19 22:59:47 +01:00
diondiondion
8836c4fc84 [Glitch] Fix g + h keyboard shortcut not working when a post is focused
Port 1dbf10198d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:25:09 +01:00
Claire
1da82862f1 [Glitch] Fix quoting overwriting current content warning
Port c6ccacdf7b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:24:40 +01:00
Claire
8c725777ed [Glitch] Fix scroll-to-status in threaded view being unreliable
Port 6ccd9c2f1f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:24:15 +01:00
Claire
126823986a [Glitch] Change private quote education modal to not show up on self-quotes
Port 261d9b33fe to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:23:42 +01:00
Claire
f04b06a44f [Glitch] Fix double encoding in links
Port 4ee21c2e29 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:23:13 +01:00
Echo
c7481cb2ca [Glitch] Emoji: Fix path resolution for emoji worker
Port c08cd6d62a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:22:37 +01:00
Echo
7141917943 [Glitch] Fix error with remote tags including percent signs
Port 6486c092f6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:19:45 +01:00
Claire
6c7a9b8311 [Glitch] Fix bogus quote approval policy not always being replaced correctly
Port a7b45682a6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:19:16 +01:00
Claire
8ae06fdbcd [Glitch] Fix hashtag completion not being inserted correctly
Port 5a57c0844a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:18:46 +01:00
diondiondion
553bb4673e [Glitch] Fix Cmd/Ctrl + Enter in the composer triggering confirmation dialog action
Port 1d081250f4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:18:16 +01:00
Claire
fe6d3690fd Merge commit '5fe316b2e94bccb91732a1d6675854abb60e9a4b' into glitch-soc/merge-4.5 2025-11-19 22:15:12 +01:00
Claire
9746c5eb6b Merge pull request #3287 from ClearlyClaire/glitch-soc/backports/4.5
Backport changes to stable-4.5
2025-11-19 22:14:32 +01:00
Claire
b5ae2f07a1 Change quotes to inherit local-only status of quoted post in composer (#3286) 2025-11-19 21:29:00 +01:00
Claire
9b91852f93 Fix d bookmark keyboard shortcut (#3285) 2025-11-19 21:28:26 +01:00
Claire
0bb7711225 Fix threaded mode not resetting quote (#3284) 2025-11-19 21:26:10 +01:00
Claire
86445f45fc Clean up CSS differences with upstream (#3283) 2025-11-19 21:26:10 +01:00
Claire
5fe316b2e9 Update dependency glob (#36941) 2025-11-19 16:29:35 +01:00
diondiondion
1dbf10198d Fix g + h keyboard shortcut not working when a post is focused (#36935) 2025-11-19 15:20:01 +01:00
Claire
c6ccacdf7b Fix quoting overwriting current content warning (#36934) 2025-11-19 15:20:01 +01:00
Claire
6ccd9c2f1f Fix scroll-to-status in threaded view being unreliable (#36927) 2025-11-19 15:20:01 +01:00
Claire
261d9b33fe Change private quote education modal to not show up on self-quotes (#36926) 2025-11-19 15:20:01 +01:00
Claire
4ee21c2e29 Fix double encoding in links (#36925) 2025-11-19 15:20:01 +01:00
Echo
c08cd6d62a Emoji: Fix path resolution for emoji worker (#36897) 2025-11-19 15:20:01 +01:00
Shugo Maeda
44d45e5705 Fix ArgumentError of tootctl upgrade storage-schema (#36914) 2025-11-19 15:20:01 +01:00
Claire
27c67f1750 Fix cross-origin handling of CSS modules (#36890) 2025-11-19 15:20:01 +01:00
renovate[bot]
bb28552859 chore(deps): update dependency js-yaml to v4.1.1 [security] (#36891)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 15:20:01 +01:00
Echo
6486c092f6 Fix error with remote tags including percent signs (#36886) 2025-11-19 15:20:01 +01:00
Claire
a7b45682a6 Fix bogus quote approval policy not always being replaced correctly (#36885) 2025-11-19 15:20:01 +01:00
Claire
5a57c0844a Fix hashtag completion not being inserted correctly (#36884) 2025-11-19 15:20:01 +01:00
diondiondion
1d081250f4 Fix Cmd/Ctrl + Enter in the composer triggering confirmation dialog action (#36870) 2025-11-19 15:20:01 +01:00
Claire
e6c8958d07 Fix missing alt-text confirmation modal not opening (#3281) 2025-11-15 16:48:12 +01:00
Claire
b2506cc110 Merge pull request #3276 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to bb6093c315 into stable-4.5
2025-11-13 17:58:54 +01:00
Claire
d18491b7a7 [Glitch] Fix error when sending new posts
Port 058f704c21 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:36:50 +01:00
Claire
13330030cd [Glitch] Fix posts coming from public/hashtag streaming being marked as unquotable
Port 55b9d21537 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:36:21 +01:00
diondiondion
f9012a774c [Glitch] Fix Cmd/Ctrl + Enter not submitting Alt text modal on some browsers
Port 6baa8f2466 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:31:18 +01:00
Echo
375af385e7 [Glitch] Fix deprecation warning in Vite
Port 28b9e9087a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:30:32 +01:00
diondiondion
7e5224a3c0 [Glitch] Fixes blank screen in browsers that don't support Intl.DisplayNames
Port fa2cc409ce to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:29:41 +01:00
Claire
140d782cba [Glitch] Fix filters not being applied to quotes in detailed view
Port 8a100d84c5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:28:58 +01:00
Echo
c7a7ce8ce7 [Glitch] Emoji: Load emoji with hash in URL
Port 9ae0464e8f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:28:15 +01:00
diondiondion
afbe0a4860 [Glitch] Fix scroll shift caused by fetch-all-replies alerts
Port 9eea4479e1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:27:30 +01:00
diondiondion
febde69d0b [Glitch] Fix dropdown menu not focusing first item when opened via keyboard
Port 30103fd2c8 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:26:57 +01:00
Claire
85b9a5944d [Glitch] Fix prepared quote not being discarded with contents when replying
Port fbe05d42fb to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:26:34 +01:00
Claire
585827c14f Merge commit 'bb6093c3153f092736e2f25959e3dcdceeb7bce1' into glitch-soc/merge-4.5 2025-11-13 17:22:33 +01:00
Claire
bb6093c315 Bump version to v4.5.1 2025-11-13 17:12:07 +01:00
Claire
058f704c21 Fix error when sending new posts (#36869) 2025-11-13 17:12:07 +01:00
diondiondion
6baa8f2466 Fix Cmd/Ctrl + Enter not submitting Alt text modal on some browsers (#36866) 2025-11-13 17:12:07 +01:00
github-actions[bot]
e742eff044 New Crowdin Translations for stable-4.5 (automated) (#36864)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-11-13 15:46:34 +01:00
Claire
55b9d21537 Fix posts coming from public/hashtag streaming being marked as unquotable (#36860) 2025-11-13 15:24:58 +01:00
Claire
59f0134578 Fix Update importing old previously-unknown activities and treating them as recent ones (#36848) 2025-11-13 15:24:58 +01:00
Echo
28b9e9087a Fix deprecation warning in Vite (#36849) 2025-11-13 15:24:58 +01:00
diondiondion
fa2cc409ce Fixes blank screen in browsers that don't support Intl.DisplayNames (#36847) 2025-11-13 15:24:58 +01:00
Claire
8a100d84c5 Fix filters not being applied to quotes in detailed view (#36843) 2025-11-13 15:24:58 +01:00
Echo
9ae0464e8f Emoji: Load emoji with hash in URL (#36808) 2025-11-13 15:24:58 +01:00
diondiondion
9eea4479e1 Fix scroll shift caused by fetch-all-replies alerts (#36807) 2025-11-13 15:24:58 +01:00
diondiondion
30103fd2c8 Fix dropdown menu not focusing first item when opened via keyboard (#36804) 2025-11-13 15:24:58 +01:00
Claire
a9a7ad62f1 Update dependency rollup from 4.46.2 to 4.46.4 (#36781) 2025-11-13 15:24:58 +01:00
Claire
ea663cf7c7 Fix /api/v1/statuses/:id/context sometimes returing Mastodon-Async-Refresh without result_count (#36779) 2025-11-13 15:24:58 +01:00
Claire
fbe05d42fb Fix prepared quote not being discarded with contents when replying (#36778) 2025-11-13 15:24:58 +01:00
Claire
29ae9c9c4b Add 4.5.x to the list of supported branches (#36761) 2025-11-06 17:12:41 +01:00
Claire
4684d5e69b Merge pull request #3269 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 26c78392f8
2025-11-06 13:09:16 +01:00
Claire
3ca92c4ae2 Merge commit '26c78392f88afccebb8a88b68bc9994a9ce12648' into glitch-soc/merge-4.5 2025-11-06 12:49:01 +01:00
Claire
81b1a34d96 Merge pull request #3267 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 048430f4e8
2025-11-06 12:45:08 +01:00
Claire
26c78392f8 Bump version to v4.5.0 (#36732) 2025-11-06 12:39:07 +01:00
Echo
caecc88247 [Glitch] Fix: correctly dismisses announcement when viewed
Port d45b4db1d7 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-06 12:20:19 +01:00
Echo
bed4ca26e2 [Glitch] Add default visualizer for audio upload without poster
Port ef3a95affc to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-06 12:19:58 +01:00
diondiondion
5d108e95d7 [Glitch] Fix spoiler toggle button being able to submit compose form
Port 3e6a9371b0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-06 12:18:21 +01:00
Claire
8a2d38a47b Merge commit '048430f4e8010672eb667f1ccf2372571cfb97dd' into glitch-soc/merge-4.5 2025-11-06 12:16:55 +01:00
github-actions[bot]
048430f4e8 New Crowdin Translations for stable-4.5 (automated) (#36745)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-11-06 11:37:54 +01:00
Echo
d45b4db1d7 Fix: correctly dismisses announcement when viewed (#36750) 2025-11-06 11:23:46 +01:00
Echo
ef3a95affc Add default visualizer for audio upload without poster (#36734) 2025-11-06 10:34:12 +01:00
diondiondion
3e6a9371b0 Fix spoiler toggle button being able to submit compose form (#36736) 2025-11-06 10:34:12 +01:00
Claire
7ab0cfd637 Merge pull request #3266 from ClearlyClaire/glitch-soc/merge-4.5
Port missing changes to stable-4.5
2025-11-05 11:27:29 +01:00
Claire
b33bcc8be6 Merge pull request #3265 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to e91c764590
2025-11-05 10:52:40 +01:00
Claire
cdad6ee0c9 [Glitch] Change paste-link-to-quote loading state from generic loading bar to compose placeholder
Port cfdd9396c0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:09:28 +01:00
Claire
2d958cb909 [Glitch] Change quote action to error instead of insert link in Private Mentions
Port ba498ae779 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:09:12 +01:00
Echo
949f15e200 [Glitch] Quote Posts: Add notifications for DMs and private posts
Port 5bae08d1ff to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:09:02 +01:00
Claire
105a2d64a7 [Glitch] Fix Skeleton placeholders being animated when setting to reduce animations is enabled
Port 0b50789c5b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:08:49 +01:00
Claire
1c17990413 [Glitch] Fix quote dropdown menu item in detailed status view
Port a978e37f4c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:08:37 +01:00
Claire
0a8f96d3be [Glitch] Remove option to disable access to local topic feeds for logged-in users
Port dd708298a8 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:08:24 +01:00
Claire
1ec8e42dbb [Glitch] Disable paste-link-to-quote flow when composing Private Mentions
Port 6d53ca63d6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:08:08 +01:00
Claire
7ea3c6a039 Merge commit 'e91c7645905972d9663a0d944b133ff24670bce2' into glitch-soc/merge-4.5 2025-11-05 10:02:33 +01:00
Claire
e91c764590 Bump version to v4.5.0-rc.3 2025-11-05 09:59:00 +01:00
Claire
cfdd9396c0 Change paste-link-to-quote loading state from generic loading bar to compose placeholder (#36695) 2025-11-05 09:59:00 +01:00
Claire
ba498ae779 Change quote action to error instead of insert link in Private Mentions (#36721) 2025-11-05 09:59:00 +01:00
Echo
5bae08d1ff Quote Posts: Add notifications for DMs and private posts (#36696) 2025-11-05 09:59:00 +01:00
Echo
5253527ec4 Add CSS Module support (#36637) 2025-11-05 09:59:00 +01:00
Claire
0b50789c5b Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716) 2025-11-05 09:59:00 +01:00
Claire
a978e37f4c Fix quote dropdown menu item in detailed status view (#36704) 2025-11-05 09:59:00 +01:00
Claire
dd708298a8 Remove option to disable access to local topic feeds for logged-in users (#36703) 2025-11-05 09:59:00 +01:00
renovate[bot]
449eb03f11 chore(deps): update dependency sidekiq to v8.0.9 (#36699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
renovate[bot]
1baede0a7c chore(deps): update dependency brakeman to v7.1.1 (#35434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
renovate[bot]
a7ecfc1ca5 fix(deps): update dependency @rails/ujs to v7.1.600 (#36634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
Claire
e62baacfc1 Increase number of quote approval job retries (#36698) 2025-11-05 09:59:00 +01:00
Rachael Wright-Munn
b5a6feb3bf Move "Privacy and reach" from "Public profile" to top-level navigation (#27294) 2025-11-05 09:59:00 +01:00
Claire
05964f571b Prevent creation of Private Mentions quoting someone who is not mentioned (#36689) 2025-11-05 09:59:00 +01:00
Claire
16a54f7158 Fix issuance of quote approval for remote private statuses (#36693) 2025-11-05 09:59:00 +01:00
Claire
6d53ca63d6 Disable paste-link-to-quote flow when composing Private Mentions (#36690) 2025-11-05 09:59:00 +01:00
renovate[bot]
93acfdd7d3 chore(deps): update dependency irb to v1.15.3 (#36682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
renovate[bot]
a209b8e544 chore(deps): update dependency rubyzip to v3.2.2 (#36687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
Claire
ca0c5e7a79 Merge pull request #3258 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to af4c372ab2 into stable-4.5
2025-10-31 16:35:41 +01:00
diondiondion
10a81a2f43 [Glitch] Fix initially selected language in Rules panel, hide selector when no alternative translations exist
Port aa579ce286 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-31 16:12:38 +01:00
diondiondion
9f8fce3c47 [Glitch] Show error when submitting empty post rather than failing silently
Port 214d59bd37 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-31 16:12:03 +01:00
Claire
5f5e6ca031 Merge commit 'af4c372ab22b636742833592235d18418604fcc1' into glitch-soc/merge-4.5 2025-10-31 16:04:05 +01:00
Claire
af4c372ab2 Bump version to v4.5.0-rc.2 2025-10-31 16:01:06 +01:00
diondiondion
aa579ce286 Fix initially selected language in Rules panel, hide selector when no alternative translations exist (#36672) 2025-10-31 16:01:06 +01:00
github-actions[bot]
adfabf8c80 New Crowdin Translations for stable-4.5 (automated) (#36670)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-31 14:51:18 +01:00
renovate[bot]
ea710df180 chore(deps): update dependency axios to v1.13.1 (#36633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 14:09:36 +01:00
renovate[bot]
e1b6e28829 chore(deps): update dependency libvips to v8.17.3 (#36654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 14:09:36 +01:00
diondiondion
214d59bd37 Show error when submitting empty post rather than failing silently (#36650) 2025-10-31 14:09:36 +01:00
Claire
e4291e9b05 Fix SMTP configuration with mail 2.9.0 (#36646) 2025-10-31 14:09:36 +01:00
Claire
868d782b2b Merge pull request #3257 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 2a9c7d2b9e
2025-10-29 14:07:12 +01:00
Claire
1b60f597d7 Merge commit '2a9c7d2b9e51cdfbc636972c0f9ffdbe06c02d59' into glitch-soc/merge-upstream 2025-10-29 13:42:26 +01:00
Claire
2a9c7d2b9e Fix quote-inline fallback being removed even for legacy quotes (#36638) 2025-10-29 11:56:34 +00:00
Claire
9db64d6908 Merge pull request #3256 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 51877081b4
2025-10-29 12:45:31 +01:00
Claire
074b3fe57e [Glitch] Change display of blocked and muted quoted users
Port e437bb919f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-29 12:25:47 +01:00
Claire
002e592667 Merge commit '51877081b435b38e1c5bd449087279469fa7c667' into glitch-soc/merge-upstream 2025-10-29 12:24:49 +01:00
Claire
51877081b4 Bump version to v4.5.0-rc.1 (#36635) 2025-10-29 11:11:33 +00:00
github-actions[bot]
7b66eefd3e New Crowdin Translations (automated) (#36632)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-29 10:13:42 +00:00
Claire
e437bb919f Change display of blocked and muted quoted users (#36619) 2025-10-29 09:13:12 +00:00
Claire
8f00874a0e Merge pull request #3255 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 4896d2c4c6
2025-10-28 23:20:31 +01:00
Claire
ac920eb364 Fix javascript linting error 2025-10-28 23:04:36 +01:00
Claire
d04e6ec597 Remove glitch-soc system emoji font option now that it's superseded by upstream 2025-10-28 22:57:31 +01:00
diondiondion
24234e6632 [Glitch] chore(deps): update dependency eslint-plugin-jsdoc to v60
Port e1bd9b944a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:46:27 +01:00
Claire
8cd8e69c4b [Glitch] Change firehose labels depending on which feeds are accessible
Port 4896d2c4c6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:38:23 +01:00
Claire
293b8f6744 [Glitch] Change styling of column banners
Port 26ec19a649 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:36:11 +01:00
Claire
48f2597a36 [Glitch] Hashtag fixes
Port b01d21c4d4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:35:41 +01:00
Echo
12c487cc3e [Glitch] Fix props in DisplayName component
Port 9c7d09993d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:34:56 +01:00
Echo
adfa407f6b [Glitch] Emoji: Remove final flag
Port 85d0cdb5f7 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:34:14 +01:00
Claire
c43a5a1834 [Glitch] Fix mention matching ignoring path
Port 3ccb6632f2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:20:52 +01:00
Claire
52e2d24a4b [Glitch] Fix URL comparison for mentions in case of empty path
Port 3bf99b8a4a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:20:27 +01:00
Echo
f94353e1e3 [Glitch] Emoji: Fix Web Worker import
Port d0d09fd3a5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:19:21 +01:00
Eugen Rochko
0565eb62d6 [Glitch] Fix hashtags not being picked up when full-width hash sign is used
Port 779a1f8448 to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 22:19:01 +01:00
Claire
48ec31bec8 Merge commit '4896d2c4c6d3bd6b878c5a075b6611c65d4203b2' into glitch-soc/merge-upstream
Conflicts:
- `app/views/settings/preferences/appearance/show.html.haml`:
  Upstream changed stuff too close to glitch-soc's theming system changes.
  Applied upstream's changes.
- `streaming/index.js`:
  Upstream refactored a bunch of stuff where our code was different due to
  local-only posts.
  Applied upstream's changes while taking care of local-only posts.
2025-10-28 22:10:12 +01:00
Claire
3bd56b92c1 Reimplement misleading link tagging in new HTML handling code (#3254) 2025-10-28 21:59:53 +01:00
Claire
70b8281730 Reimplement mention rewriting in new HTML handling code (#3247) 2025-10-28 21:22:36 +01:00
Claire
fb9e33099f Merge pull request #3253 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 1dead10312
2025-10-28 20:54:49 +01:00
Claire
79169408b0 Fix tests on glitch-soc 2025-10-28 20:39:10 +01:00
Renaud Chaput
5a051d07c6 [Glitch] Add a new setting to choose the server landing page
Port 779a1f8448 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-28 20:26:40 +01:00
Claire
5f3f75559f Merge commit '1dead10312caa0cc7719cb80052af549ddf3e6a1' into glitch-soc/merge-upstream 2025-10-28 20:25:09 +01:00
Claire
4896d2c4c6 Change firehose labels depending on which feeds are accessible (#36607) 2025-10-28 16:59:37 +00:00
Renaud Chaput
795aaa14bf Remove environment variables to config Fetch All Replies behaviour (#36627) 2025-10-28 15:58:18 +00:00
diondiondion
e1bd9b944a chore(deps): update dependency eslint-plugin-jsdoc to v60 (#36466) 2025-10-28 15:17:33 +00:00
Claire
26ec19a649 Change styling of column banners (#36531) 2025-10-28 14:45:46 +00:00
Claire
b01d21c4d4 Hashtag fixes (#36625) 2025-10-28 14:26:08 +00:00
Claire
3ccb6632f2 Fix mention matching ignoring path (#36626) 2025-10-28 14:05:39 +00:00
Claire
8fb524e07f Add support for Update of converted object types (#36322) 2025-10-28 14:05:14 +00:00
Echo
9c7d09993d Fix props in DisplayName component (#36622) 2025-10-28 14:02:37 +00:00
renovate[bot]
3efc747be3 Update dependency axios to v1.13.0 (#36612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 13:26:05 +00:00
renovate[bot]
1f5cdb30c7 Update dependency @vitejs/plugin-react to v5.1.0 (#36600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 13:23:38 +00:00
renovate[bot]
3cace4098a Update dependency devise-two-factor to v6.2.0 (#36574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 13:23:32 +00:00
Claire
ccfac2716d Add streaming server side filtering for live/topic feed settings (#36585) 2025-10-28 13:23:05 +00:00
diondiondion
422fa1cf9f Revert "Fix custom emoji width (#27969)" (#36620) 2025-10-28 12:36:22 +00:00
renovate[bot]
2b5f6838ed Update dependency annotaterb to v4.20.0 (#36527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 11:59:21 +00:00
Echo
85d0cdb5f7 Emoji: Remove final flag (#36409) 2025-10-28 11:33:27 +00:00
renovate[bot]
e4fc18abfd Update dependency simple_form to v5.4.0 (#36604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 11:29:07 +00:00
renovate[bot]
e322c1777b Update dependency webmock to v3.26.0 (#36605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 11:27:54 +00:00
Nicholas La Roux
f53c4db05c [Vite] Remove overridden build.target in favor of legacy plugin defaults (#36611) 2025-10-28 11:22:41 +00:00
renovate[bot]
4905c194b8 Update dependency mail to v2.9.0 (#36575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 08:32:36 +00:00
renovate[bot]
7ba06a661c Update dependency @reduxjs/toolkit to v2.9.2 (#36572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 08:32:29 +00:00
github-actions[bot]
5d00ae7eb3 New Crowdin Translations (automated) (#36617)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-28 08:29:36 +00:00
Claire
4b42fe6aba Change API behavior of reblogs wrt. quotes for consistency (#36559) 2025-10-28 08:05:23 +00:00
Claire
3bf99b8a4a Fix URL comparison for mentions in case of empty path (#36613) 2025-10-27 18:19:52 +00:00
Echo
d0d09fd3a5 Emoji: Fix Web Worker import (#36603) 2025-10-27 17:36:01 +00:00
Eugen Rochko
76053fb4a9 Fix hashtags not being picked up when full-width hash sign is used (#36103)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-10-27 17:18:01 +00:00
David Roetzel
402686c76c Remove http_message_signatures feature flag (#36610) 2025-10-27 16:06:44 +00:00
marousta
dc851c9efc Fix custom emoji width (#27969) 2025-10-27 15:56:06 +00:00
Eugen Rochko
1dead10312 Change min. characters required for logged-out account search from 5 to 3 (#36487) 2025-10-27 15:52:21 +00:00
M.J. Fieggen (Joni)
e8382c7332 Fix layout of severed relationships when purged events are listed (#36593) 2025-10-27 15:19:38 +00:00
Eugen Rochko
bfcf21e915 Fix vacuums being interrupted by a single batch failure (#36606) 2025-10-27 14:22:54 +00:00
Matt Jankowski
b60bae6361 Handle unreachable network error for search services (#36587) 2025-10-27 13:28:56 +00:00
Claire
38f15a89fe Fix recent settings migrations (#36602) 2025-10-27 12:24:24 +00:00
renovate[bot]
ab5b7e3776 Update dependency webauthn to v3.4.3 (#36599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 10:21:12 +00:00
renovate[bot]
1230d05b18 Update dependency rubyzip to v3.2.1 (#36598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 10:21:08 +00:00
Renaud Chaput
779a1f8448 Add a new setting to choose the server landing page (#36588) 2025-10-27 10:16:59 +00:00
github-actions[bot]
e40ca321ed New Crowdin Translations (automated) (#36590)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-27 09:45:37 +00:00
renovate[bot]
5f837001e6 Update opentelemetry-ruby (non-major) (#36557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 09:40:13 +00:00
Matt Jankowski
2640cf5317 Update stoplight to version 5.4.0 (#36581) 2025-10-27 09:38:01 +00:00
Claire
7f19b5ca2b Merge pull request #3252 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 1ba579b0a1
2025-10-24 11:27:13 +02:00
diondiondion
305f1e5757 [Glitch] Fix "new post highlighting" in threads being applied when navigating between posts
Port 1ba579b0a1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-23 18:45:53 +02:00
Claire
b11bd2bdbb [Glitch] Add UI support for disabled live feeds
Port 2fa5dd6d1f to glitch-soc

Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-23 18:45:27 +02:00
Claire
deed31ba8c Merge commit '1ba579b0a181fbfff514ef32b50179d2ab1fc342' into glitch-soc/merge-upstream
Conflicts:
- `app/models/public_feed.rb`:
  Minor conflict due to glitch-soc's local-only posts.
  Adopted upstream's changes.
- `spec/models/tag_feed_spec.rb`:
  Minor conflict due to glitch-soc's local-only posts.
  Adopted upstream's changes.
2025-10-23 18:33:52 +02:00
diondiondion
1ba579b0a1 Fix "new post highlighting" in threads being applied when navigating between posts (#36583) 2025-10-23 15:52:07 +00:00
Claire
6b2051b7b3 Fix bookmarks export when one bookmarked status is soft-deleted (#36576) 2025-10-23 11:51:23 +00:00
Claire
2fa5dd6d1f Add UI support for disabled live feeds (#36577)
Co-authored-by: diondiondion <mail@diondiondion.com>
2025-10-23 09:59:43 +00:00
github-actions[bot]
f7b99cd48a New Crowdin Translations (automated) (#36569)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-23 09:29:02 +00:00
renovate[bot]
92aeecfbdc Update dependency vite to v7.1.12 (#36573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 08:41:34 +00:00
Claire
7774cd6670 Add disabled setting for live and topic feeds, as well as user permission to bypass that (#36563) 2025-10-23 08:37:05 +00:00
Claire
c6e2ac5af9 Merge pull request #3251 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 9f7075a0ce
2025-10-23 10:36:11 +02:00
Claire
ee87afd6a4 [Glitch] Remove unnecessary restrictions on HTML handling
Port 9f7075a0ce to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-22 18:08:21 +02:00
diondiondion
2d8b7a7fd8 [Glitch] Fix text overflow alignment for long author names in News
Port 7538bc77b7 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-22 18:08:00 +02:00
diondiondion
cbc07af929 [Glitch] Refresh thread replies periodically & when refocusing window
Port 7ea2af6ae2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-22 18:07:34 +02:00
Claire
ee9a15031b Merge commit '9f7075a0ce2b6ecef8d92ef318785fa8ce708688' into glitch-soc/merge-upstream 2025-10-22 18:06:32 +02:00
Claire
9f7075a0ce Remove unnecessary restrictions on HTML handling (#36548) 2025-10-22 13:55:41 +00:00
renovate[bot]
c40648f7b3 Update dependency pino to v9.14.0 (#36529)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 13:53:20 +00:00
renovate[bot]
2bd5c2f528 Update dependency ioredis to v5.8.2 (#36544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 13:52:59 +00:00
renovate[bot]
1e28ec628b Update dependency rubocop to v1.81.6 (#36541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 13:52:43 +00:00
diondiondion
7538bc77b7 Fix text overflow alignment for long author names in News (#36562) 2025-10-22 13:08:51 +00:00
diondiondion
7ea2af6ae2 Refresh thread replies periodically & when refocusing window (#36547) 2025-10-22 09:43:03 +00:00
belatedly
6adbd9ce52 Fix discovery preamble missing word in EN and EN-GB locales (#36560) 2025-10-22 09:18:02 +00:00
github-actions[bot]
08ae77fd9c New Crowdin Translations (automated) (#36556)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-22 08:24:40 +00:00
Claire
a0aa5fe8ea Merge pull request #3250 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 17eb1a7e66
2025-10-21 21:36:50 +02:00
Claire
209434cb1d Merge commit '17eb1a7e668dbba6e79612395b99407e8e8de6b9' into glitch-soc/merge-upstream 2025-10-21 18:15:56 +02:00
Claire
17eb1a7e66 Fix scheduled quote posts being posted as non-quote posts (#36550) 2025-10-21 16:00:40 +00:00
Claire
aba30a85be Fix value of quote_approval_policy and quoted_status_id in ScheduledStatus serializer (#36549) 2025-10-21 16:00:30 +00:00
Renaud Chaput
de80a54555 Update recommended Node version to 24 (LTS) (#36539) 2025-10-21 14:26:24 +00:00
Renaud Chaput
b80ec3721d Drop support for PostgreSQL 13 (#36540) 2025-10-21 14:26:00 +00:00
Claire
f4ca3e6c74 Merge pull request #3249 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 118ff13bd0
2025-10-21 16:13:52 +02:00
Claire
5674100f0b Merge commit '118ff13bd028cfb19370a12edb9307ea96007ad7' into glitch-soc/merge-upstream 2025-10-21 15:57:43 +02:00
Claire
118ff13bd0 Bump version to v4.5.0-beta.2 (#36543) 2025-10-21 13:43:38 +00:00
Claire
405a49df44 Merge commit from fork
* Refuse granting quote authorization for reblogs

* Add validation to reject quotes of reblogs

* Do not process quotes of reblogs as potentially valid quotes

* Refuse to serve quoted reblogs over REST API
2025-10-21 15:00:28 +02:00
github-actions[bot]
2b9e4294fe New Crowdin Translations (automated) (#36538)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-21 08:50:24 +00:00
Matt Jankowski
2eccd7b53c Specs for validation error in API responses (#36507) 2025-10-21 07:33:30 +00:00
renovate[bot]
74172ced81 Update dependency playwright to v1.55.1 [SECURITY] (#36534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 07:20:24 +00:00
Claire
a74b3c549a Merge pull request #3246 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 341ea7f462
2025-10-20 22:13:15 +02:00
Echo
8b3ff4f8b1 [Glitch] Emoji: Fix unneeded re-renders when StatusContent changes
Port 6e2973aa2d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-20 21:40:05 +02:00
Claire
af1b658c20 [Glitch] Fix position of quote dropdown menu item when “quick boosting” is enabled
Port 97c8cc5606 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-20 21:38:56 +02:00
Echo
7b44fd6d3c [Glitch] Emoji: Swap mode test to an emoji from 16.0
Port 50dfab30c2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-20 21:38:37 +02:00
Claire
9080083263 Merge commit '341ea7f462c68ebe2fc5ee15dc7c58aa5775d5a0' into glitch-soc/merge-upstream 2025-10-20 21:37:50 +02:00
renovate[bot]
341ea7f462 Update dependency vite to v7.1.11 (#36526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 17:07:28 +00:00
renovate[bot]
168cba35e3 Update dependency puma to v7.1.0 (#36519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 17:07:11 +00:00
Echo
6e2973aa2d Emoji: Fix unneeded re-renders when StatusContent changes (#36532) 2025-10-20 14:52:27 +00:00
Claire
97c8cc5606 Fix position of quote dropdown menu item when “quick boosting” is enabled (#36528) 2025-10-20 13:05:40 +00:00
Echo
50dfab30c2 Emoji: Swap mode test to an emoji from 16.0 (#36530) 2025-10-20 13:04:38 +00:00
renovate[bot]
53e20d5c83 Update dependency jsdom to v27.0.1 (#36524)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 12:11:04 +00:00
renovate[bot]
0a1111d5a5 Update dependency @reduxjs/toolkit to v2.9.1 (#36515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 12:10:45 +00:00
renovate[bot]
95be29d700 Update dependency rubocop-performance to v1.26.1 (#36525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 10:56:54 +00:00
Ben Sheldon [he/him]
843c43c97a Replace ThreadingHelper wait loop with functional CyclicBarrier (#36508) 2025-10-20 10:10:43 +00:00
github-actions[bot]
82483ed8b0 New Crowdin Translations (automated) (#36520)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-20 10:10:37 +00:00
Claire
4c5e9e2419 Merge pull request #3245 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 612771de46
2025-10-18 21:33:35 +02:00
Claire
5123f8aa94 [Glitch] Fix relationship not being fetched to evaluate whether to show a quote post
Port 811c1eaf7e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-17 19:08:59 +02:00
diondiondion
460222e8e1 [Glitch] Add new "quick boosting" setting
Port 6dad80eb8c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-17 19:08:20 +02:00
Claire
22e3c0e745 Merge commit '612771de465da60dbd58790adc6b1556a7fa739b' into glitch-soc/merge-upstream
Conflicts:
- `app/models/user_settings.rb`:
  Upstream added a setting adjacent to a glitch-soc-only setting.
  Added upstream's setting while keeping ours.
- `app/serializers/initial_state_serializer.rb`:
  Upstream added a setting adjacent to a glitch-soc-only setting.
  Added upstream's setting while keeping ours.
2025-10-17 19:00:18 +02:00
Claire
84563e54cf Merge pull request #3244 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to c96e28a41d
2025-10-17 18:58:06 +02:00
diondiondion
e1b109f074 [Glitch] Add keyboard shortcut info banner to the boosting preferences section
Port 20961c7538 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-17 18:39:19 +02:00
Echo
d81bdb96e0 [Glitch] Emoji: Fix emoji picker not centering native emoji
Port 63dc426fae to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-17 18:38:43 +02:00
Emelia Smith
2b2de5cdb1 [Glitch] Implement quote posts in Moderator UI
Port 210b389643 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-17 18:38:19 +02:00
Claire
4c0104084d Merge commit 'c96e28a41d6f3dee898b09ab1b250ac5b5dfd9e4' into glitch-soc/merge-upstream 2025-10-17 18:36:27 +02:00
Claire
cfee957dd1 Merge pull request #3243 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 51d0bfcb38
2025-10-17 18:35:17 +02:00
Echo
90ab9fe13c [Glitch] Emoji: Update Twemoji to v16
Port 51d0bfcb38 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-17 18:13:18 +02:00
Claire
515ce38a52 Merge commit '51d0bfcb385b49bfdd30b742d05e8e37257168d6' into glitch-soc/merge-upstream 2025-10-17 18:11:59 +02:00
renovate[bot]
612771de46 Update dependency rack-attack to v6.8.0 (#36471)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 14:10:42 +00:00
renovate[bot]
950e7beeea Update dependency rubyzip to v3.2.0 (#36472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 14:10:39 +00:00
renovate[bot]
1018a4def4 Update dependency vite-plugin-static-copy to v3.1.4 (#36480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 14:10:36 +00:00
Claire
811c1eaf7e Fix relationship not being fetched to evaluate whether to show a quote post (#36517) 2025-10-17 13:03:18 +00:00
diondiondion
6dad80eb8c Add new "quick boosting" setting (#36516) 2025-10-17 13:02:47 +00:00
Claire
c96e28a41d Change HttpMessageSignature to perform assertions directly on Linzer objects (#36510) 2025-10-17 08:41:28 +00:00
github-actions[bot]
ccac6da3e8 New Crowdin Translations (automated) (#36513)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-17 08:41:07 +00:00
Claire
ff8f0135b3 Merge pull request #3242 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 7530f06dee
2025-10-16 20:14:24 +02:00
Echo
be5c1cceea [Glitch] Emoji: Fix autoplay incorrectly being applied
Port 7530f06dee to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-16 19:26:41 +02:00
diondiondion
614dd00457 [Glitch] Restructure appearance settings to introduce new Advanced settings section
Port SCSS changes from 05244c335d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-16 19:26:07 +02:00
Claire
323c6e51fe Merge commit '7530f06deea63a03e0ca05f777f0f1582372c305' into glitch-soc/merge-upstream
Conflicts:
- `app/views/settings/preferences/appearance/show.html.haml`:
  Upstream reorganized the whole page, while glitch-soc had an extra setting.
  Adopted upstream's redesign and moved the extra setting where it made sense.
2025-10-16 19:23:29 +02:00
Claire
cf16039ea1 Merge pull request #3241 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 241ad1c587
2025-10-16 19:21:19 +02:00
Claire
383445c977 [Glitch] Fix pinned hashtag columns fully refreshing unprompted
Port ef53dcfd8c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-16 18:38:10 +02:00
diondiondion
2fb14cbb2b [Glitch] Show new replies early if the fetch-all-replies task takes long to finish
Port 869eeecfee to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-16 18:37:48 +02:00
diondiondion
0512ffcbcd [Glitch] Fix low-contrast hover colour of alert actions (light theme only)
Port 28a42bb62c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-16 18:37:13 +02:00
Claire
f56fc1bab4 Merge commit '241ad1c5878161f5f4a19c16ed6c042f1c2cb6c2' into glitch-soc/merge-upstream 2025-10-16 18:36:25 +02:00
diondiondion
20961c7538 Add keyboard shortcut info banner to the boosting preferences section (#36506) 2025-10-16 14:53:23 +00:00
Echo
63dc426fae Emoji: Fix emoji picker not centering native emoji (#36502) 2025-10-16 13:45:20 +00:00
Emelia Smith
210b389643 Implement quote posts in Moderator UI (#35964) 2025-10-16 13:40:24 +00:00
Echo
51d0bfcb38 Emoji: Update Twemoji to v16 (#36501) 2025-10-16 13:15:16 +00:00
Echo
7530f06dee Emoji: Fix autoplay incorrectly being applied (#36503) 2025-10-16 12:46:17 +00:00
renovate[bot]
28339cad6d Update dependency rollup-plugin-visualizer to v6.0.5 (#36499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 10:29:13 +00:00
github-actions[bot]
5404f92cee New Crowdin Translations (automated) (#36494)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-16 10:29:07 +00:00
diondiondion
05244c335d Restructure appearance settings to introduce new Advanced settings section (#36496) 2025-10-16 09:44:24 +00:00
Claire
241ad1c587 Update docker-compose.yml sidekiq health check to work for both 4.4 and 4.5 (#36498) 2025-10-16 09:24:22 +00:00
Claire
ef53dcfd8c Fix pinned hashtag columns fully refreshing unprompted (#36497) 2025-10-16 09:10:53 +00:00
diondiondion
869eeecfee Show new replies early if the fetch-all-replies task takes long to finish (#36481) 2025-10-15 17:30:47 +00:00
Claire
24089589b7 Merge pull request #3239 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 5a8ab0a3e6
2025-10-15 17:41:44 +02:00
diondiondion
28a42bb62c Fix low-contrast hover colour of alert actions (light theme only) (#36484) 2025-10-15 15:38:36 +00:00
Aung Htet Nay
905aa9434d Change FFmpeg source to GitHub mirror in Dockerfile (#36424) 2025-10-15 15:25:39 +00:00
Matt Jankowski
ef3f2ced09 [Glitch] Update stylelint-config-standard-scss to version 16.0.0
Port c6de46d12d to glitch-soc

Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-15 16:42:23 +02:00
Emelia Smith
2ccab68636 [Glitch] Fix rendering of poll options in status history modal
Port d8bdce2835 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-15 16:37:45 +02:00
Claire
ef413ef713 Merge commit '5a8ab0a3e62a8825e28bb74c1760ba6488d20c97' into glitch-soc/merge-upstream 2025-10-15 16:35:37 +02:00
Claire
5a8ab0a3e6 Update changelog with last-minute changes (#36482) 2025-10-15 14:12:33 +00:00
Claire
c5fb080ab8 Bump version to v4.5.0-beta.1 (#36479) 2025-10-15 13:09:25 +00:00
Claire
fab0dd0bcf Change redirection for denied registration from web app to sign-in page with error message (#36384) 2025-10-15 11:37:22 +00:00
Matt Jankowski
c6de46d12d Update stylelint-config-standard-scss to version 16.0.0 (#36429)
Co-authored-by: diondiondion <mail@diondiondion.com>
2025-10-15 11:30:09 +00:00
Claire
3160f5746d Bump version to v4.4.7 (#36478) 2025-10-15 09:11:03 +00:00
Emelia Smith
d8bdce2835 Fix rendering of poll options in status history modal (#35633) 2025-10-15 09:05:10 +00:00
github-actions[bot]
339673d533 New Crowdin Translations (automated) (#36476)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-15 08:54:47 +00:00
renovate[bot]
d0d1fcd034 Update opentelemetry-ruby (non-major) (#36477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 07:56:45 +00:00
renovate[bot]
3550508421 Update dependency core-js to v3.46.0 (#36417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 07:55:10 +00:00
renovate[bot]
e3e5067772 Update formatjs monorepo (#36415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 07:11:45 +00:00
renovate[bot]
5fd380096f Update dependency vite-plugin-pwa to v1.1.0 (#36452)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 07:10:52 +00:00
renovate[bot]
d788e45628 Update dependency vite to v7.1.10 (#36468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 07:08:49 +00:00
Claire
b4aae2262b Merge pull request #3236 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 50743cc35b
2025-10-14 18:37:22 +02:00
Claire
264d068d8d Change new accounts to use new ActivityPub numeric ID scheme (#36365) 2025-10-14 16:36:55 +00:00
Claire
c4c906eb0c [Glitch] Fix error in logged-out hashtag view when remote posts require log-in
Port 3232eee358 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-14 18:05:28 +02:00
Claire
7330ec670b [Glitch] Fix WebUI mistakenly allowing to attach quotes when editing
Port 156f031ed0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-14 18:04:48 +02:00
Echo
8d1e67b6b2 [Glitch] Emoji: Cleanup new code
Port 0c64e7f75e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-14 18:04:18 +02:00
diondiondion
a7f207604a [Glitch] Fix videos not being indented properly in thread view
Port 9a001e7839 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-14 18:03:25 +02:00
Claire
302e3fe8be Merge commit '50743cc35be19deae9356bd21f1143e3a9b4fbb8' into glitch-soc/merge-upstream 2025-10-14 18:01:40 +02:00
Claire
50743cc35b Fix forwarder being called with nil status when quote post is soft-deleted (#36463) 2025-10-14 15:15:45 +00:00
Claire
fd516347cb Fix deletion of posts quoting soft-deleted local post (#36461) 2025-10-14 15:15:38 +00:00
Claire
3232eee358 Fix error in logged-out hashtag view when remote posts require log-in (#36467) 2025-10-14 14:04:18 +00:00
github-actions[bot]
484225895f New Crowdin Translations (automated) (#36457)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-14 12:26:21 +00:00
Claire
156f031ed0 Fix WebUI mistakenly allowing to attach quotes when editing (#36464) 2025-10-14 12:21:13 +00:00
renovate[bot]
ad858ebe81 Update dependency strong_migrations to v2.5.1 (#36458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 12:17:40 +00:00
Jonathan de Jong
f2f711deeb Fix allow_referrer_origin typo (#36460) 2025-10-14 12:07:15 +00:00
Claire
44ecc4b1e3 Fix moderation warning e-mails that include posts (#36462) 2025-10-14 11:48:51 +00:00
Echo
0c64e7f75e Emoji: Cleanup new code (#36402) 2025-10-14 09:36:25 +00:00
Claire
b0ef7ee922 Merge pull request #3235 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to edd7fd9872
2025-10-14 11:20:14 +02:00
diondiondion
9a001e7839 Fix videos not being indented properly in thread view (#36459) 2025-10-14 08:58:03 +00:00
Echo
ed456a6446 [Glitch] Emoji: Picker native rendering
Port edd7fd9872 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-13 18:07:21 +02:00
Claire
af82db5271 Merge commit 'edd7fd98722a0d49ea486aa790a186735e491f35' into glitch-soc/merge-upstream 2025-10-13 18:07:08 +02:00
Echo
edd7fd9872 Emoji: Picker native rendering (#36454) 2025-10-13 15:29:39 +00:00
Eugen Rochko
33f739da44 Fix permalink redirects continuing to work for suspended accounts (#36453) 2025-10-13 15:18:01 +00:00
Claire
12c5e892f8 Merge pull request #3234 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 254fff93ca
2025-10-13 17:07:56 +02:00
Claire
be762da45f Merge commit '254fff93ca3604438a94a453bedfe6f499e2cd66' into glitch-soc/merge-upstream 2025-10-13 16:51:40 +02:00
Claire
254fff93ca Bump version to v4.4.6 (#36447) 2025-10-13 14:31:12 +00:00
Claire
2971ac9863 Fix streaming still being authorized for suspended accounts (#36448) 2025-10-13 13:35:44 +00:00
Claire
3354f4ce8b Merge pull request #3231 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 7e98fa9b47
2025-10-13 14:44:20 +02:00
Claire
8921494571 Merge commit '7e98fa9b476fdaed235519f1d527eb956004ba0c' into glitch-soc/merge-upstream 2025-10-13 14:26:11 +02:00
Emelia Smith
7e98fa9b47 Merge commit from fork
* Require read, read:statuses or read:notifications scope to access streaming APIs

* Add additional tests for scope-based channel access

We were missing tests in the affirmative for subscribing to the user:notification channel, this adds them
2025-10-13 14:20:57 +02:00
Emelia Smith
24dcb18013 Merge commit from fork
* Ensure tootctl revokes sessions, access tokens and web push subscriptions

* Fix test coverage
2025-10-13 14:20:23 +02:00
Emelia Smith
8d09e4ef23 Merge commit from fork
* Streaming: Ensure disabled users cannot connect to streaming

* Streaming: Disconnect when the user is disabled
2025-10-13 14:19:14 +02:00
renovate[bot]
692cfe27fa Update dependency opentelemetry-instrumentation-excon to v0.25.2 (#36436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 10:01:00 +00:00
github-actions[bot]
8b78c033e8 New Crowdin Translations (automated) (#36433)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-13 09:25:52 +00:00
Claire
ab93e9fc8a Update dependency rack (#36443) 2025-10-13 09:13:17 +00:00
Claire
807a4bdec9 Merge pull request #3227 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 0219b7cad7
2025-10-10 21:32:34 +02:00
Emelia Smith
5dff3414ce [Glitch] Improve display of content warnings in Admin UI
Port 8898f120dc to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-10 19:01:38 +02:00
Emelia Smith
adc0e15167 [Glitch] Add support for displaying link previews for Admin UI
Port 81350c7cfb to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-10 19:01:38 +02:00
Claire
ee88f8550c Merge commit '0219b7cad7d9ef800f82cc561571b70da040433f' into glitch-soc/merge-upstream 2025-10-10 19:01:35 +02:00
github-actions[bot]
bcccfb19df New Crowdin Translations (automated) (#3224)
* New Crowdin translations

* Fix bogus no.yml

* Fix bogus simple_form.no.yml

---------

Co-authored-by: GitHub Actions <noreply@github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-10-10 18:52:05 +02:00
Claire
2575ff5ce2 Fix crowdin download script for glitch-soc stable branches (#3225) 2025-10-10 18:44:33 +02:00
Claire
0219b7cad7 Add result_count to Mastodon-Async-Refresh header when needed (#36239) 2025-10-10 12:26:10 +00:00
github-actions[bot]
3f2ee09827 New Crowdin Translations (automated) (#36420)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-10 09:06:57 +00:00
Emelia Smith
8898f120dc Improve display of content warnings in Admin UI (#35935) 2025-10-10 08:55:41 +00:00
Emelia Smith
81350c7cfb Add support for displaying link previews for Admin UI (#35958) 2025-10-10 08:43:48 +00:00
Claire
8aee7fa400 Merge pull request #3221 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to c858fc77ef
2025-10-10 10:29:45 +02:00
Echo
8a50fb02ce [Glitch] Fixes handled link formatting
Port c858fc77ef to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-09 18:03:39 +02:00
Echo
3c3a812a9c [Glitch] Fix: Embed author handle using wrong DisplayName
Port 258869278e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-09 18:03:15 +02:00
Echo
48bb64cde3 [Glitch] Emoji: Fixes issue with handled link not correctly showing remote users
Port 5bc7c4b7e8 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-09 17:59:59 +02:00
Claire
490c89a5a9 Merge commit 'c858fc77ef194be0217fd98eae84efd261dba798' into glitch-soc/merge-upstream 2025-10-09 17:59:13 +02:00
Echo
c858fc77ef Fixes handled link formatting (#36410)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-10-09 14:31:13 +00:00
Echo
258869278e Fix: Embed author handle using wrong DisplayName (#36413) 2025-10-09 14:08:36 +00:00
Claire
d4a4a7177a Fix crash when serializing quotes of deleted posts for ActivityPub (#36381) 2025-10-09 13:52:38 +00:00
Claire
b7c5e60426 Fix quote post state sometimes not being updated through streaming server (#36408) 2025-10-09 09:53:45 +00:00
github-actions[bot]
a459ccf616 New Crowdin Translations (automated) (#36406)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-09 09:12:27 +00:00
renovate[bot]
ba70dcf827 Update docker.io/ruby Docker tag to v3.4.7 (#36407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 08:25:26 +00:00
Matt Jankowski
0152659245 Use tag filter for pending tag count on admin dashboard (#36404) 2025-10-09 08:08:29 +00:00
Claire
44b45ab0b7 Merge pull request #3220 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to b8444d9bb7
2025-10-08 20:15:31 +02:00
Echo
5bc7c4b7e8 Emoji: Fixes issue with handled link not correctly showing remote users (#36403) 2025-10-08 17:17:03 +00:00
Echo
62fc92dfd8 [Glitch] Emoji: Announcements
Port babb7b2b9d to glitch-soc

Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:51:03 +02:00
Echo
c08a874ba9 [Glitch] Emoji: Statuses
Port 0c1ca6c969 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:45:30 +02:00
Echo
c4ef050eb6 [Glitch] Emoji: Account page
Port 6abda76d13 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:29:16 +02:00
diondiondion
c52473eebc [Glitch] Ensure Fetch-all-replies snackbar is shown at the bottom of the screen
Port e4c3854ae8 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:24:39 +02:00
Echo
fc1b407d89 [Glitch] Emoji: Compare history modal
Port e02ea3e110 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:24:07 +02:00
Echo
f870904e5f [Glitch] Emoji: Bypass legacy emoji normalization
Port 3c9b828c71 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:22:47 +02:00
Brad Dunbar
1c278df424 [Glitch] Resolve typescript eslint warning
Port c578a0cb74 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:21:01 +02:00
Echo
7f5232c377 [Glitch] Emoji: Remove re: from handleElement in StatusContent
Port 9027d60420 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:19:40 +02:00
diondiondion
da99ec0eea [Glitch] Fetch all replies: Only display "More replies found" prompt when there really are new replies
Port 474fbb2770 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:19:12 +02:00
Claire
d34b4f3fd0 [Glitch] Add feature to automatically attach quote on eligible link past in Web UI composer
Port cda07686df to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:18:23 +02:00
Echo
d5ce785267 [Glitch] Allow modern_emojis to be enabled purely server-side
Port 68a36d5a57 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:17:07 +02:00
Echo
0281768cfd [Glitch] Emoji: Link Replacement
Port ffac4cb05f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 18:16:38 +02:00
Claire
f800057008 Merge commit 'b8444d9bb7885d75112a3cae74b6a5c711c7d547' into glitch-soc/merge-upstream 2025-10-08 18:07:04 +02:00
Renaud Chaput
b8444d9bb7 Do not automatically run Prettier on the streaming server code. (#36400) 2025-10-08 15:51:53 +00:00
Echo
babb7b2b9d Emoji: Announcements (#36397)
Co-authored-by: diondiondion <mail@diondiondion.com>
2025-10-08 15:07:01 +00:00
renovate[bot]
5c92312d4d Update dependency cross-env to v10.1.0 (#36297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 14:19:27 +00:00
Echo
0c1ca6c969 Emoji: Statuses (#36393) 2025-10-08 14:18:11 +00:00
renovate[bot]
2b213e9b1b Update dependency ruby to v3.4.7 (#36387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 14:17:42 +00:00
renovate[bot]
4fd5b6e73b Update opentelemetry-ruby (non-major) (#36313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 13:15:22 +00:00
Claire
0be0a8898a Fix Update/Delete of quoted status not being forwarded to quoters's followers (#36390) 2025-10-08 12:56:32 +00:00
renovate[bot]
d8f0326b02 Update dependency sidekiq to v8.0.8 (#36388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 12:39:24 +00:00
Claire
987f1e897b Fix JSON payload being potentially mutated when processing interaction policies (#36392) 2025-10-08 12:31:51 +00:00
Echo
6abda76d13 Emoji: Account page (#36385) 2025-10-08 11:11:25 +00:00
github-actions[bot]
3867f3bc61 New Crowdin Translations (automated) (#36386)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-08 10:06:30 +00:00
Renaud Chaput
4fce4337d8 Update rack and uri to the latest release (#36389) 2025-10-08 09:35:36 +00:00
Matt Jankowski
092f46f61a Use bundler version 2.7.2 (#36367) 2025-10-08 08:21:23 +00:00
diondiondion
e4c3854ae8 Ensure Fetch-all-replies snackbar is shown at the bottom of the screen (#36383) 2025-10-07 16:43:40 +00:00
renovate[bot]
aa7bcd3ae3 Update formatjs monorepo (#36356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 15:34:45 +00:00
Claire
bc7119b3cb Remove unused feature flag from sample configuration file (#36382) 2025-10-07 15:34:06 +00:00
Echo
e02ea3e110 Emoji: Compare history modal (#36378) 2025-10-07 15:22:00 +00:00
Echo
3c9b828c71 Emoji: Bypass legacy emoji normalization (#36377) 2025-10-07 15:21:50 +00:00
Brad Dunbar
c578a0cb74 Resolve typescript eslint warning (#36314) 2025-10-07 14:42:15 +00:00
renovate[bot]
da6ae98e57 Update dependency ioredis to v5.8.1 (#36361)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 14:29:58 +00:00
renovate[bot]
fb6fd7b7e1 Update dependency pino to v9.13.1 (#36337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 14:29:19 +00:00
renovate[bot]
d51717c101 Update dependency vite to v7.1.9 (#36332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 14:29:04 +00:00
diondiondion
63bbe4ee16 Display quotes in email notifications (#36379) 2025-10-07 12:50:40 +00:00
github-actions[bot]
adcbab527a New Crowdin Translations (automated) (#36371)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-07 12:37:46 +00:00
Claire
a7f89d13d2 Change index on follows table to improve performance of some queries (#36374) 2025-10-07 12:37:40 +00:00
Renaud Chaput
e8dab026bb Fix quote mailer preview to use the latest quote notification (#36373) 2025-10-07 10:19:53 +00:00
Eugen Rochko
503c4d16bb [Glitch] Fix wrong styles on rules and buttons in the sign-up form
Port 2df86d6413 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-06 20:11:26 +02:00
Claire
f7fa3300ae Merge pull request #3216 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 4dc21d7afd
2025-10-06 20:11:14 +02:00
Claire
e82def7dad [Glitch] Split timeline_preview setting into more granular settings
Port 2d2c525097 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-06 19:31:02 +02:00
nicole mikołajczyk
3b659cfb48 [Glitch] Do not display mute button in hashtag dropdown when unauthenticated
Port 62f91eddf4 to glitch-soc

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-06 19:26:48 +02:00
Claire
73a625c284 Merge commit '4dc21d7afdb485402555908bc72d071f8b25ec36' into glitch-soc/merge-upstream
Conflicts:
- `config/settings.yml`:
  Upstream replaced a setting with 4 new ones, while glitch-soc had modified
  the default value of that setting.
  Removed the old setting and added the new settings, but with defaults
  matching glitch-soc's previous behavior.
2025-10-06 19:24:16 +02:00
Echo
9027d60420 Emoji: Remove re: from handleElement in StatusContent (#36366) 2025-10-06 16:20:15 +00:00
diondiondion
474fbb2770 Fetch all replies: Only display "More replies found" prompt when there really are new replies (#36334) 2025-10-06 14:13:24 +00:00
diondiondion
4a40f81067 Link to local accounts from settings (#36340) 2025-10-06 14:10:26 +00:00
Claire
cda07686df Add feature to automatically attach quote on eligible link past in Web UI composer (#36364) 2025-10-06 13:43:20 +00:00
Echo
68a36d5a57 Allow modern_emojis to be enabled purely server-side (#36342) 2025-10-06 13:34:51 +00:00
Echo
ffac4cb05f Emoji: Link Replacement (#36341) 2025-10-06 09:31:10 +00:00
github-actions[bot]
4dc21d7afd New Crowdin Translations (automated) (#36349)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-06 09:11:31 +00:00
Claire
2d2c525097 Split timeline_preview setting into more granular settings (#36338) 2025-10-06 08:34:05 +00:00
nicole mikołajczyk
62f91eddf4 Do not display mute button in hashtag dropdown when unauthenticated (#36353)
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
2025-10-05 14:24:54 +00:00
Claire
d5097398fe Merge pull request #3215 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 80c8a84740
2025-10-04 20:27:34 +02:00
diondiondion
64393e0878 [Glitch] Allow quotes to be displayed in the featured carousel
Port 0d7af7e1fe to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-04 14:23:54 +02:00
Claire
432af3c25d Merge commit '80c8a847409c73c4e9a086c8f398a984cf5c2137' into glitch-soc/merge-upstream 2025-10-04 14:22:26 +02:00
Claire
80c8a84740 Fix redirect to external object when URL is missing or malformed (#36347) 2025-10-03 17:30:23 +00:00
github-actions[bot]
1561517387 New Crowdin Translations (automated) (#36345)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-03 08:07:10 +00:00
renovate[bot]
5543967e5c chore(deps): update dependency rollup-plugin-visualizer to v6.0.4 (#36336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 07:57:48 +00:00
diondiondion
0d7af7e1fe Allow quotes to be displayed in the featured carousel (#36335) 2025-10-02 13:51:47 +00:00
github-actions[bot]
4809b38f6e New Crowdin Translations (automated) (#36329)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-02 08:34:38 +00:00
renovate[bot]
554dd1a76a chore(deps): update dependency omniauth to v2.1.4 (#36331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 08:25:40 +00:00
Claire
ade87f395c Merge pull request #3214 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to f69ca085db
2025-10-02 10:05:07 +02:00
diondiondion
97065cd266 [Glitch] Add quote/boost dropdown menu to boost button in media modal
Port 669738ef3b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-01 19:48:50 +02:00
Terence Eden
74ee0960a1 [Glitch] Add overflow to .more-from-author
Port 6c3c2714d7 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-01 19:46:49 +02:00
metalhead
be62874934 [Glitch] Add support for dynamic viewport height
Port e4094d9fb2 to glitch-soc

Co-authored-by: l <eh.chahkiev@smartics.ru>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-01 19:46:29 +02:00
Claire
0e97992940 [Glitch] Add UI support for displaying quotes of other people's posts
Port d51723bb57 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-01 19:46:00 +02:00
diondiondion
4f13515468 [Glitch] Update confirmation dialogs for follow button actions "unfollow", "unblock", and "withdraw request"
Port 473bd84c24 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-01 19:43:20 +02:00
Echo
7430e3c74d [Glitch] Emoji Component
Port c12b8f51c1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-01 19:40:20 +02:00
Brad Dunbar
752214622c [Glitch] Convert mastodon/initial_state to TypeScript
Port ac50e5eebc to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-01 19:34:17 +02:00
Claire
17e0c745c6 Merge commit 'f69ca085dbfca2253404574dcdc4dc6c2aaa35c0' into glitch-soc/merge-upstream 2025-10-01 19:13:18 +02:00
Brad Dunbar
f69ca085db ESLint: Ignore .bundle directory (#36321) 2025-10-01 15:48:15 +00:00
Claire
3ee1378932 Fix approval for remote quotes of local posts not being streamed to local users (#36317) 2025-10-01 13:51:43 +00:00
Claire
f03d1bb21f Add numeric_ap_ids feature flag to switch new accounts to numeric AP identifiers (#36316) 2025-10-01 13:09:56 +00:00
diondiondion
669738ef3b Add quote/boost dropdown menu to boost button in media modal (#36315) 2025-10-01 12:18:19 +00:00
renovate[bot]
94a4e9d5a9 chore(deps): update dependency vite-plugin-static-copy to v3.1.3 (#36312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 09:48:26 +00:00
github-actions[bot]
9114e72c50 New Crowdin Translations (automated) (#36308)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-01 09:47:16 +00:00
Terence Eden
6c3c2714d7 Add overflow to .more-from-author (#36310) 2025-10-01 09:23:49 +00:00
metalhead
e4094d9fb2 Add support for dynamic viewport height (#36272)
Co-authored-by: l <eh.chahkiev@smartics.ru>
2025-10-01 08:53:29 +00:00
Claire
a9234d99cd Merge pull request #3213 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 45219dbf64
2025-10-01 10:52:59 +02:00
Claire
cbb94bdfde Merge commit '45219dbf64805746a472e50bb7c9bcb52972ab2a' into glitch-soc/merge-upstream 2025-09-30 18:06:45 +02:00
Claire
d51723bb57 Add UI support for displaying quotes of other people's posts (#36301) 2025-09-30 15:10:09 +00:00
Claire
5af40ff960 Fix some routes for numeric AP identifiers (#36304) 2025-09-30 15:09:59 +00:00
diondiondion
473bd84c24 Update confirmation dialogs for follow button actions "unfollow", "unblock", and "withdraw request" (#36289) 2025-09-30 14:55:25 +00:00
Echo
c12b8f51c1 Emoji Component (#36293) 2025-09-30 13:06:02 +00:00
Brad Dunbar
ac50e5eebc Convert mastodon/initial_state to TypeScript (#36274) 2025-09-30 11:14:58 +00:00
Claire
589af7a1cc Change GET /api/v1/statuses/:id/quotes to allow listing quotes to other people's posts (#36291) 2025-09-30 09:56:03 +00:00
Claire
45219dbf64 Fix spurious notification of local boosters and quoters when updating quote policy (#36299) 2025-09-30 09:40:58 +00:00
renovate[bot]
a6236148d8 chore(deps): update dependency haml-rails to v3 (#36288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 08:44:15 +00:00
Claire
6deeeec2ed Merge pull request #3210 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 150f0fcba5
2025-09-30 10:08:10 +02:00
Emelia Smith
5b97f25a15 Add integration tests for mastodon-streaming (#36025)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Co-authored-by: David Roetzel <david@roetzel.de>
2025-09-30 07:27:09 +00:00
Brad Dunbar
c4cd2c955b [Glitch] Remove shallow prop from Wrapper
Port aae9a5528a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-29 18:44:40 +02:00
Claire
b6c53c1129 Merge commit '150f0fcba5585782e2cac49d971904f02d5e6fbd' into glitch-soc/merge-upstream
Conflicts:
- `app/controllers/follower_accounts_controller.rb`:
  Upstream refactored a part of the code where glitch-soc has changed the code
  to potentially hide followers count.
  Adapt upstream's changes.
2025-09-29 18:41:46 +02:00
Claire
150f0fcba5 Add support for numeric-based URIs for local accounts (#32724) 2025-09-29 12:05:48 +00:00
renovate[bot]
4d7c208da3 chore(deps): update node.js to 22.20 (#36252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:45:31 +00:00
renovate[bot]
dc72719f4c chore(deps): update dependency hiredis-client to v0.26.1 (#36286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:30:10 +00:00
renovate[bot]
9f1a12b749 chore(deps): update dependency @vitejs/plugin-react to v5.0.4 (#36282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:30:01 +00:00
renovate[bot]
adbd57e5a9 chore(deps): update dependency rubyzip to v3.1.1 (#36278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:29:52 +00:00
renovate[bot]
8779bbc4c1 chore(deps): update rubocop (non-major) to v1.81.1 (#36277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:29:37 +00:00
renovate[bot]
bbb698937a chore(deps): update dependency pino to v9.12.0 (#36287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:29:15 +00:00
github-actions[bot]
4d8e848c6a New Crowdin Translations (automated) (#36276)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-09-29 09:16:16 +00:00
Brad Dunbar
aae9a5528a Remove shallow prop from Wrapper (#36275) 2025-09-29 09:10:27 +00:00
Claire
e871d4ebbb Merge pull request #3209 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to a44a3f6d40
2025-09-27 23:47:08 +02:00
diondiondion
a4dc785de3 [Glitch] Update "Follow" button labels
Port cb5bbbfb05 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-27 22:05:42 +02:00
Echo
8cccabb714 [Glitch] Adds new HTMLBlock component
Port e07b9dfdc1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-27 22:05:42 +02:00
Claire
106b0a9d93 Merge commit 'a44a3f6d4047568921469ff9fbd212f553b1e7f4' into glitch-soc/merge-upstream 2025-09-27 21:46:57 +02:00
diondiondion
eea86088cf [Glitch] Fix page being vertically scrollable in Advanced UI
Port 1571514e49 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-27 21:37:25 +02:00
Brad Dunbar
381fc173c7 [Glitch] Refactor getFocusedItemIndex to avoid conditionals that closest already handles
Port 238d74fe81 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-27 21:37:00 +02:00
diondiondion
364aced99e [Glitch] Allow accessing ref of ScrollContainer's child
Port 11bd515648 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-27 21:35:41 +02:00
diondiondion
3cd021d4d9 [Glitch] Replace react-router-scroll-4 with inlined implementation
Port d801cf8e59 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-27 21:34:50 +02:00
Claire
8fad4926c5 Merge commit '1571514e49ec02a57c050612b3bca856f54933fb' into glitch-soc/merge-upstream 2025-09-27 21:29:19 +02:00
Claire
a44a3f6d40 Expand test coverage of ActivityPub::TagManager class (#36260) 2025-09-26 10:00:53 +00:00
diondiondion
cb5bbbfb05 Update "Follow" button labels (#36264) 2025-09-26 10:00:50 +00:00
Echo
e07b9dfdc1 Adds new HTMLBlock component (#36262) 2025-09-26 09:50:59 +00:00
diondiondion
1571514e49 Fix page being vertically scrollable in Advanced UI (#36271) 2025-09-26 09:23:30 +00:00
Claire
5110c6006a Merge pull request #3207 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 6d2493ca7c
2025-09-26 10:55:36 +02:00
Brad Dunbar
238d74fe81 Refactor getFocusedItemIndex to avoid conditionals that closest already handles (#36267) 2025-09-26 08:53:08 +00:00
github-actions[bot]
7431c50566 New Crowdin Translations (automated) (#36270)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-09-26 08:42:28 +00:00
renovate[bot]
c2d426a565 chore(deps): update dependency rubocop to v1.81.0 (#36269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 08:21:06 +00:00
Matt Jankowski
f61d8cb02a Hold usable value lists in admin settings form (#36268) 2025-09-26 08:21:03 +00:00
Claire
40f9873504 [Glitch] Fix not being able to author quotes with CW but no text
Port 66686994c1 to glitch-soc

Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-25 18:51:58 +02:00
renovate[bot]
3eed83b6ff [Glitch] chore(deps): update dependency typescript to ~5.9.0
Port cc54b33720 to glitch-soc

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-25 18:46:12 +02:00
Claire
4053209a05 Merge commit '6d2493ca7c39a5d60edd7109de0c24256f8fff8c' into glitch-soc/merge-upstream 2025-09-25 18:44:23 +02:00
Claire
2c6de7dca4 Merge pull request #3206 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to df72a2dbbe
2025-09-25 18:39:18 +02:00
diondiondion
11bd515648 Allow accessing ref of ScrollContainer's child (#36265) 2025-09-25 16:14:49 +00:00
diondiondion
d801cf8e59 Replace react-router-scroll-4 with inlined implementation (#36253) 2025-09-25 12:26:50 +00:00
renovate[bot]
6d2493ca7c chore(deps): update dependency puma to v7.0.4 (#36240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 10:03:53 +00:00
Claire
66686994c1 Fix not being able to author quotes with CW but no text (#36153)
Co-authored-by: diondiondion <mail@diondiondion.com>
2025-09-25 09:25:00 +00:00
Renaud Chaput
719b2de3c3 Update uuid package to latest version (#36259) 2025-09-25 08:54:02 +00:00
github-actions[bot]
d2bdb03da0 New Crowdin Translations (automated) (#36258)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-09-25 08:53:58 +00:00
renovate[bot]
507e6dc473 fix(deps): update dependency ioredis to v5.8.0 (#36234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 07:58:20 +00:00
renovate[bot]
52d5e628a4 chore(deps): update dependency hiredis-client to v0.26.0 (#36233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 07:57:47 +00:00
renovate[bot]
85213dab47 chore(deps): update yarn to v4.10.3 (#36178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 07:56:33 +00:00
renovate[bot]
8fac87d77c chore(deps): update dependency rails to v8.0.3 (#36230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 07:55:46 +00:00
renovate[bot]
fda3589498 fix(deps): update dependency sass to v1.93.2 (#36231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 07:55:09 +00:00
renovate[bot]
0798d0c95a chore(deps): update dependency pundit to v2.5.2 (#36251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 07:54:58 +00:00
renovate[bot]
33fd8c774b chore(deps): update dependency webauthn to v3.4.2 (#36243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 07:54:41 +00:00
renovate[bot]
e0f7aedf41 chore(deps): update dependency propshaft to v1.3.1 (#36241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 07:54:07 +00:00
renovate[bot]
cc54b33720 chore(deps): update dependency typescript to ~5.9.0 (#36212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: diondiondion <mail@diondiondion.com>
2025-09-25 07:52:37 +00:00
Claire
e2a8b5c30f [Glitch] Fix newly-highlighted replies not being interactable
Port df72a2dbbe to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-24 23:03:44 +02:00
Claire
9db7ad670f Merge commit 'df72a2dbbec8173515568c02427076ebff5c2297' into glitch-soc/merge-upstream 2025-09-24 23:03:32 +02:00
Claire
df72a2dbbe Fix newly-highlighted replies not being interactable (#36256) 2025-09-24 20:37:20 +00:00
diondiondion
ea3f6ce2e5 [Glitch] Implement new design for "Refetch all"
Port 3a81ee8f5b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-24 18:43:35 +02:00
Echo
7430d399b5 [Glitch] Fix missed event handler
Port 29d9f81e42 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-24 18:42:23 +02:00
diondiondion
26e59bec15 [Glitch] Highlight newly added replies in thread view
Port 059bf1e980 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-24 18:41:09 +02:00
Claire
500619ba4c [Glitch] Remove the outgoing_quotes feature flag, making the feature unconditional
Port e1f7847b64 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-24 18:37:55 +02:00
diondiondion
ee64228963 [Glitch] Fix unfortunate action button wrapping in admin area
Port 6cbc857ee0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-24 18:34:02 +02:00
Echo
351dc60a2f [Glitch] Refactor emoji GIF animation
Port 6bd90940b6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-24 18:33:26 +02:00
Claire
b15b07606c Merge commit '28be5a199f685b9fb26742c4d78ddca53df05d57' into glitch-soc/merge-upstream 2025-09-24 18:26:13 +02:00
Claire
28be5a199f Fix Private Messages self-quoting private posts being changed to followers-only (#36249) 2025-09-24 10:58:52 +00:00
diondiondion
3a81ee8f5b Implement new design for "Refetch all" (#36172) 2025-09-24 09:54:07 +00:00
Echo
29d9f81e42 Fix missed event handler (#36248) 2025-09-24 09:37:11 +00:00
diondiondion
059bf1e980 Highlight newly added replies in thread view (#36237) 2025-09-24 09:27:33 +00:00
github-actions[bot]
23a69e3bd7 New Crowdin Translations (automated) (#36246)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-09-24 09:18:50 +00:00
Claire
e1f7847b64 Remove the outgoing_quotes feature flag, making the feature unconditional (#36130) 2025-09-24 08:58:08 +00:00
diondiondion
6cbc857ee0 Fix unfortunate action button wrapping in admin area (#36247) 2025-09-24 08:57:18 +00:00
David Roetzel
37cec638df Update to puma 7 (#36238) 2025-09-23 13:40:04 +00:00
github-actions[bot]
82f5901a3f New Crowdin Translations (automated) (#36228)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-09-23 09:56:46 +00:00
Echo
6bd90940b6 Refactor emoji GIF animation (#36165) 2025-09-23 08:53:14 +00:00
1040 changed files with 23239 additions and 11469 deletions

View File

@@ -318,24 +318,3 @@ MAX_POLL_OPTION_CHARS=100
# -----------------------
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952
# Fetch All Replies Behavior
# --------------------------
# When a user expands a post (DetailedStatus view), fetch all of its replies
# (default: false)
FETCH_REPLIES_ENABLED=false
# Period to wait between fetching replies (in minutes)
FETCH_REPLIES_COOLDOWN_MINUTES=15
# Period to wait after a post is first created before fetching its replies (in minutes)
FETCH_REPLIES_INITIAL_WAIT_MINUTES=5
# Max number of replies to fetch - total, recursively through a whole reply tree
FETCH_REPLIES_MAX_GLOBAL=1000
# Max number of replies to fetch - for a single post
FETCH_REPLIES_MAX_SINGLE=500
# Max number of replies Collection pages to fetch - total
FETCH_REPLIES_MAX_PAGES=500

View File

@@ -9,7 +9,44 @@ permissions:
packages: write
jobs:
check-latest-stable:
runs-on: ubuntu-latest
outputs:
latest: ${{ steps.check.outputs.is_latest_stable }}
steps:
# Repository needs to be cloned to list branches
- name: Clone repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Check latest stable
shell: bash
id: check
run: |
ref="${GITHUB_REF#refs/tags/}"
if [[ "$ref" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then
current="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
else
echo "tag $ref is not semver"
echo "is_latest_stable=false" >> "$GITHUB_OUTPUT"
exit 0
fi
latest=$(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/stable-*.*" \
| sed -E 's#^origin/stable-##' \
| sort -Vr \
| head -n1)
if [[ "$current" == "$latest" ]]; then
echo "is_latest_stable=true" >> "$GITHUB_OUTPUT"
else
echo "is_latest_stable=false" >> "$GITHUB_OUTPUT"
fi
build-image:
needs: check-latest-stable
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: Dockerfile
@@ -20,13 +57,14 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
latest=${{ needs.check-latest-stable.outputs.latest }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
secrets: inherit
build-image-streaming:
needs: check-latest-stable
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
@@ -37,7 +75,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
latest=${{ needs.check-latest-stable.outputs.latest }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}

View File

@@ -9,7 +9,7 @@ permissions:
jobs:
download-translations-stable:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
if: github.repository == 'glitch-soc/mastodon'
steps:
- name: Checkout

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@
/public/packs
/public/packs-dev
/public/packs-test
stats.html
.env
.env.production
node_modules/

2
.nvmrc
View File

@@ -1 +1 @@
22.19
24.10

View File

@@ -1 +1 @@
3.4.6
3.4.7

View File

@@ -50,9 +50,13 @@ const preview: Preview = {
locale: 'en',
},
decorators: [
(Story, { parameters, globals }) => {
(Story, { parameters, globals, args }) => {
// Get the locale from the global toolbar
// and merge it with any parameters or args state.
const { locale } = globals as { locale: string };
const { state = {} } = parameters;
const { state: argsState = {} } = args;
const reducer = reducerWithInitialState(
{
meta: {
@@ -60,7 +64,9 @@ const preview: Preview = {
},
},
state as Record<string, unknown>,
argsState as Record<string, unknown>,
);
const store = configureStore({
reducer,
middleware(getDefaultMiddleware) {

View File

@@ -2,6 +2,252 @@
All notable changes to this project will be documented in this file.
## [4.5.4] - 2026-01-07
### Security
- Fix SSRF protection bypass ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-xfrj-c749-jxxq))
- Fix missing ownership check in severed relationships controller ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-ww85-x9cp-5v24))
### Changed
- Change HTTP Signature verification status from 401 to 503 on temporary failure to get remote actor (#37221 by @ClearlyClaire)
### Fixed
- Fix custom emojis not being rendered in profile fields (#37365 by @ClearlyClaire)
- Fix serialization of context pages (#37376 by @ClearlyClaire)
- Fix quotes with CWs but no text not having fallback link (#37361 by @ClearlyClaire)
- Fix outdated link target for “locked” warning (#37366 by @ClearlyClaire)
- Fix local custom emojis sometimes being rendered in remote posts (#37284 by @ChaosExAnima)
- Fix some assets not being loaded from configured CDN (#37310 by @ChaosExAnima)
- Fix notifications page error in Tor browser (#37285 by @diondiondion)
- Fix custom emojis not being displayed in CWs and fav/boost notifications (#37272 and #37306 by @ChaosExAnima and @ClearlyClaire)
- Fix default `Admin` role not including `view_feeds` permission (#37301 by @ClearlyClaire)
- Fix hashtag autocomplete replacing suggestion's first characters with input (#37281 by @ClearlyClaire)
- Fix mentions of domain-blocked users being processed (#37257 by @ClearlyClaire)
## [4.5.3] - 2025-12-08
### Security
- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8))
### Fixed
- Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases (#37140 by @ClearlyClaire)
- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima)
- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire)
- Fix creation of duplicate conversations (#37108 by @oneiros)
- Fix extraneous `noreferrer` in external links (#37107 by @ChaosExAnima)
- Fix edge case error handling in some database migrations (#37079 by @ClearlyClaire)
- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire)
- Fix post navigation in single-column mode when Advanced UI is enabled (#37044 by @diondiondion)
- Fix `tootctl status remove` removing quoted posts and remote quotes of local posts (#37009 by @ClearlyClaire)
- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire)
- Fix compose autosuggest always lowercasing input token (#36995 by @ClearlyClaire)
## [4.5.2] - 2025-11-20
### Changed
- Change private quote education modal to not show up on self-quotes (#36926 by @ClearlyClaire)
### Fixed
- Fix missing fallback link in CW-only quote posts (#36963 by @ClearlyClaire)
- Fix statuses without text being hidden while loading (#36962 by @ClearlyClaire)
- Fix `g` + `h` keyboard shortcut not working when a post is focused (#36935 by @diondiondion)
- Fix quoting overwriting current content warning (#36934 by @ClearlyClaire)
- Fix scroll-to-status in threaded view being unreliable (#36927 by @ClearlyClaire)
- Fix path resolution for emoji worker (#36897 by @ChaosExAnima)
- Fix `tootctl upgrade storage-schema` failing with `ArgumentError` (#36914 by @shugo)
- Fix cross-origin handling of CSS modules (#36890 by @ClearlyClaire)
- Fix error with remote tags including percent signs (#36886 and #36925 by @ChaosExAnima and @ClearlyClaire)
- Fix bogus quote approval policy not always being replaced correctly (#36885 by @ClearlyClaire)
- Fix hashtag completion not being inserted correctly (#36884 by @ClearlyClaire)
- Fix Cmd/Ctrl + Enter in the composer triggering confirmation dialog action (#36870 by @diondiondion)
## [4.5.1] - 2025-11-13
### Fixed
- Fix Cmd/Ctrl + Enter not submitting Alt text modal on some browsers (#36866 by @diondiondion)
- Fix posts coming from public/hashtag streaming being marked as unquotable (#36860 and #36869 by @ClearlyClaire)
- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire)
- Fix blank screen in browsers that don't support `Intl.DisplayNames` (#36847 by @diondiondion)
- Fix filters not being applied to quotes in detailed view (#36843 by @ClearlyClaire)
- Fix scroll shift caused by fetch-all-replies alerts (#36807 by @diondiondion)
- Fix dropdown menu not focusing first item when opened via keyboard (#36804 by @diondiondion)
- Fix assets build issue on arch64 (#36781 by @ClearlyClaire)
- Fix `/api/v1/statuses/:id/context` sometimes returing `Mastodon-Async-Refresh` without `result_count` (#36779 by @ClearlyClaire)
- Fix prepared quote not being discarded with contents when replying (#36778 by @ClearlyClaire)
## [4.5.0] - 2025-11-06
### Added
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550, #36559, #36693, #36704, #36690, #36689, #36696, #36721, #36695 and #36736 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
This includes a revamp of the composer interface.\
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484, #36481, #36583, #36627 and #36547 by @ClearlyClaire, @diondiondion, @Gargron and @renchap)
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, #36607 and #36703 by @ClearlyClaire)\
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds.\
The setting for local topic feeds has 2 values: `public` and `authenticated`. Every other setting has 3 values: `public`, `authenticated`, `disabled`.\
When `disabled`, users with the “View live and topic feeds” will still be able to view them.
- Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm)
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
- Add a new server setting to choose the server landing page (#36588 and #36602 by @ClearlyClaire and @renchap)
- Add support for `Update` activities on converted object types (#36322 by @ClearlyClaire)
- Add support for dynamic viewport height (#36272 by @e1berd)
- Add support for numeric-based URIs for new local accounts (#32724, #36304, #36316, and #36365 by @ClearlyClaire)
- Add default visualizer for audio upload without poster (#36734 by @ChaosExAnima)
- Add Traditional Mongolian to posting languages (#36196 by @shimon1024)
- Add example post with manual quote approval policy to `dev:populate_sample_data` (#36099 by @ClearlyClaire)
- Add server-side support for handling posts with a quote policy allowing followers to quote (#36093 and #36127 by @ClearlyClaire)
- Add schema.org markup to SEO-enabled posts (#36075 by @Gargron)
- Add migration to fill unset default quote policy based on default post privacy (#36041 by @ClearlyClaire)
- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
- Add feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502, #36532, #36603, #36409, #36638 and #36750 by @ChaosExAnima, @ClearlyClaire and @braddunbar)\
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
- Add support for exposing conversation context for new public conversations according to FEP-7888 (#35959 and #36064 by @ClearlyClaire and @jesseplusplus)
- Add digest re-check before removing followers in synchronization mechanism (#34273 by @ClearlyClaire)
- Add support for displaying Valkey version on admin dashboard (#35785 by @ykzts)
- Add delivery failure tracking and handling to FASP jobs (#35625, #35628, and #35723 by @oneiros)
- Add example of quote post with a preview card to development sample data (#35616 by @ClearlyClaire)
- Add second set of blocked text that applies to accounts regardless of account age for spam-blocking (#35563 by @ClearlyClaire)
### Changed
- Change confirmation dialogs for follow button actions “unfollow”, “unblock”, and “withdraw request” (#36289 by @diondiondion)
- Change “Follow” button labels (#36264 by @diondiondion)
- Change appearance settings to introduce new Advanced settings section (#36496 and #36506 by @diondiondion)
- Change display of blocked and muted quoted users (#36619 by @ClearlyClaire)\
This adds `blocked_account`, `blocked_domain` and `muted_account` values to the `state` attribute of `Quote` and `ShallowQuote` REST API entities.
- Change submitting an empty post to show an error rather than failing silently (#36650 by @diondiondion)
- Change "Privacy and reach" settings from "Public profile" to their own top-level category (#27294 by @ChaelCodes)
- Change number of times quote verification is retried to better deal with temporary failures (#36698 by @ClearlyClaire)
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
- Change styling of column banners (#36531 by @ClearlyClaire)
- Change recommended Node version to 24 (LTS) (#36539 by @renchap)
- Change min. characters required for logged-out account search from 5 to 3 (#36487 by @Gargron)
- Change browser target to Vite legacy plugin defaults (#36611 by @larouxn)
- Change index on `follows` table to improve performance of some queries (#36374 by @ClearlyClaire)
- Change links to accounts in settings and moderation views to link to local view unless account is suspended (#36340 by @diondiondion)
- Change redirection for denied registration from web app to sign-in page with error message (#36384 by @ClearlyClaire)
- Change support for RFC9421 HTTP signatures to be enabled unconditionally (#36610 by @oneiros)
- Change wording and design of interaction dialog to simplify it (#36124 by @diondiondion)
- Change dropdown menus to allow disabled items to be focused (#36078 by @diondiondion)
- Change modal background colours in light mode (#36069 by @diondiondion)
- Change “Posting defaults” settings page to enforce `nobody` quote policy for `private` default visibility (#36040 by @ClearlyClaire)
- Change description of “Quiet public” (#36032 by @ClearlyClaire)
- Change “Boost with original visibility” to “Share again with your followers” (#36035 by @ClearlyClaire)
- Change handling of push subscriptions to automatically delete invalid ones on delivery (#35987 by @ThisIsMissEm)
- Change design of quote posts in web UI (#35584 and #35834 by @Gargron)
- Change auditable accounts to be sorted by username in admin action logs interface (#35272 by @breadtk)
- Change order of translation restoration and service credit on post card (#33619 by @colindean)
- Change position of add more to be inside table toolbar on reports (#35963 by @ThisIsMissEm)
- Change docker-compose.yml sidekiq health check to work for both 4.4 and 4.5 (#36498 by @ClearlyClaire)
### Fixed
- Fix relationship not being fetched to evaluate whether to show a quote post (#36517 by @ClearlyClaire)
- Fix rendering of poll options in status history modal (#35633 by @ThisIsMissEm)
- Fix “mute” button being displayed to unauthenticated visitors in hashtag dropdown (#36353 by @mkljczk)
- Fix initially selected language in Rules panel, hide selector when no alternative translations exist (#36672 by @diondiondion)
- Fix URL comparison for mentions in case of empty path (#36613 and #36626 by @ClearlyClaire)
- Fix hashtags not being picked up when full-width hash sign is used (#36103 and #36625 by @ClearlyClaire and @Gargron)
- Fix layout of severed relationships when purged events are listed (#36593 by @mejofi)
- Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716 by @ClearlyClaire)
- Fix vacuum tasks being interrupted by a single batch failure (#36606 by @Gargron)
- Fix handling of unreachable network error for search services (#36587 by @mjankowski)
- Fix bookmarks export when a bookmarked status is soft-deleted (#36576 by @ClearlyClaire)
- Fix text overflow alignment for long author names in News (#36562 by @diondiondion)
- Fix discovery preamble missing word in admin settings (#36560 by @belatedly)
- Fix overflow handling of `.more-from-author` (#36310 by @edent)
- Fix unfortunate action button wrapping in admin area (#36247 by @diondiondion)
- Fix translate button width in Safari (#36164 and #36216 by @diondiondion)
- Fix login page linking to other pages within OAuth authorization flow (#36115 by @Gargron)
- Fix stale search results being displayed in Web UI while new query is in progress (#36053 by @ChaosExAnima)
- Fix YouTube iframe not being able to start at a defined time (#26584 by @BrunoViveiros)
- Fix banned text being able to be circumvented via unicode (#35978 by @Gargron)
- Fix batch table toolbar displaying under status media (#35962 by @ThisIsMissEm)
- Fix incorrect RSS feed MIME type in gzip_types directive (#35562 by @iioflow)
- Fix 404 error after deleting status from detail view (#35800) (#35881 by @crafkaz)
- Fix feeds keyboard navigation issues (#35853, #35864, and #36267 by @braddunbar and @diondiondion)
- Fix layout shift caused by “Who to follow” widget (#35861 by @diondiondion)
- Fix Vagrantfile (#35765 by @ClearlyClaire)
- Fix reply indicator displaying wrong avatar in rare cases (#35756 by @ClearlyClaire)
- Fix `Chewy::UndefinedUpdateStrategy` in `dev:populate_sample_data` task when Elasticsearch is enabled (#35615 by @ClearlyClaire)
- Fix unnecessary account note addition for already-muted moved-to users (#35566 by @mjankowski)
- Fix seeded admin user creation failing on specific configurations (#35565 by @oneiros)
- Fix media modal images in Web UI having redundant `title` attribute (#35468 by @mayank99)
- Fix inconsistent default privacy post setting when unset in settings (#35422 by @oneiros)
- Fix glitchy status keyboard navigation (#35455 and #35504 by @diondiondion)
- Fix post being submitted when pressing “Enter” in the CW field (#35445 by @diondiondion)
### Removed
- Remove support for PostgreSQL 13 (#36540 by @renchap)
## [4.4.8] - 2025-10-21
### Security
- Fix quote control bypass ([GHSA-8h43-rcqj-wpc6](https://github.com/mastodon/mastodon/security/advisories/GHSA-8h43-rcqj-wpc6))
## [4.4.7] - 2025-10-15
### Fixed
- Fix forwarder being called with `nil` status when quote post is soft-deleted (#36463 by @ClearlyClaire)
- Fix moderation warning e-mails that include posts (#36462 by @ClearlyClaire)
- Fix allow_referrer_origin typo (#36460 by @ShadowJonathan)
## [4.4.6] - 2025-10-13
### Security
- Update dependencies `rack` and `uri`
- Fix streaming server connection not being closed on user suspension (by @ThisIsMissEm, [GHSA-r2fh-jr9c-9pxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-r2fh-jr9c-9pxh))
- Fix password change through admin CLI not invalidating existing sessions and access tokens (by @ThisIsMissEm, [GHSA-f3q3-rmf7-9655](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q3-rmf7-9655))
- Fix streaming server allowing access to public timelines even without the `read` or `read:statuses` OAuth scopes (by @ThisIsMissEm, [GHSA-7gwh-mw97-qjgp](https://github.com/mastodon/mastodon/security/advisories/GHSA-7gwh-mw97-qjgp))
### Added
- Add support for processing quotes of deleted posts signaled through a `Tombstone` (#36381 by @ClearlyClaire)
### Fixed
- Fix quote post state sometimes not being updated through streaming server (#36408 by @ClearlyClaire)
- Fix inconsistent “pending tags” count on admin dashboard (#36404 by @mjankowski)
- Fix JSON payload being potentially mutated when processing interaction policies (#36392 by @ClearlyClaire)
- Fix quotes not being displayed in email notifications (#36379 by @diondiondion)
- Fix redirect to external object when URL is missing or malformed (#36347 by @ClearlyClaire)
- Fix quotes not being displayed in the featured carousel (#36335 by @diondiondion)
## [4.4.5] - 2025-09-23
### Security
- Update dependencies
### Added
- Add support for `has:quote` in search (#36217 by @ClearlyClaire)
### Changed
- Change quoted posts from silenced accounts to use a click-through rather than being hidden (#36166 and #36167 by @ClearlyClaire)
### Fixed
- Fix processing of out-of-order `Update` as implicit updates (#36190 by @ClearlyClaire)
- Fix getting `Create` and `Update` out of order (#36176 by @ClearlyClaire)
- Fix quotes with Content Warnings but no text being shown without Content Warnings (#36150 by @ClearlyClaire)
## [4.4.4] - 2025-09-16
### Security

View File

@@ -13,10 +13,10 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.6"
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
ARG RUBY_VERSION="3.4.7"
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"]
# renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22"
ARG NODE_MAJOR_VERSION="24"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="trixie"]
ARG DEBIAN_VERSION="trixie"
# Node.js image to use for base image based on combined variables (ex: 20-trixie-slim)
@@ -183,7 +183,7 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.17.2
ARG VIPS_VERSION=8.17.3
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
@@ -208,12 +208,12 @@ FROM build AS ffmpeg
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
ARG FFMPEG_VERSION=8.0
# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"]
ARG FFMPEG_URL=https://ffmpeg.org/releases
ARG FFMPEG_URL=https://github.com/FFmpeg/FFmpeg/archive/refs/tags
WORKDIR /usr/local/ffmpeg/src
# Download and extract ffmpeg source code
ADD ${FFMPEG_URL}/ffmpeg-${FFMPEG_VERSION}.tar.xz /usr/local/ffmpeg/src/
RUN tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz;
ADD ${FFMPEG_URL}/n${FFMPEG_VERSION}.tar.gz /usr/local/ffmpeg/src/
RUN tar xf n${FFMPEG_VERSION}.tar.gz && mv FFmpeg-n${FFMPEG_VERSION} ffmpeg-${FFMPEG_VERSION};
WORKDIR /usr/local/ffmpeg/src/ffmpeg-${FFMPEG_VERSION}

35
Gemfile
View File

@@ -4,12 +4,12 @@ source 'https://rubygems.org'
ruby '>= 3.2.0', '< 3.5.0'
gem 'propshaft'
gem 'puma', '~> 6.3'
gem 'puma', '~> 7.0'
gem 'rails', '~> 8.0'
gem 'thor', '~> 1.2'
gem 'dotenv'
gem 'haml-rails', '~>2.0'
gem 'haml-rails', '~>3.0'
gem 'pg', '~> 1.5'
gem 'pghero'
@@ -105,20 +105,20 @@ gem 'prometheus_exporter', '~> 2.2', require: false
gem 'opentelemetry-api', '~> 1.7.0'
group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.27.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.37.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.26.0', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.26.0', require: false
gem 'opentelemetry-exporter-otlp', '~> 0.31.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.10.0', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.26.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.27.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.26.0', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.26.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.32.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.39.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.28.0', require: false
gem 'opentelemetry-sdk', '~> 1.4', require: false
end
@@ -160,6 +160,9 @@ group :test do
# Stub web requests for specs
gem 'webmock', '~> 3.18'
# Websocket driver for testing integration between rails/sidekiq and streaming
gem 'websocket-driver', '~> 0.8', require: false
end
group :development do

View File

@@ -10,29 +10,29 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
actioncable (8.0.3)
actionpack (= 8.0.3)
activesupport (= 8.0.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
actionmailbox (8.0.3)
actionpack (= 8.0.3)
activejob (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
mail (>= 2.8.0)
actionmailer (8.0.2.1)
actionpack (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activesupport (= 8.0.2.1)
actionmailer (8.0.3)
actionpack (= 8.0.3)
actionview (= 8.0.3)
activejob (= 8.0.3)
activesupport (= 8.0.3)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.2.1)
actionview (= 8.0.2.1)
activesupport (= 8.0.2.1)
actionpack (8.0.3)
actionview (= 8.0.3)
activesupport (= 8.0.3)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -40,15 +40,15 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.2.1)
actionpack (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
actiontext (8.0.3)
actionpack (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.2.1)
activesupport (= 8.0.2.1)
actionview (8.0.3)
activesupport (= 8.0.3)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@@ -58,22 +58,22 @@ GEM
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (8.0.2.1)
activesupport (= 8.0.2.1)
activejob (8.0.3)
activesupport (= 8.0.3)
globalid (>= 0.3.6)
activemodel (8.0.2.1)
activesupport (= 8.0.2.1)
activerecord (8.0.2.1)
activemodel (= 8.0.2.1)
activesupport (= 8.0.2.1)
activemodel (8.0.3)
activesupport (= 8.0.3)
activerecord (8.0.3)
activemodel (= 8.0.3)
activesupport (= 8.0.3)
timeout (>= 0.4.0)
activestorage (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activesupport (= 8.0.2.1)
activestorage (8.0.3)
actionpack (= 8.0.3)
activejob (= 8.0.3)
activerecord (= 8.0.3)
activesupport (= 8.0.3)
marcel (~> 1.0)
activesupport (8.0.2.1)
activesupport (8.0.3)
base64
benchmark (>= 0.3)
bigdecimal
@@ -90,13 +90,13 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
android_key_attestation (0.3.0)
annotaterb (4.19.0)
annotaterb (4.20.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1135.0)
aws-partitions (1.1168.0)
aws-sdk-core (3.215.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@@ -116,19 +116,19 @@ GEM
base64 (0.3.0)
bcp47_spec (0.2.1)
bcrypt (3.1.20)
benchmark (0.4.1)
benchmark (0.5.0)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.2.3)
bigdecimal (3.3.1)
bindata (2.5.1)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.8)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.0.2)
brakeman (7.1.1)
racc
browser (6.2.0)
builder (3.3.0)
@@ -150,7 +150,7 @@ GEM
playwright-ruby-client (>= 1.16.0)
case_transform (0.2)
activesupport
cbor (0.5.9.8)
cbor (0.5.10.1)
cgi (0.4.2)
charlock_holmes (0.7.9)
chewy (7.6.0)
@@ -168,7 +168,7 @@ GEM
cose (1.3.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
crack (1.0.0)
crack (1.0.1)
bigdecimal
rexml
crass (1.0.6)
@@ -190,10 +190,10 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (6.1.0)
activesupport (>= 7.0, < 8.1)
devise-two-factor (6.2.0)
activesupport (>= 7.0, < 8.2)
devise (~> 4.0)
railties (>= 7.0, < 8.1)
railties (>= 7.0, < 8.2)
rotp (~> 6.0)
devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0)
@@ -207,7 +207,7 @@ GEM
railties (>= 5)
dotenv (3.1.8)
drb (2.2.3)
dry-cli (1.2.0)
dry-cli (1.3.0)
elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11)
elasticsearch-transport (= 7.17.11)
@@ -224,20 +224,20 @@ GEM
mail (~> 2.7)
email_validator (2.2.4)
activemodel
erb (5.0.2)
erb (5.1.3)
erubi (1.13.1)
et-orbi (1.2.11)
et-orbi (1.4.0)
tzinfo
excon (1.2.8)
excon (1.3.0)
logger
fabrication (3.0.0)
faker (3.5.2)
i18n (>= 1.8.11, < 2)
faraday (2.13.4)
faraday (2.14.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.3.0)
faraday-follow_redirects (0.4.0)
faraday (>= 1, < 3)
faraday-httpclient (2.0.2)
httpclient (>= 2.2)
@@ -266,23 +266,24 @@ GEM
fog-openstack (1.1.5)
fog-core (~> 2.1)
fog-json (>= 1.0)
formatador (1.1.1)
formatador (1.2.1)
reline
forwardable (1.3.3)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
fugit (1.12.0)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.2.1)
globalid (1.3.0)
activesupport (>= 6.1)
google-protobuf (4.31.1)
google-protobuf (4.32.1)
bigdecimal
rake (>= 13)
googleapis-common-protos-types (1.20.0)
google-protobuf (>= 3.18, < 5.a)
googleapis-common-protos-types (1.22.0)
google-protobuf (~> 4.26)
haml (6.3.0)
temple (>= 0.8.2)
thor
tilt
haml-rails (2.1.0)
haml-rails (3.0.0)
actionpack (>= 5.1)
activesupport (>= 5.1)
haml (>= 4.0.6)
@@ -293,15 +294,15 @@ GEM
rainbow
rubocop (>= 1.0)
sysexits (~> 1.1)
hashdiff (1.2.0)
hashdiff (1.2.1)
hashie (5.0.0)
hcaptcha (7.1.0)
json
highline (3.1.2)
reline
hiredis (0.6.3)
hiredis-client (0.25.3)
redis-client (= 0.25.3)
hiredis-client (0.26.1)
redis-client (= 0.26.1)
hkdf (0.3.0)
htmlentities (4.3.4)
http (5.3.1)
@@ -309,7 +310,7 @@ GEM
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0)
http-cookie (1.0.8)
http-cookie (1.1.0)
domain_name (~> 0.5)
http-form_data (2.3.0)
http_accept_language (2.1.1)
@@ -336,7 +337,7 @@ GEM
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.8.1)
irb (1.15.2)
irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
@@ -345,9 +346,9 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
json (2.13.2)
json (2.15.1)
json-canonicalization (1.0.0)
json-jwt (1.16.7)
json-jwt (1.17.0)
activesupport (>= 4.2)
aes_key_wrap
base64
@@ -425,7 +426,8 @@ GEM
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
@@ -438,16 +440,16 @@ GEM
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0916)
mime-types-data (3.2025.0924)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.25.5)
minitest (5.26.0)
msgpack (1.8.0)
multi_json (1.17.0)
mutex_m (0.3.0)
net-http (0.6.0)
uri
net-imap (0.5.9)
net-imap (0.5.12)
date
net-protocol
net-ldap (0.20.0)
@@ -466,8 +468,9 @@ GEM
oj (3.16.11)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.3)
omniauth (2.1.4)
hashie (>= 3.4.6)
logger
rack (>= 2.2.3)
rack-protection
omniauth-cas (3.0.2)
@@ -496,102 +499,77 @@ GEM
tzinfo
validate_url
webfinger (~> 2.0)
openssl (3.3.0)
openssl (3.3.2)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.7.0)
opentelemetry-common (0.22.0)
opentelemetry-common (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.30.0)
opentelemetry-exporter-otlp (0.31.1)
google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.2)
opentelemetry-sdk (~> 1.10)
opentelemetry-semantic_conventions
opentelemetry-helpers-sql (0.1.1)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation (0.3.0)
opentelemetry-helpers-sql (0.2.0)
opentelemetry-api (~> 1.7)
opentelemetry-helpers-sql-obfuscation (0.4.0)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.4.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-action_pack (0.13.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rack (~> 0.21)
opentelemetry-instrumentation-action_view (0.9.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_job (0.8.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_model_serializers (0.22.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (0.6.1)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-action_pack (0.15.1)
opentelemetry-instrumentation-rack (~> 0.29)
opentelemetry-instrumentation-action_view (0.11.1)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-active_job (0.10.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-active_model_serializers (0.24.0)
opentelemetry-instrumentation-active_support (>= 0.7.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_record (0.9.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_storage (0.1.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_support (0.8.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_record (0.11.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-active_storage (0.3.1)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-active_support (0.10.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-base (0.25.0)
opentelemetry-api (~> 1.7)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.22.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-excon (0.24.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-faraday (0.28.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http (0.25.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http_client (0.24.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-net_http (0.24.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-pg (0.30.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-concurrent_ruby (0.24.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-excon (0.26.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-faraday (0.30.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http (0.27.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http_client (0.26.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-net_http (0.26.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-pg (0.32.0)
opentelemetry-helpers-sql
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rack (0.27.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rails (0.37.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.4.0)
opentelemetry-instrumentation-action_pack (~> 0.13.0)
opentelemetry-instrumentation-action_view (~> 0.9.0)
opentelemetry-instrumentation-active_job (~> 0.8.0)
opentelemetry-instrumentation-active_record (~> 0.9.0)
opentelemetry-instrumentation-active_storage (~> 0.1.0)
opentelemetry-instrumentation-active_support (~> 0.8.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-instrumentation-redis (0.26.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-sidekiq (0.26.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rack (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rails (0.39.1)
opentelemetry-instrumentation-action_mailer (~> 0.6)
opentelemetry-instrumentation-action_pack (~> 0.15)
opentelemetry-instrumentation-action_view (~> 0.11)
opentelemetry-instrumentation-active_job (~> 0.10)
opentelemetry-instrumentation-active_record (~> 0.11)
opentelemetry-instrumentation-active_storage (~> 0.3)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-concurrent_ruby (~> 0.23)
opentelemetry-instrumentation-redis (0.28.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-sidekiq (0.28.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-registry (0.4.0)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.9.0)
opentelemetry-sdk (1.10.0)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
@@ -615,7 +593,7 @@ GEM
playwright-ruby-client (1.55.0)
concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0)
pp (0.6.2)
pp (0.6.3)
prettyprint
premailer (1.27.0)
addressable
@@ -626,10 +604,10 @@ GEM
net-smtp
premailer (~> 1.7, >= 1.7.9)
prettyprint (0.2.0)
prism (1.4.0)
prism (1.5.2)
prometheus_exporter (2.3.0)
webrick
propshaft (1.2.1)
propshaft (1.3.1)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
@@ -637,14 +615,14 @@ GEM
date
stringio
public_suffix (6.0.2)
puma (6.6.1)
puma (7.1.0)
nio4r (~> 2.0)
pundit (2.5.1)
pundit (2.5.2)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.16)
rack-attack (6.7.0)
rack (3.2.4)
rack-attack (6.8.0)
rack (>= 1.0, < 4)
rack-cors (3.0.0)
logger
@@ -669,20 +647,20 @@ GEM
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.2.1)
actioncable (= 8.0.2.1)
actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.2.1)
actionpack (= 8.0.2.1)
actiontext (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activemodel (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
rails (8.0.3)
actioncable (= 8.0.3)
actionmailbox (= 8.0.3)
actionmailer (= 8.0.3)
actionpack (= 8.0.3)
actiontext (= 8.0.3)
actionview (= 8.0.3)
activejob (= 8.0.3)
activemodel (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
bundler (>= 1.15.0)
railties (= 8.0.2.1)
railties (= 8.0.3)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
@@ -693,13 +671,14 @@ GEM
rails-i18n (8.0.2)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
railties (8.0.3)
actionpack (= 8.0.3)
activesupport (= 8.0.3)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.0)
@@ -712,26 +691,27 @@ GEM
readline (~> 0.0)
rdf-normalize (0.7.0)
rdf (~> 3.3)
rdoc (6.14.2)
rdoc (6.15.1)
erb
psych (>= 4.0.0)
tsort
readline (0.0.4)
reline
redcarpet (3.6.1)
redis (4.8.1)
redis-client (0.25.3)
redis-client (0.26.1)
connection_pool
regexp_parser (2.11.2)
regexp_parser (2.11.3)
reline (0.6.2)
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
rexml (3.4.4)
rotp (6.3.0)
rouge (4.6.0)
rouge (4.6.1)
rpam2 (4.0.2)
rqrcode (3.1.0)
chunky_png (~> 1.0)
@@ -764,8 +744,8 @@ GEM
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
rspec-support (3.13.4)
rubocop (1.80.2)
rspec-support (3.13.6)
rubocop (1.81.6)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -773,10 +753,10 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.46.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.46.0)
rubocop-ast (1.47.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@@ -785,11 +765,11 @@ GEM
rubocop-i18n (3.2.3)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-performance (1.26.0)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rails (2.33.3)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.33.4)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@@ -811,11 +791,11 @@ GEM
ruby-vips (2.2.5)
ffi (~> 1.12)
logger
rubyzip (3.1.0)
rubyzip (3.2.2)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
safety_net_attestation (0.5.0)
jwt (>= 2.0, < 4.0)
sanitize (7.0.0)
crass (~> 1.0.2)
nokogiri (>= 1.16.8)
@@ -825,7 +805,7 @@ GEM
securerandom (0.4.1)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
sidekiq (8.0.7)
sidekiq (8.0.9)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
@@ -842,9 +822,9 @@ GEM
thor (>= 1.0, < 3.0)
simple-navigation (4.4.0)
activesupport (>= 2.3.2)
simple_form (5.3.1)
actionpack (>= 5.2)
activemodel (>= 5.2)
simple_form (5.4.0)
actionpack (>= 7.0)
activemodel (>= 7.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -855,10 +835,10 @@ GEM
stackprof (0.2.27)
starry (0.2.0)
base64
stoplight (5.3.8)
stoplight (5.4.0)
zeitwerk
stringio (3.1.7)
strong_migrations (2.5.0)
strong_migrations (2.5.1)
activerecord (>= 7.1)
swd (2.0.3)
activesupport (>= 3)
@@ -879,6 +859,7 @@ GEM
bindata (~> 2.4)
openssl (> 2.0)
openssl-signature_algorithm (~> 1.0)
tsort (0.2.0)
tty-color (0.6.0)
tty-cursor (0.7.1)
tty-prompt (0.23.1)
@@ -899,10 +880,10 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.9.1)
unicode-display_width (3.1.5)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uri (1.0.4)
useragent (0.16.11)
validate_url (1.0.15)
activemodel (>= 3.0.0)
@@ -918,19 +899,19 @@ GEM
zeitwerk (~> 2.2)
warden (1.2.9)
rack (>= 2.0.9)
webauthn (3.4.1)
webauthn (3.4.3)
android_key_attestation (~> 0.3.0)
bindata (~> 2.4)
cbor (~> 0.5.9)
cose (~> 1.1)
openssl (>= 2.2)
safety_net_attestation (~> 0.4.0)
safety_net_attestation (~> 0.5.0)
tpm-key_attestation (~> 0.14.0)
webfinger (2.1.3)
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.25.1)
webmock (3.26.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -988,7 +969,7 @@ DEPENDENCIES
flatware-rspec
fog-core (<= 2.6.0)
fog-openstack (~> 1.0)
haml-rails (~> 2.0)
haml-rails (~> 3.0)
haml_lint
hcaptcha (~> 7.1)
hiredis (~> 0.6)
@@ -1028,20 +1009,20 @@ DEPENDENCIES
omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.8.0)
opentelemetry-api (~> 1.7.0)
opentelemetry-exporter-otlp (~> 0.30.0)
opentelemetry-instrumentation-active_job (~> 0.8.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-instrumentation-excon (~> 0.24.0)
opentelemetry-instrumentation-faraday (~> 0.28.0)
opentelemetry-instrumentation-http (~> 0.25.0)
opentelemetry-instrumentation-http_client (~> 0.24.0)
opentelemetry-instrumentation-net_http (~> 0.24.0)
opentelemetry-instrumentation-pg (~> 0.30.0)
opentelemetry-instrumentation-rack (~> 0.27.0)
opentelemetry-instrumentation-rails (~> 0.37.0)
opentelemetry-instrumentation-redis (~> 0.26.0)
opentelemetry-instrumentation-sidekiq (~> 0.26.0)
opentelemetry-exporter-otlp (~> 0.31.0)
opentelemetry-instrumentation-active_job (~> 0.10.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.24.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.24.0)
opentelemetry-instrumentation-excon (~> 0.26.0)
opentelemetry-instrumentation-faraday (~> 0.30.0)
opentelemetry-instrumentation-http (~> 0.27.0)
opentelemetry-instrumentation-http_client (~> 0.26.0)
opentelemetry-instrumentation-net_http (~> 0.26.0)
opentelemetry-instrumentation-pg (~> 0.32.0)
opentelemetry-instrumentation-rack (~> 0.29.0)
opentelemetry-instrumentation-rails (~> 0.39.0)
opentelemetry-instrumentation-redis (~> 0.28.0)
opentelemetry-instrumentation-sidekiq (~> 0.28.0)
opentelemetry-sdk (~> 1.4)
ox (~> 2.14)
parslet
@@ -1052,7 +1033,7 @@ DEPENDENCIES
prometheus_exporter (~> 2.2)
propshaft
public_suffix (~> 6.0)
puma (~> 6.3)
puma (~> 7.0)
pundit (~> 2.3)
rack-attack (~> 6.6)
rack-cors
@@ -1100,10 +1081,11 @@ DEPENDENCIES
webauthn (~> 3.0)
webmock (~> 3.18)
webpush!
websocket-driver (~> 0.8)
xorcist (~> 1.1)
RUBY VERSION
ruby 3.4.1p0
BUNDLED WITH
2.7.1
2.7.2

View File

@@ -73,7 +73,7 @@ Mastodon is a **free, open-source social network server** based on [ActivityPub]
### Requirements
- **Ruby** 3.2+
- **PostgreSQL** 13+
- **PostgreSQL** 14+
- **Redis** 7.0+
- **Node.js** 20+

View File

@@ -15,7 +15,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| Version | Supported |
| ------- | ---------------- |
| 4.5.x | Yes |
| 4.4.x | Yes |
| 4.3.x | Yes |
| 4.3.x | Until 2026-05-06 |
| 4.2.x | Until 2026-01-08 |
| < 4.2 | No |

View File

@@ -71,6 +71,10 @@ class AccountsController < ApplicationController
params[:username]
end
def account_id_param
params[:id]
end
def skip_temporary_suspension_response?
request.format == :json
end

View File

@@ -36,9 +36,8 @@ class ActivityPub::ContextsController < ActivityPub::BaseController
def context_presenter
first_page = ActivityPub::CollectionPresenter.new(
id: items_context_url(@conversation, page_params),
type: :unordered,
part_of: items_context_url(@conversation),
part_of: context_url(@conversation),
next: next_page,
items: @items.map { |status| status.local? ? ActivityPub::TagManager.instance.uri_for(status) : status.uri }
)
@@ -52,7 +51,7 @@ class ActivityPub::ContextsController < ActivityPub::BaseController
page = ActivityPub::CollectionPresenter.new(
id: items_context_url(@conversation, page_params),
type: :unordered,
part_of: items_context_url(@conversation),
part_of: context_url(@conversation),
next: next_page,
items: @items.map { |status| status.local? ? ActivityPub::TagManager.instance.uri_for(status) : status.uri }
)

View File

@@ -22,13 +22,13 @@ class ActivityPub::LikesController < ActivityPub::BaseController
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
def likes_collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_status_likes_url(@account, @status),
id: ActivityPub::TagManager.instance.likes_uri_for(@status),
type: :unordered,
size: @status.favourites_count
)

View File

@@ -73,6 +73,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
end
def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
return super if params[:account_username].present? || params[:account_id].present?
@account = Account.representative
end
end

View File

@@ -9,7 +9,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
before_action :set_quote_authorization
def show
expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode?
expires_in 30.seconds, public: true if @quote.quoted_status.distributable? && public_fetch_mode?
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
@@ -23,8 +23,8 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
return not_found unless @quote.status.present? && @quote.quoted_status.present?
authorize @quote.status, :show?
rescue Mastodon::NotPermittedError
authorize @quote.quoted_status, :show?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -25,7 +25,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
@@ -37,7 +37,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status, page_params),
id: ActivityPub::TagManager.instance.replies_uri_for(@status, page_params),
type: :unordered,
part_of: account_status_replies_url(@account, @status),
next: next_page,
@@ -47,7 +47,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
return page if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status),
id: ActivityPub::TagManager.instance.replies_uri_for(@status),
type: :unordered,
first: page
)
@@ -66,8 +66,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
# Only consider remote accounts
return nil if @replies.size < DESCENDANTS_LIMIT
account_status_replies_url(
@account,
ActivityPub::TagManager.instance.replies_uri_for(
@status,
page: true,
min_id: @replies&.last&.id,
@@ -77,8 +76,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
# For now, we're serving only self-replies, but next page might be other accounts
next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT
account_status_replies_url(
@account,
ActivityPub::TagManager.instance.replies_uri_for(
@status,
page: true,
min_id: next_only_other_accounts ? nil : @replies&.last&.id,

View File

@@ -22,13 +22,13 @@ class ActivityPub::SharesController < ActivityPub::BaseController
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
def shares_collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_status_shares_url(@account, @status),
id: ActivityPub::TagManager.instance.shares_uri_for(@status),
type: :unordered,
size: @status.reblogs_count
)

View File

@@ -9,10 +9,16 @@ module Admin
@pending_appeals_count = Appeal.pending.async_count
@pending_reports_count = Report.unresolved.async_count
@pending_tags_count = Tag.pending_review.async_count
@pending_tags_count = pending_tags.async_count
@pending_users_count = User.pending.async_count
@system_checks = Admin::SystemCheck.perform(current_user)
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
end
private
def pending_tags
::Trends::TagFilter.new(status: :pending_review).results
end
end
end

View File

@@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
def set_poll
@poll = Poll.find(params[:poll_id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
def set_poll
@poll = Poll.find(params[:id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < Api::BaseController
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
bookmark&.destroy!
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -25,7 +25,7 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } })
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -4,7 +4,6 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base
include Api::InteractionPoliciesConcern
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action -> { check_feature_enabled }
def update
authorize @status, :update?
@@ -22,12 +21,8 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base
params.permit(:quote_approval_policy)
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled?
end
def broadcast_updates!
DistributionWorker.perform_async(@status.id, { 'update' => true })
DistributionWorker.perform_async(@status.id, { 'update' => true, 'skip_notifications' => true })
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 })
end
end

View File

@@ -4,13 +4,13 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: :revoke
before_action :check_owner!
before_action :set_statuses, only: :index
before_action :set_quote, only: :revoke
after_action :insert_pagination_headers, only: :index
def index
cache_if_unauthenticated!
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer
end
@@ -24,18 +24,26 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
private
def check_owner!
authorize @status, :list_quotes?
end
def set_quote
@quote = @status.quotes.find_by!(status_id: params[:id])
end
def load_statuses
def set_statuses
scope = default_statuses
scope = scope.not_excluded_by_account(current_account) unless current_account.nil?
scope.merge(paginated_quotes).to_a
@statuses = scope.merge(paginated_quotes).to_a
# Store next page info before filtering
@records_continue = @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
@pagination_since_id = @statuses.first.quote.id unless @statuses.empty?
@pagination_max_id = @statuses.last.quote.id if @records_continue
if current_account&.id != @status.account_id
domains = @statuses.filter_map(&:account_domain).uniq
account_ids = @statuses.map(&:account_id).uniq
relations = current_account&.relations_map(account_ids, domains) || {}
@statuses.reject! { |status| StatusFilter.new(status, current_account, relations).filtered? }
end
end
def default_statuses
@@ -58,15 +66,9 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
api_v1_status_quotes_url pagination_params(since_id: pagination_since_id) unless @statuses.empty?
end
def pagination_max_id
@statuses.last.quote.id
end
def pagination_since_id
@statuses.first.quote.id
end
attr_reader :pagination_max_id, :pagination_since_id
def records_continue?
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
@records_continue
end
end

View File

@@ -36,7 +36,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } })
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
@@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
def set_reblog
@reblog = Status.find(params[:status_id])
authorize @reblog, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -66,7 +66,7 @@ class Api::V1::StatusesController < Api::BaseController
if async_refresh.running?
add_async_refresh_header(async_refresh)
elsif !current_account.nil? && @status.should_fetch_replies?
add_async_refresh_header(AsyncRefresh.create(refresh_key))
add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
WorkerBatch.new.within do |batch|
batch.connect(refresh_key, threshold: 1.0)
@@ -147,7 +147,7 @@ class Api::V1::StatusesController < Api::BaseController
def set_status
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
@@ -159,9 +159,7 @@ class Api::V1::StatusesController < Api::BaseController
end
def set_quoted_status
return unless Mastodon::Feature.outgoing_quotes_enabled?
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
@quoted_status = Status.find(status_params[:quoted_status_id])&.proper if status_params[:quoted_status_id].present?
authorize(@quoted_status, :quote?) if @quoted_status.present?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
# TODO: distinguish between non-existing and non-quotable posts

View File

@@ -3,14 +3,8 @@
class Api::V1::Timelines::BaseController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
before_action :require_user!, if: :require_auth?
private
def require_auth?
!Setting.timeline_preview
end
def pagination_collection
@statuses
end

View File

@@ -3,8 +3,8 @@
class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
include AsyncRefreshesConcern
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show]
before_action :require_user!, only: [:show]
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :require_user!
PERMITTED_PARAMS = %i(local limit).freeze

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
class Api::V1::Timelines::LinkController < Api::V1::Timelines::TopicController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_preview_card
before_action :set_statuses

View File

@@ -2,6 +2,7 @@
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :require_user!, if: :require_auth?
PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze
@@ -13,6 +14,16 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
private
def require_auth?
if truthy_param?(:local)
Setting.local_live_feed_access != 'public'
elsif truthy_param?(:remote)
Setting.remote_live_feed_access != 'public'
else
Setting.local_live_feed_access != 'public' || Setting.remote_live_feed_access != 'public'
end
end
def load_statuses
preloaded_public_statuses_page
end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
class Api::V1::Timelines::TagController < Api::V1::Timelines::TopicController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :load_tag
@@ -14,10 +14,6 @@ class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
private
def require_auth?
!Setting.timeline_preview
end
def load_tag
@tag = Tag.find_normalized(params[:id])
end

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
class Api::V1::Timelines::TopicController < Api::V1::Timelines::BaseController
before_action :require_user!, if: :require_auth?
private
def require_auth?
if truthy_param?(:local)
Setting.local_topic_feed_access != 'public'
elsif truthy_param?(:remote)
Setting.remote_topic_feed_access != 'public'
else
Setting.local_topic_feed_access != 'public' || Setting.remote_topic_feed_access != 'public'
end
end
end

View File

@@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
def set_status
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -89,7 +89,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def check_enabled_registrations
redirect_to root_path unless allowed_registration?(request.remote_ip, @invite)
redirect_to new_user_session_path, alert: I18n.t('devise.failure.closed_registrations', email: Setting.site_contact_email) unless allowed_registration?(request.remote_ip, @invite)
end
def invite_code

View File

@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
def set_resource
@resource = located_resource
authorize(@resource, :show?) if @resource.is_a?(Status)
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -18,7 +18,11 @@ module AccountOwnedConcern
end
def set_account
@account = Account.find_local!(username_param)
@account = username_param.present? ? Account.find_local!(username_param) : Account.local.find(account_id_param)
end
def account_id_param
params[:account_id]
end
def username_param

View File

@@ -4,8 +4,6 @@ module Api::InteractionPoliciesConcern
extend ActiveSupport::Concern
def quote_approval_policy
return nil unless Mastodon::Feature.outgoing_quotes_enabled?
case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16

View File

@@ -6,6 +6,9 @@ module AsyncRefreshesConcern
def add_async_refresh_header(async_refresh, retry_seconds: 3)
return unless async_refresh.running?
response.headers['Mastodon-Async-Refresh'] = "id=\"#{async_refresh.id}\", retry=#{retry_seconds}"
value = "id=\"#{async_refresh.id}\", retry=#{retry_seconds}"
value += ", result_count=#{async_refresh.result_count}" unless async_refresh.result_count.nil?
response.headers['Mastodon-Async-Refresh'] = value
end
end

View File

@@ -72,10 +72,13 @@ module SignatureVerification
rescue Mastodon::SignatureVerificationError => e
fail_with! e.message
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
@signature_verification_failure_code ||= 503
fail_with! "Failed to fetch remote data: #{e.message}"
rescue Mastodon::UnexpectedResponseError
@signature_verification_failure_code ||= 503
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
rescue Stoplight::Error::RedLight
@signature_verification_failure_code ||= 503
fail_with! 'Fetching attempt skipped because of recent connection failure'
end

View File

@@ -58,20 +58,22 @@ class FollowerAccountsController < ApplicationController
end
def collection_presenter
options = { type: :ordered }
options = {}
options[:size] = @account.followers_count unless Setting.hide_followers_count || @account.user&.setting_hide_followers_count
if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account, page: params.fetch(:page, 1)),
id: page_url(params.fetch(:page, 1)),
type: :ordered,
items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.account) },
part_of: account_followers_url(@account),
part_of: ActivityPub::TagManager.instance.followers_uri_for(@account),
next: next_page_url,
prev: prev_page_url,
**options
)
else
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account),
id: ActivityPub::TagManager.instance.followers_uri_for(@account),
type: :ordered,
first: page_url(1),
**options
)

View File

@@ -49,7 +49,7 @@ class FollowingAccountsController < ApplicationController
end
def page_url(page)
account_following_index_url(@account, page: page) unless page.nil?
ActivityPub::TagManager.instance.following_uri_for(@account, page: page) unless page.nil?
end
def next_page_url
@@ -63,17 +63,17 @@ class FollowingAccountsController < ApplicationController
def collection_presenter
if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account, page: params.fetch(:page, 1)),
id: page_url(params.fetch(:page, 1)),
type: :ordered,
size: @account.following_count,
items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.target_account) },
part_of: account_following_index_url(@account),
part_of: ActivityPub::TagManager.instance.following_uri_for(@account),
next: next_page_url,
prev: prev_page_url
)
else
ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account),
id: ActivityPub::TagManager.instance.following_uri_for(@account),
type: :ordered,
size: @account.following_count,
first: page_url(1)

View File

@@ -34,7 +34,7 @@ class MediaController < ApplicationController
def verify_permitted_status!
authorize @media_attachment.status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -26,7 +26,7 @@ class SeveredRelationshipsController < ApplicationController
private
def set_event
@event = AccountRelationshipSeveranceEvent.find(params[:id])
@event = AccountRelationshipSeveranceEvent.where(account: current_account).find(params[:id])
end
def following_data

View File

@@ -62,7 +62,7 @@ class StatusesController < ApplicationController
def set_status
@status = @account.statuses.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -113,6 +113,7 @@ module ApplicationHelper
end
def material_symbol(icon, attributes = {})
whitespace = attributes.delete(:whitespace) { true }
safe_join(
[
inline_svg_tag(
@@ -121,7 +122,7 @@ module ApplicationHelper
role: :img,
data: attributes[:data]
),
' ',
whitespace ? ' ' : '',
]
)
end

View File

@@ -21,7 +21,13 @@ module HomeHelper
end
end
else
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
account_url = if account.suspended?
ActivityPub::TagManager.instance.url_for(account)
else
web_url("@#{account.pretty_acct}")
end
link_to(path || account_url, class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do
image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar', width: 46, height: 46)
end +

View File

@@ -46,6 +46,14 @@ module StatusesHelper
status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n")
end
def status_classnames(status, is_quote)
if is_quote
'status--is-quote'
elsif status.quote.present?
'status--has-quote'
end
end
def status_description(status)
components = [[media_summary(status), status_text_summary(status)].compact_blank.join(' · ')]
@@ -57,6 +65,20 @@ module StatusesHelper
components.compact_blank.join("\n\n")
end
# This logic should be kept in sync with https://github.com/mastodon/mastodon/blob/425311e1d95c8a64ddac6c724fca247b8b893a82/app/javascript/mastodon/features/status/components/card.jsx#L160
def preview_card_aspect_ratio_classname(preview_card)
interactive = preview_card.type == 'video'
large_image = (preview_card.image.present? && preview_card.width > preview_card.height) || interactive
if large_image && interactive
'status-card__image--video'
elsif large_image
'status-card__image--large'
else
'status-card__image--normal'
end
end
def visibility_icon(status)
VISIBLITY_ICONS[status.visibility.to_sym]
end

View File

@@ -0,0 +1,78 @@
{
"global": {
"class": "className",
"id": true,
"title": true,
"dir": true,
"lang": true
},
"tags": {
"p": {},
"br": {
"children": false
},
"span": {
"attributes": {
"translate": true
}
},
"a": {
"attributes": {
"href": true,
"rel": true,
"translate": true,
"target": true,
"title": true
}
},
"abbr": {
"attributes": {
"title": true
}
},
"del": {},
"s": {},
"pre": {},
"blockquote": {
"attributes": {
"cite": true
}
},
"code": {},
"b": {},
"strong": {},
"u": {},
"sub": {},
"sup": {},
"i": {},
"img": {
"children": false,
"attributes": {
"src": true,
"alt": true,
"title": true
}
},
"em": {},
"h1": {},
"h2": {},
"h3": {},
"h4": {},
"h5": {},
"ul": {},
"ol": {
"attributes": {
"start": true,
"reversed": true
}
},
"li": {
"attributes": {
"value": true
}
},
"ruby": {},
"rt": {},
"rp": {}
}
}

View File

@@ -1,6 +1,7 @@
import { createRoot } from 'react-dom/client';
import Rails from '@rails/ujs';
import { decode, ValidationError } from 'blurhash';
import ready from '../mastodon/ready';
@@ -362,6 +363,46 @@ ready(() => {
document.querySelectorAll('[data-admin-component]').forEach((element) => {
void mountReactComponent(element);
});
document
.querySelectorAll<HTMLCanvasElement>('canvas[data-blurhash]')
.forEach((canvas) => {
const blurhash = canvas.dataset.blurhash;
if (blurhash) {
try {
// decode returns a Uint8ClampedArray<ArrayBufferLike> not Uint8ClampedArray<ArrayBuffer>
const pixels = decode(
blurhash,
32,
32,
) as Uint8ClampedArray<ArrayBuffer>;
const ctx = canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32);
ctx?.putImageData(imageData, 0, 0);
} catch (err) {
if (err instanceof ValidationError) {
// ignore blurhash validation errors
return;
}
throw err;
}
}
});
document
.querySelectorAll<HTMLDivElement>('.preview-card')
.forEach((previewCard) => {
const spoilerButton = previewCard.querySelector('.spoiler-button');
if (!spoilerButton) {
return;
}
spoilerButton.addEventListener('click', () => {
previewCard.classList.toggle('preview-card--image-visible');
});
});
}).catch((reason: unknown) => {
throw reason;
});

View File

@@ -5,6 +5,7 @@ import { throttle } from 'lodash';
import api from 'flavours/glitch/api';
import { browserHistory } from 'flavours/glitch/components/router';
import { countableText } from 'flavours/glitch/features/compose/util/counter';
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'flavours/glitch/settings';
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
@@ -57,7 +58,6 @@ export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
@@ -93,6 +93,7 @@ const messages = defineMessages({
open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
blankPostError: { id: 'compose.error.blank_post', defaultMessage: 'Post can\'t be blank.' },
});
export const ensureComposeIsVisible = (getState) => {
@@ -197,19 +198,33 @@ export function directCompose(account) {
};
}
/**
* @callback ComposeSuccessCallback
* @param {Object} status
*/
/**
* @param {null | string} overridePrivacy
* @param {undefined | Function} successCallback
* @param {undefined | ComposeSuccessCallback} successCallback
*/
export function submitCompose(overridePrivacy = null, successCallback = undefined) {
return function (dispatch, getState) {
let status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
const statusId = getState().getIn(['compose', 'id'], null);
const hasQuote = !!getState().getIn(['compose', 'quoted_status_id']);
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
const spoiler_text = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
const fulltext = `${spoiler_text ?? ''}${countableText(status ?? '')}`;
const hasText = fulltext.trim().length > 0;
if (!(hasText || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
dispatch(showAlert({
message: messages.blankPostError,
}));
dispatch(focusCompose());
if ((!status || !status.length) && media.size === 0) {
return;
}
@@ -245,12 +260,12 @@ export function submitCompose(overridePrivacy = null, successCallback = undefine
method: statusId === null ? 'post' : 'put',
data: {
status,
spoiler_text,
content_type: getState().getIn(['compose', 'content_type']),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')),
media_attributes,
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
spoiler_text: spoilerText,
sensitive: getState().getIn(['compose', 'sensitive']) || (spoiler_text.length > 0 && media.size !== 0),
visibility: visibility,
poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']),
@@ -652,6 +667,7 @@ export function fetchComposeSuggestions(token) {
fetchComposeSuggestionsEmojis(dispatch, getState, token);
break;
case '#':
case '':
fetchComposeSuggestionsTags(dispatch, getState, token);
break;
default:
@@ -693,11 +709,20 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
dispatch(useEmoji(suggestion));
} else if (suggestion.type === 'hashtag') {
completion = `#${suggestion.name}`;
// TODO: it could make sense to keep the “most capitalized” of the two
const tokenName = token.slice(1); // strip leading '#'
const suggestionPrefix = suggestion.name.slice(0, tokenName.length);
const prefixMatchesSuggestion = suggestionPrefix.localeCompare(tokenName, undefined, { sensitivity: 'accent' }) === 0;
if (prefixMatchesSuggestion) {
completion = token + suggestion.name.slice(tokenName.length);
} else {
completion = `${token.slice(0, 1)}${suggestion.name}`;
}
startPosition = position - 1;
} else if (suggestion.type === 'account') {
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
startPosition = position;
completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`;
startPosition = position - 1;
}
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
@@ -808,13 +833,6 @@ export function changeComposeSpoilerText(text) {
};
}
export function changeComposeVisibility(value) {
return {
type: COMPOSE_VISIBILITY_CHANGE,
value,
};
}
export function insertEmojiCompose(position, emoji, needsSpace) {
return {
type: COMPOSE_EMOJI_INSERT,

View File

@@ -4,6 +4,7 @@ import { createAction } from '@reduxjs/toolkit';
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { apiUpdateMedia } from 'flavours/glitch/api/compose';
import { apiGetSearch } from 'flavours/glitch/api/search';
import type { ApiMediaAttachmentJSON } from 'flavours/glitch/api_types/media_attachments';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import {
@@ -12,13 +13,19 @@ import {
} from 'flavours/glitch/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
import type { Status } from '../models/status';
import type { Status, StatusVisibility } from '../models/status';
import type { RootState } from '../store';
import { showAlert } from './alerts';
import { focusCompose } from './compose';
import { changeCompose, focusCompose } from './compose';
import { importFetchedStatuses } from './importer';
import { openModal } from './modal';
const messages = defineMessages({
quoteErrorEdit: {
id: 'quote_error.edit',
defaultMessage: 'Quotes cannot be added when editing a post.',
},
quoteErrorUpload: {
id: 'quote_error.upload',
defaultMessage: 'Quoting is not allowed with media attachments.',
@@ -35,6 +42,10 @@ const messages = defineMessages({
id: 'quote_error.unauthorized',
defaultMessage: 'You are not authorized to quote this post.',
},
quoteErrorPrivateMention: {
id: 'quote_error.private_mentions',
defaultMessage: 'Quoting is not allowed with direct mentions.',
},
});
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
@@ -61,6 +72,39 @@ const simulateModifiedApiResponse = (
return data;
};
export const changeComposeVisibility = createAppThunk(
'compose/visibility_change',
(visibility: StatusVisibility, { dispatch, getState }) => {
if (visibility !== 'direct') {
return visibility;
}
const state = getState();
const quotedStatusId = state.compose.get('quoted_status_id') as
| string
| null;
if (!quotedStatusId) {
return visibility;
}
// Remove the quoted status
dispatch(quoteComposeCancel());
const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
if (!quotedStatus) {
return visibility;
}
// Append the quoted status URL to the compose text
const url = quotedStatus.get('url') as string;
const text = state.compose.get('text') as string;
if (!text.includes(url)) {
const newText = text.trim() ? `${text}\n\n${url}` : url;
dispatch(changeCompose(newText));
}
return visibility;
},
);
export const changeUploadCompose = createDataLoadingThunk(
'compose/changeUpload',
async (
@@ -122,7 +166,11 @@ export const quoteComposeByStatus = createAppThunk(
false,
);
if (composeState.get('poll')) {
if (composeState.get('id')) {
dispatch(showAlert({ message: messages.quoteErrorEdit }));
} else if (composeState.get('privacy') === 'direct') {
dispatch(showAlert({ message: messages.quoteErrorPrivateMention }));
} else if (composeState.get('poll')) {
dispatch(showAlert({ message: messages.quoteErrorPoll }));
} else if (
composeState.get('is_uploading') ||
@@ -165,6 +213,61 @@ export const quoteComposeById = createAppThunk(
},
);
const composeStateForbidsLink = (composeState: RootState['compose']) => {
return (
composeState.get('quoted_status_id') ||
composeState.get('is_submitting') ||
composeState.get('poll') ||
composeState.get('is_uploading') ||
composeState.get('id') ||
composeState.get('privacy') === 'direct'
);
};
export const pasteLinkCompose = createDataLoadingThunk(
'compose/pasteLink',
async ({ url }: { url: string }) => {
return await apiGetSearch({
q: url,
type: 'statuses',
resolve: true,
limit: 2,
});
},
(data, { dispatch, getState, requestId }) => {
const composeState = getState().compose;
if (
composeStateForbidsLink(composeState) ||
composeState.get('fetching_link') !== requestId // Request has been cancelled
)
return;
dispatch(importFetchedStatuses(data.statuses));
if (
data.statuses.length === 1 &&
data.statuses[0] &&
['automatic', 'manual'].includes(
data.statuses[0].quote_approval?.current_user ?? 'denied',
)
) {
dispatch(quoteComposeById(data.statuses[0].id));
}
},
{
useLoadingBar: false,
condition: (_, { getState }) =>
!getState().compose.get('fetching_link') &&
!composeStateForbidsLink(getState().compose),
},
);
// Ideally this would cancel the action and the HTTP request, but this is good enough
export const cancelPasteLinkCompose = createAction(
'compose/cancelPasteLinkCompose',
);
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
export const setComposeQuotePolicy = createAction<ApiQuotePolicy>(

View File

@@ -46,11 +46,11 @@ export function importFetchedAccounts(accounts) {
return importAccounts({ accounts: normalAccounts });
}
export function importFetchedStatus(status) {
return importFetchedStatuses([status]);
export function importFetchedStatus(status, options = {}) {
return importFetchedStatuses([status], options);
}
export function importFetchedStatuses(statuses) {
export function importFetchedStatuses(statuses, options = {}) {
return (dispatch, getState) => {
const accounts = [];
const normalStatuses = [];
@@ -58,7 +58,7 @@ export function importFetchedStatuses(statuses) {
const filters = [];
function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), { ...options, settings: getState().get('local_settings') }));
pushUnique(accounts, status.account);
if (status.filtered) {

View File

@@ -1,8 +1,5 @@
import escapeTextContentForBrowser from 'escape-html';
import { makeEmojiMap } from 'flavours/glitch/models/custom_emoji';
import emojify from '../../features/emoji/emoji';
import { autoHideCW } from '../../utils/content_warning';
const domParser = new DOMParser();
@@ -30,9 +27,12 @@ function stripQuoteFallback(text) {
return wrapper.innerHTML;
}
export function normalizeStatus(status, normalOldStatus, settings) {
export function normalizeStatus(status, normalOldStatus, { settings, bogusQuotePolicy = false }) {
const normalStatus = { ...status };
if (bogusQuotePolicy)
normalStatus.quote_approval = null;
normalStatus.account = status.account.id;
if (status.reblog && status.reblog.id) {
@@ -80,11 +80,10 @@ export function normalizeStatus(status, normalOldStatus, settings) {
} else {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus.emojis);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.contentHtml = normalStatus.content;
normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText);
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
@@ -105,6 +104,8 @@ export function normalizeStatus(status, normalOldStatus, settings) {
}
if (normalOldStatus) {
normalStatus.quote_approval ||= normalOldStatus.get('quote_approval');
const list = normalOldStatus.get('media_attachments');
if (normalStatus.media_attachments && list) {
normalStatus.media_attachments.forEach(item => {
@@ -120,14 +121,12 @@ export function normalizeStatus(status, normalOldStatus, settings) {
}
export function normalizeStatusTranslation(translation, status) {
const emojiMap = makeEmojiMap(status.get('emojis').toJS());
const normalTranslation = {
detected_source_language: translation.detected_source_language,
language: translation.language,
provider: translation.provider,
contentHtml: emojify(translation.content, emojiMap),
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
contentHtml: translation.content,
spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text),
spoiler_text: translation.spoiler_text,
};
@@ -141,9 +140,8 @@ export function normalizeStatusTranslation(translation, status) {
export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
normalAnnouncement.contentHtml = normalAnnouncement.content;
return normalAnnouncement;
}

View File

@@ -85,6 +85,8 @@ export function fetchStatus(id, {
dispatch(fetchStatusSuccess(skipLoading));
}).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
if (error.status === 404)
dispatch(deleteFromTimelines(id));
});
};
}
@@ -204,8 +206,8 @@ export function deleteStatusFail(id, error) {
};
}
export const updateStatus = status => dispatch =>
dispatch(importFetchedStatus(status));
export const updateStatus = (status, { bogusQuotePolicy }) => dispatch =>
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
export function muteStatus(id) {
return (dispatch) => {

View File

@@ -9,8 +9,9 @@ import { importFetchedStatuses } from './importer';
export const fetchContext = createDataLoadingThunk(
'status/context',
({ statusId }: { statusId: string }) => apiGetContext(statusId),
({ context, refresh }, { dispatch }) => {
({ statusId }: { statusId: string; prefetchOnly?: boolean }) =>
apiGetContext(statusId),
({ context, refresh }, { dispatch, actionArg: { prefetchOnly = false } }) => {
const statuses = context.ancestors.concat(context.descendants);
dispatch(importFetchedStatuses(statuses));
@@ -18,6 +19,7 @@ export const fetchContext = createDataLoadingThunk(
return {
context,
refresh,
prefetchOnly,
};
},
);
@@ -26,6 +28,14 @@ export const completeContextRefresh = createAction<{ statusId: string }>(
'status/context/complete',
);
export const showPendingReplies = createAction<{ statusId: string }>(
'status/context/showPendingReplies',
);
export const clearPendingReplies = createAction<{ statusId: string }>(
'status/context/clearPendingReplies',
);
export const setStatusQuotePolicy = createDataLoadingThunk(
'status/setQuotePolicy',
({ statusId, policy }: { statusId: string; policy: ApiQuotePolicy }) => {

View File

@@ -32,27 +32,38 @@ import {
const randomUpTo = max =>
Math.floor(Math.random() * Math.floor(max));
/**
* @typedef {import('flavours/glitch/store').AppDispatch} Dispatch
* @typedef {import('flavours/glitch/store').GetState} GetState
* @typedef {import('redux').UnknownAction} UnknownAction
* @typedef {function(Dispatch, GetState): Promise<void>} FallbackFunction
*/
/**
* @param {string} timelineId
* @param {string} channelName
* @param {Object.<string, string>} params
* @param {Object} options
* @param {function(Function, Function): Promise<void>} [options.fallback]
* @param {function(): void} [options.fillGaps]
* @param {FallbackFunction} [options.fallback]
* @param {function(): UnknownAction} [options.fillGaps]
* @param {function(object): boolean} [options.accept]
* @returns {function(): void}
*/
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
const { messages } = getLocale();
// Public streams are currently not returning personalized quote policies
const bogusQuotePolicy = channelName.startsWith('public') || channelName.startsWith('hashtag');
return connectStream(channelName, params, (dispatch, getState) => {
// @ts-ignore
const locale = getState().getIn(['meta', 'locale']);
// @ts-expect-error
let pollingId;
/**
* @param {function(Function, Function): Promise<void>} fallback
* @param {FallbackFunction} fallback
*/
const useFallback = async fallback => {
@@ -89,11 +100,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
switch (data.event) {
case 'update':
// @ts-expect-error
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), { accept: options.accept, bogusQuotePolicy }));
break;
case 'status.update':
// @ts-expect-error
dispatch(updateStatus(JSON.parse(data.payload)));
dispatch(updateStatus(JSON.parse(data.payload), { bogusQuotePolicy }));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
@@ -132,7 +143,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
};
/**
* @param {Function} dispatch
* @param {Dispatch} dispatch
*/
async function refreshHomeTimelineAndNotification(dispatch) {
await dispatch(expandHomeTimeline({ maxId: undefined }));
@@ -151,7 +162,11 @@ async function refreshHomeTimelineAndNotification(dispatch) {
* @returns {function(): void}
*/
export const connectUserStream = () =>
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
connectTimelineStream('home', 'user', {}, {
fallback: refreshHomeTimelineAndNotification,
// @ts-expect-error
fillGaps: fillHomeTimelineGaps
});
/**
* @param {Object} options
@@ -159,7 +174,10 @@ export const connectUserStream = () =>
* @returns {function(): void}
*/
export const connectCommunityStream = ({ onlyMedia } = {}) =>
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, {
// @ts-expect-error
fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia }))
});
/**
* @param {Object} options
@@ -169,7 +187,10 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
* @returns {function(): void}
*/
export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) });
connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, {
// @ts-expect-error
fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly })
});
/**
* @param {string} columnId
@@ -192,4 +213,7 @@ export const connectDirectStream = () =>
* @returns {function(): void}
*/
export const connectListStream = listId =>
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, {
// @ts-expect-error
fillGaps: () => fillListTimelineGaps(listId)
});

View File

@@ -33,7 +33,7 @@ export const loadPending = timeline => ({
timeline,
});
export function updateTimeline(timeline, status, accept) {
export function updateTimeline(timeline, status, { accept = undefined, bogusQuotePolicy = false } = {}) {
return (dispatch, getState) => {
if (typeof accept === 'function' && !accept(status)) {
return;
@@ -55,7 +55,7 @@ export function updateTimeline(timeline, status, accept) {
filtered = filters.length > 0;
}
dispatch(importFetchedStatus(status));
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
dispatch({
type: TIMELINE_UPDATE,

View File

@@ -0,0 +1,28 @@
// See app/serializers/rest/announcement_serializer.rb
import type { ApiCustomEmojiJSON } from './custom_emoji';
import type { ApiMentionJSON, ApiStatusJSON, ApiTagJSON } from './statuses';
export interface ApiAnnouncementJSON {
id: string;
content: string;
starts_at: null | string;
ends_at: null | string;
all_day: boolean;
published_at: string;
updated_at: null | string;
read: boolean;
mentions: ApiMentionJSON[];
statuses: ApiStatusJSON[];
tags: ApiTagJSON[];
emojis: ApiCustomEmojiJSON[];
reactions: ApiAnnouncementReactionJSON[];
}
export interface ApiAnnouncementReactionJSON {
name: string;
count: number;
me: boolean;
url?: string;
static_url?: string;
}

View File

@@ -4,6 +4,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import {
blockAccount,
@@ -33,7 +34,7 @@ import { me } from 'flavours/glitch/initial_state';
import type { MenuItem } from 'flavours/glitch/models/dropdown_menu';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { Permalink } from './permalink';
import { Permalink } from '../permalink';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -333,9 +334,10 @@ export const Account: React.FC<AccountProps> = ({
{account &&
withBio &&
(account.note.length > 0 ? (
<div
<EmojiHTML
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
htmlString={account.note_emojified}
extraEmojis={account.emojis}
/>
) : (
<div className='account__note account__note--missing'>

View File

@@ -1,10 +1,9 @@
import { useCallback } from 'react';
import classNames from 'classnames';
import { useLinks } from 'flavours/glitch/hooks/useLinks';
import { EmojiHTML } from '../features/emoji/emoji_html';
import { useAppSelector } from '../store';
import { isModernEmojiEnabled } from '../utils/environment';
import { EmojiHTML } from './emoji/html';
import { useElementHandledLink } from './status/handled_link';
interface AccountBioProps {
className: string;
@@ -17,22 +16,16 @@ export const AccountBio: React.FC<AccountBioProps> = ({
accountId,
showDropdown = false,
}) => {
const handleClick = useLinks(showDropdown);
const handleNodeChange = useCallback(
(node: HTMLDivElement | null) => {
if (!showDropdown || !node || node.childNodes.length === 0) {
return;
}
addDropdownToHashtags(node, accountId);
},
[showDropdown, accountId],
);
const htmlHandlers = useElementHandledLink({
hashtagAccountId: showDropdown ? accountId : undefined,
});
const note = useAppSelector((state) => {
const account = state.accounts.get(accountId);
if (!account) {
return '';
}
return isModernEmojiEnabled() ? account.note : account.note_emojified;
return account.note_emojified;
});
const extraEmojis = useAppSelector((state) => {
const account = state.accounts.get(accountId);
@@ -44,33 +37,11 @@ export const AccountBio: React.FC<AccountBioProps> = ({
}
return (
<div
className={`${className} translate`}
onClickCapture={handleClick}
ref={handleNodeChange}
>
<EmojiHTML htmlString={note} extraEmojis={extraEmojis} />
</div>
<EmojiHTML
htmlString={note}
extraEmojis={extraEmojis}
className={classNames(className, 'translate')}
{...htmlHandlers}
/>
);
};
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
if (!node) {
return;
}
for (const childNode of node.childNodes) {
if (!(childNode instanceof HTMLElement)) {
continue;
}
if (
childNode instanceof HTMLAnchorElement &&
(childNode.classList.contains('hashtag') ||
childNode.innerText.startsWith('#')) &&
!childNode.dataset.menuHashtag
) {
childNode.dataset.menuHashtag = accountId;
} else if (childNode.childNodes.length > 0) {
addDropdownToHashtags(childNode, accountId);
}
}
}

View File

@@ -1,42 +1,71 @@
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { useLinks } from 'flavours/glitch/hooks/useLinks';
import type { Account } from 'flavours/glitch/models/account';
export const AccountFields: React.FC<{
fields: Account['fields'];
limit: number;
}> = ({ fields, limit = -1 }) => {
const handleClick = useLinks();
import { EmojiHTML } from './emoji/html';
import { useElementHandledLink } from './status/handled_link';
export const AccountFields: React.FC<Pick<Account, 'fields' | 'emojis'>> = ({
fields,
emojis,
}) => {
const intl = useIntl();
const htmlHandlers = useElementHandledLink();
if (fields.size === 0) {
return null;
}
return (
<div className='account-fields' onClickCapture={handleClick}>
{fields.take(limit).map((pair, i) => (
<dl
key={i}
className={classNames({ verified: pair.get('verified_at') })}
>
<dt
dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }}
<>
{fields.map((pair, i) => (
<dl key={i} className={classNames({ verified: pair.verified_at })}>
<EmojiHTML
as='dt'
htmlString={pair.name_emojified}
extraEmojis={emojis}
className='translate'
{...htmlHandlers}
/>
<dd className='translate' title={pair.get('value_plain') ?? ''}>
{pair.get('verified_at') && (
<Icon id='check' icon={CheckIcon} className='verified__mark' />
)}
<span
dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }}
<dd className='translate' title={pair.value_plain ?? ''}>
{pair.verified_at && (
<span
title={intl.formatMessage(
{
id: 'account.link_verified_on',
defaultMessage:
'Ownership of this link was checked on {date}',
},
{
date: intl.formatDate(pair.verified_at, dateFormatOptions),
},
)}
>
<Icon id='check' icon={CheckIcon} className='verified__mark' />
</span>
)}{' '}
<EmojiHTML
as='span'
htmlString={pair.value_emojified}
extraEmojis={emojis}
{...htmlHandlers}
/>
</dd>
</dl>
))}
</div>
</>
);
};
const dateFormatOptions: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
};

View File

@@ -8,6 +8,7 @@ const meta = {
component: Alert,
args: {
isActive: true,
isLoading: false,
animateFrom: 'side',
title: '',
message: '',
@@ -20,6 +21,12 @@ const meta = {
type: 'boolean',
description: 'Animate to the active (displayed) state of the alert',
},
isLoading: {
control: 'boolean',
type: 'boolean',
description:
'Display a loading indicator in the alert, replacing the dismiss button if present',
},
animateFrom: {
control: 'radio',
type: 'string',
@@ -108,3 +115,11 @@ export const InSizedContainer: Story = {
</div>
),
};
export const WithLoadingIndicator: Story = {
args: {
...WithDismissButton.args,
isLoading: true,
},
render: InSizedContainer.render,
};

View File

@@ -3,6 +3,7 @@ import { useIntl } from 'react-intl';
import classNames from 'classnames';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { IconButton } from '../icon_button';
@@ -10,21 +11,23 @@ import { IconButton } from '../icon_button';
* Snackbar/Toast-style notification component.
*/
export const Alert: React.FC<{
isActive?: boolean;
animateFrom?: 'side' | 'below';
title?: string;
message: string;
action?: string;
onActionClick?: () => void;
onDismiss?: () => void;
isActive?: boolean;
isLoading?: boolean;
animateFrom?: 'side' | 'below';
}> = ({
isActive,
animateFrom = 'side',
title,
message,
action,
onActionClick,
onDismiss,
isActive,
isLoading,
animateFrom = 'side',
}) => {
const intl = useIntl();
@@ -51,7 +54,13 @@ export const Alert: React.FC<{
</button>
)}
{onDismiss && (
{isLoading && (
<span className='notification-bar__loading-indicator'>
<LoadingIndicator />
</span>
)}
{onDismiss && !isLoading && (
<IconButton
title={intl.formatMessage({
id: 'dismissable_banner.dismiss',

View File

@@ -28,7 +28,7 @@ const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
return [null, null];
}
word = word.trim().toLowerCase();
word = word.trim();
if (word.length > 0) {
return [left + 1, word];
@@ -61,7 +61,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
static defaultProps = {
autoFocus: true,
searchTokens: ['@', ':', '#'],
searchTokens: ['@', '', ':', '#', ''],
};
state = {

View File

@@ -25,11 +25,11 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
word = str.slice(left, right + caretPosition);
}
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
if (!word || word.trim().length < 3 || ['@', '', ':', '#', ''].indexOf(word[0]) === -1) {
return [null, null];
}
word = word.trim().toLowerCase();
word = word.trim();
if (word.length > 0) {
return [left + 1, word];
@@ -150,10 +150,7 @@ const AutosuggestTextarea = forwardRef(({
}, [suggestions, onSuggestionSelected, textareaRef]);
const handlePaste = useCallback((e) => {
if (e.clipboardData && e.clipboardData.files.length === 1) {
onPaste(e.clipboardData.files);
e.preventDefault();
}
onPaste(e);
}, [onPaste]);
// Show the suggestions again whenever they change and the textarea is focused

View File

@@ -30,9 +30,12 @@ const Blurhash: React.FC<Props> = ({
try {
const pixels = decode(hash, width, height);
const ctx = canvas.getContext('2d');
const imageData = new ImageData(pixels, width, height);
const imageData = ctx?.createImageData(width, height);
imageData?.data.set(pixels);
ctx?.putImageData(imageData, 0, 0);
if (imageData) {
ctx?.putImageData(imageData, 0, 0);
}
} catch (err) {
console.error('Blurhash decoding failure', { err, hash });
}

View File

@@ -1,25 +1,48 @@
import type { List } from 'immutable';
import type { CustomEmoji } from '../models/custom_emoji';
import type { Status } from '../models/status';
import { EmojiHTML } from './emoji/html';
import type { IconName } from './media_icon';
import { MediaIcon } from './media_icon';
import { StatusBanner, BannerVariant } from './status_banner';
export const ContentWarning: React.FC<{
text: string;
status: Status;
expanded?: boolean;
onClick?: () => void;
icons?: IconName[];
}> = ({ text, expanded, onClick, icons }) => (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Warning}
>
{icons?.map((icon) => (
<MediaIcon
className='status__content__spoiler-icon'
icon={icon}
key={`icon-${icon}`}
}> = ({ status, expanded, onClick, icons }) => {
const hasSpoiler = !!status.get('spoiler_text');
if (!hasSpoiler) {
return null;
}
const text =
status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml');
if (typeof text !== 'string' || text.length === 0) {
return null;
}
return (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Warning}
>
{icons?.map((icon) => (
<MediaIcon
className='status__content__spoiler-icon'
icon={icon}
key={`icon-${icon}`}
/>
))}
<EmojiHTML
as='span'
htmlString={text}
extraEmojis={status.get('emojis') as List<CustomEmoji>}
/>
))}
<span dangerouslySetInnerHTML={{ __html: text }} />
</StatusBanner>
);
</StatusBanner>
);
};

View File

@@ -2,30 +2,28 @@ import type { ComponentPropsWithoutRef, FC } from 'react';
import classNames from 'classnames';
import { EmojiHTML } from '@/flavours/glitch/features/emoji/emoji_html';
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
import { AnimateEmojiProvider } from '../emoji/context';
import { EmojiHTML } from '../emoji/html';
import { Skeleton } from '../skeleton';
import type { DisplayNameProps } from './index';
export const DisplayNameWithoutDomain: FC<
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
ComponentPropsWithoutRef<'span'>
> = ({ account, className, children, ...props }) => {
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
> = ({ account, className, children, localDomain: _, ...props }) => {
return (
<span {...props} className={classNames('display-name', className)}>
<AnimateEmojiProvider
{...props}
as='span'
className={classNames('display-name', className)}
>
<bdi>
{account ? (
<EmojiHTML
className='display-name__html'
htmlString={
isModernEmojiEnabled()
? account.get('display_name')
: account.get('display_name_html')
}
shallow
htmlString={account.get('display_name_html')}
as='strong'
extraEmojis={account.get('emojis')}
/>
) : (
<strong className='display-name__html'>
@@ -34,6 +32,6 @@ export const DisplayNameWithoutDomain: FC<
)}
</bdi>
{children}
</span>
</AnimateEmojiProvider>
);
};

View File

@@ -1,23 +1,24 @@
import type { ComponentPropsWithoutRef, FC } from 'react';
import { EmojiHTML } from '@/flavours/glitch/features/emoji/emoji_html';
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
import { EmojiHTML } from '../emoji/html';
import type { DisplayNameProps } from './index';
export const DisplayNameSimple: FC<
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
ComponentPropsWithoutRef<'span'>
> = ({ account, ...props }) => {
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
> = ({ account, localDomain: _, ...props }) => {
if (!account) {
return null;
}
const accountName = isModernEmojiEnabled()
? account.get('display_name')
: account.get('display_name_html');
return (
<bdi>
<EmojiHTML {...props} htmlString={accountName} shallow as='span' />
<EmojiHTML
{...props}
as='span'
htmlString={account.get('display_name_html')}
extraEmojis={account.get('emojis')}
/>
</bdi>
);
};

View File

@@ -26,6 +26,7 @@ import {
closeDropdownMenu,
} from 'flavours/glitch/actions/dropdown_menu';
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
import { fetchStatus } from 'flavours/glitch/actions/statuses';
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
import { isUserTouching } from 'flavours/glitch/is_mobile';
import {
@@ -42,16 +43,10 @@ import { IconButton } from './icon_button';
let id = 0;
export interface RenderItemFnHandlers {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
}
export type RenderItemFn<Item = MenuItem> = (
item: Item,
index: number,
handlers: RenderItemFnHandlers,
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void,
onClick: React.MouseEventHandler,
) => React.ReactNode;
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
@@ -101,7 +96,6 @@ export const DropdownMenu = <Item = MenuItem,>({
onItemClick,
}: DropdownMenuProps<Item>) => {
const nodeRef = useRef<HTMLDivElement>(null);
const focusedItemRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const handleDocumentClick = (e: MouseEvent) => {
@@ -163,8 +157,11 @@ export const DropdownMenu = <Item = MenuItem,>({
document.addEventListener('click', handleDocumentClick, { capture: true });
document.addEventListener('keydown', handleKeyDown, { capture: true });
if (focusedItemRef.current && openedViaKeyboard) {
focusedItemRef.current.focus({ preventScroll: true });
if (openedViaKeyboard) {
const firstMenuItem = nodeRef.current?.querySelector<
HTMLAnchorElement | HTMLButtonElement
>('li:first-child > :is(a, button)');
firstMenuItem?.focus({ preventScroll: true });
}
return () => {
@@ -175,13 +172,6 @@ export const DropdownMenu = <Item = MenuItem,>({
};
}, [onClose, openedViaKeyboard]);
const handleFocusedItemRef = useCallback(
(c: HTMLAnchorElement | HTMLButtonElement | null) => {
focusedItemRef.current = c as HTMLElement;
},
[],
);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
@@ -207,15 +197,6 @@ export const DropdownMenu = <Item = MenuItem,>({
[onClose, onItemClick, items],
);
const handleItemKeyUp = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
handleItemClick(e);
}
},
[handleItemClick],
);
const nativeRenderItem = (option: Item, i: number) => {
if (!isMenuItem(option)) {
return null;
@@ -232,9 +213,7 @@ export const DropdownMenu = <Item = MenuItem,>({
if (isActionItem(option)) {
element = (
<button
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
aria-disabled={disabled}
>
@@ -248,9 +227,7 @@ export const DropdownMenu = <Item = MenuItem,>({
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<DropdownMenuItemContent item={option} />
@@ -258,13 +235,7 @@ export const DropdownMenu = <Item = MenuItem,>({
);
} else {
element = (
<Link
to={option.to}
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<Link to={option.to} onClick={handleItemClick} data-index={i}>
<DropdownMenuItemContent item={option} />
</Link>
);
@@ -307,15 +278,7 @@ export const DropdownMenu = <Item = MenuItem,>({
})}
>
{items.map((option, i) =>
renderItemMethod(
option,
i,
{
onClick: handleItemClick,
onKeyUp: handleItemKeyUp,
},
i === 0 ? handleFocusedItemRef : undefined,
),
renderItemMethod(option, i, handleItemClick),
)}
</ul>
)}
@@ -340,6 +303,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
*/
scrollKey?: string;
status?: ImmutableMap<string, unknown>;
needsStatusRefresh?: boolean;
forceDropdown?: boolean;
renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>;
@@ -363,6 +327,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
placement = 'bottom',
offset = [5, 5],
status,
needsStatusRefresh,
forceDropdown = false,
renderItem,
renderHeader,
@@ -382,6 +347,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
const prefetchAccountId = status
? status.getIn(['account', 'id'])
: undefined;
const statusId = status?.get('id') as string | undefined;
const handleClose = useCallback(() => {
if (buttonRef.current) {
@@ -399,7 +365,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
}, [dispatch, currentId]);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
(e: React.MouseEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = items?.[i];
@@ -420,10 +386,20 @@ export const Dropdown = <Item extends object | null = MenuItem>({
[handleClose, onItemClick, items],
);
const toggleDropdown = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const { type } = e;
const isKeypressRef = useRef(false);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
isKeypressRef.current = true;
}
}, []);
const unsetIsKeypress = useCallback(() => {
isKeypressRef.current = false;
}, []);
const toggleDropdown = useCallback(
(e: React.MouseEvent) => {
if (open) {
handleClose();
} else {
@@ -436,6 +412,15 @@ export const Dropdown = <Item extends object | null = MenuItem>({
dispatch(fetchRelationships([prefetchAccountId]));
}
if (needsStatusRefresh && statusId) {
dispatch(
fetchStatus(statusId, {
forceFetch: true,
alsoFetchContext: false,
}),
);
}
if (isUserTouching() && !forceDropdown) {
dispatch(
openModal({
@@ -450,10 +435,11 @@ export const Dropdown = <Item extends object | null = MenuItem>({
dispatch(
openDropdownMenu({
id: currentId,
keyboard: type !== 'click',
keyboard: isKeypressRef.current,
scrollKey,
}),
);
isKeypressRef.current = false;
}
}
},
@@ -468,6 +454,8 @@ export const Dropdown = <Item extends object | null = MenuItem>({
items,
forceDropdown,
handleClose,
statusId,
needsStatusRefresh,
],
);
@@ -484,6 +472,9 @@ export const Dropdown = <Item extends object | null = MenuItem>({
const buttonProps = {
disabled,
onClick: toggleDropdown,
onKeyDown: handleKeyDown,
onKeyUp: unsetIsKeypress,
onBlur: unsetIsKeypress,
'aria-expanded': open,
'aria-controls': menuId,
ref: buttonRef,

View File

@@ -58,17 +58,7 @@ export const EditedTimestamp: React.FC<{
}, []);
const renderItem = useCallback(
(
item: HistoryItem,
index: number,
{
onClick,
onKeyUp,
}: {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
},
) => {
(item: HistoryItem, index: number, onClick: React.MouseEventHandler) => {
const formattedDate = (
<RelativeTimestamp
timestamp={item.get('created_at') as string}
@@ -98,7 +88,7 @@ export const EditedTimestamp: React.FC<{
className='dropdown-menu__item edited-timestamp__history__item'
key={item.get('created_at') as string}
>
<button data-index={index} onClick={onClick} onKeyUp={onKeyUp}>
<button data-index={index} onClick={onClick} type='button'>
{label}
</button>
</li>

View File

@@ -0,0 +1,102 @@
import type { MouseEventHandler, PropsWithChildren } from 'react';
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { cleanExtraEmojis } from '@/flavours/glitch/features/emoji/normalize';
import { autoPlayGif } from '@/flavours/glitch/initial_state';
import { polymorphicForwardRef } from '@/types/polymorphic';
import type {
CustomEmojiMapArg,
ExtraCustomEmojiMap,
} from 'flavours/glitch/features/emoji/types';
// Animation context
export const AnimateEmojiContext = createContext<boolean | null>(null);
// Polymorphic provider component
type AnimateEmojiProviderProps = Required<PropsWithChildren> & {
className?: string;
};
export const AnimateEmojiProvider = polymorphicForwardRef<
'div',
AnimateEmojiProviderProps
>(
(
{
children,
as: Wrapper = 'div',
className,
onMouseEnter,
onMouseLeave,
...props
},
ref,
) => {
const [animate, setAnimate] = useState(autoPlayGif ?? false);
const handleEnter: MouseEventHandler<HTMLDivElement> = useCallback(
(event) => {
onMouseEnter?.(event);
if (!autoPlayGif) {
setAnimate(true);
}
},
[onMouseEnter],
);
const handleLeave: MouseEventHandler<HTMLDivElement> = useCallback(
(event) => {
onMouseLeave?.(event);
if (!autoPlayGif) {
setAnimate(false);
}
},
[onMouseLeave],
);
// If there's a parent context or GIFs autoplay, we don't need handlers.
const parentContext = useContext(AnimateEmojiContext);
if (parentContext !== null) {
return (
<Wrapper {...props} className={className} ref={ref}>
{children}
</Wrapper>
);
}
return (
<Wrapper
{...props}
className={className}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
ref={ref}
>
<AnimateEmojiContext.Provider value={animate}>
{children}
</AnimateEmojiContext.Provider>
</Wrapper>
);
},
);
AnimateEmojiProvider.displayName = 'AnimateEmojiProvider';
// Handle custom emoji
export const CustomEmojiContext = createContext<ExtraCustomEmojiMap>({});
export const CustomEmojiProvider = ({
children,
emojis: rawEmojis,
}: PropsWithChildren<{ emojis?: CustomEmojiMapArg }>) => {
const emojis = useMemo(() => cleanExtraEmojis(rawEmojis) ?? {}, [rawEmojis]);
return (
<CustomEmojiContext.Provider value={emojis}>
{children}
</CustomEmojiContext.Provider>
);
};

View File

@@ -0,0 +1,56 @@
import type { ComponentProps } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { importCustomEmojiData } from '@/flavours/glitch/features/emoji/loader';
import { Emoji } from './index';
type EmojiProps = ComponentProps<typeof Emoji> & { state: string };
const meta = {
title: 'Components/Emoji',
component: Emoji,
args: {
code: '🖤',
state: 'auto',
},
argTypes: {
code: {
name: 'Emoji',
},
state: {
control: {
type: 'select',
labels: {
auto: 'Auto',
native: 'Native',
twemoji: 'Twemoji',
},
},
options: ['auto', 'native', 'twemoji'],
name: 'Emoji Style',
mapping: {
auto: { meta: { emoji_style: 'auto' } },
native: { meta: { emoji_style: 'native' } },
twemoji: { meta: { emoji_style: 'twemoji' } },
},
},
},
render(args) {
void importCustomEmojiData();
return <Emoji {...args} />;
},
} satisfies Meta<EmojiProps>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const CustomEmoji: Story = {
args: {
code: ':custom:',
},
};

View File

@@ -0,0 +1,59 @@
import { useMemo } from 'react';
import type { CustomEmojiMapArg } from '@/flavours/glitch/features/emoji/types';
import type {
OnAttributeHandler,
OnElementHandler,
} from '@/flavours/glitch/utils/html';
import { htmlStringToComponents } from '@/flavours/glitch/utils/html';
import { polymorphicForwardRef } from '@/types/polymorphic';
import { AnimateEmojiProvider, CustomEmojiProvider } from './context';
import { textToEmojis } from './index';
export interface EmojiHTMLProps {
htmlString: string;
extraEmojis?: CustomEmojiMapArg;
className?: string;
onElement?: OnElementHandler;
onAttribute?: OnAttributeHandler;
}
export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
(
{
extraEmojis,
htmlString,
as: asProp = 'div', // Rename for syntax highlighting
className = '',
onElement,
onAttribute,
...props
},
ref,
) => {
const contents = useMemo(
() =>
htmlStringToComponents(htmlString, {
onText: textToEmojis,
onElement,
onAttribute,
}),
[htmlString, onAttribute, onElement],
);
return (
<CustomEmojiProvider emojis={extraEmojis}>
<AnimateEmojiProvider
{...props}
as={asProp}
className={className}
ref={ref}
>
{contents}
</AnimateEmojiProvider>
</CustomEmojiProvider>
);
},
);
EmojiHTML.displayName = 'EmojiHTML';

View File

@@ -0,0 +1,99 @@
import type { FC } from 'react';
import { useContext, useEffect, useState } from 'react';
import { EMOJI_TYPE_CUSTOM } from '@/flavours/glitch/features/emoji/constants';
import { useEmojiAppState } from '@/flavours/glitch/features/emoji/mode';
import { unicodeHexToUrl } from '@/flavours/glitch/features/emoji/normalize';
import {
isStateLoaded,
loadEmojiDataToState,
shouldRenderImage,
stringToEmojiState,
tokenizeText,
} from '@/flavours/glitch/features/emoji/render';
import { AnimateEmojiContext, CustomEmojiContext } from './context';
interface EmojiProps {
code: string;
showFallback?: boolean;
showLoading?: boolean;
}
export const Emoji: FC<EmojiProps> = ({
code,
showFallback = true,
showLoading = true,
}) => {
const customEmoji = useContext(CustomEmojiContext);
// First, set the emoji state based on the input code.
const [state, setState] = useState(() =>
stringToEmojiState(code, customEmoji),
);
// If we don't have data, then load emoji data asynchronously.
const appState = useEmojiAppState();
useEffect(() => {
if (state !== null) {
void loadEmojiDataToState(state, appState.currentLocale).then(setState);
}
}, [appState.currentLocale, state]);
const animate = useContext(AnimateEmojiContext);
const fallback = showFallback ? code : null;
// If the code is invalid or we otherwise know it's not valid, show the fallback.
if (!state) {
return fallback;
}
if (!shouldRenderImage(state, appState.mode)) {
return code;
}
if (!isStateLoaded(state)) {
if (showLoading) {
return <span className='emojione emoji-loading' title={code} />;
}
return fallback;
}
if (state.type === EMOJI_TYPE_CUSTOM) {
const shortcode = `:${state.code}:`;
return (
<img
src={animate ? state.data.url : state.data.static_url}
alt={shortcode}
title={shortcode}
className='emojione custom-emoji'
loading='lazy'
/>
);
}
const src = unicodeHexToUrl(state.code, appState.darkTheme);
return (
<img
src={src}
alt={state.data.unicode}
title={state.data.label}
className='emojione'
loading='lazy'
/>
);
};
/**
* Takes a text string and converts it to an array of React nodes.
* @param text The text to be tokenized and converted.
*/
export function textToEmojis(text: string) {
return tokenizeText(text).map((token, index) => {
if (typeof token === 'string') {
return token;
}
return <Emoji code={token.code} key={`emoji-${token.code}-${index}`} />;
});
}

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
/**
* A helper component for managing the rendering of components that
* need to stay in the DOM a bit longer to finish their CSS exit animation.
*
* In the future, replace this component with plain CSS once that is feasible.
* This will require broader support for `transition-behavior: allow-discrete`
* and https://developer.mozilla.org/en-US/docs/Web/CSS/overlay.
*/
export const ExitAnimationWrapper: React.FC<{
/**
* Set this to true to indicate that the nested component should be rendered
*/
isActive: boolean;
/**
* How long the component should be rendered after `isActive` was set to `false`
*/
delayMs?: number;
/**
* Set this to true to also delay the entry of the nested component until after
* another one has exited full.
*/
withEntryDelay?: boolean;
/**
* Render prop that provides the nested component with the `delayedIsActive` flag
*/
children: (delayedIsActive: boolean) => React.ReactNode;
}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => {
const [delayedIsActive, setDelayedIsActive] = useState(false);
useEffect(() => {
if (isActive && !withEntryDelay) {
setDelayedIsActive(true);
return () => '';
} else {
const timeout = setTimeout(() => {
setDelayedIsActive(isActive);
}, delayMs);
return () => {
clearTimeout(timeout);
};
}
}, [isActive, delayMs, withEntryDelay]);
if (!isActive && !delayedIsActive) {
return null;
}
return children(isActive && delayedIsActive);
};

View File

@@ -20,7 +20,7 @@ import { useDrag } from '@use-gesture/react';
import { expandAccountFeaturedTimeline } from '@/flavours/glitch/actions/timelines';
import { Icon } from '@/flavours/glitch/components/icon';
import { IconButton } from '@/flavours/glitch/components/icon_button';
import StatusContainer from '@/flavours/glitch/containers/status_container';
import { StatusQuoteManager } from '@/flavours/glitch/components/status_quoted';
import { usePrevious } from '@/flavours/glitch/hooks/usePrevious';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
@@ -218,12 +218,7 @@ const FeaturedCarouselItem: React.FC<
ref={handleRef}
{...props}
>
<StatusContainer
// @ts-expect-error inferred props are wrong
id={statusId}
contextType='account'
withCounters
/>
<StatusQuoteManager id={statusId} contextType='account' withCounters />
</animated.div>
);
};

View File

@@ -8,6 +8,7 @@ import { useIdentity } from '@/flavours/glitch/identity_context';
import {
fetchRelationships,
followAccount,
unmuteAccount,
} from 'flavours/glitch/actions/accounts';
import { openModal } from 'flavours/glitch/actions/modal';
import { Button } from 'flavours/glitch/components/button';
@@ -15,17 +16,50 @@ import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { me } from 'flavours/glitch/initial_state';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
const messages = defineMessages({
import { useBreakpoint } from '../features/ui/hooks/useBreakpoint';
const longMessages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
followRequest: {
id: 'account.follow_request',
defaultMessage: 'Request to follow',
},
followRequestCancel: {
id: 'account.follow_request_cancel',
defaultMessage: 'Cancel request',
},
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});
const shortMessages = {
...longMessages, // Align type signature of shortMessages and longMessages
...defineMessages({
followBack: {
id: 'account.follow_back_short',
defaultMessage: 'Follow back',
},
followRequest: {
id: 'account.follow_request_short',
defaultMessage: 'Request',
},
followRequestCancel: {
id: 'account.follow_request_cancel_short',
defaultMessage: 'Cancel',
},
editProfile: { id: 'account.edit_profile_short', defaultMessage: 'Edit' },
}),
};
export const FollowButton: React.FC<{
accountId?: string;
compact?: boolean;
}> = ({ accountId, compact }) => {
labelLength?: 'auto' | 'short' | 'long';
className?: string;
}> = ({ accountId, compact, labelLength = 'auto', className }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { signedIn } = useIdentity();
@@ -60,29 +94,60 @@ export const FollowButton: React.FC<{
if (accountId === me) {
return;
} else if (account && (relationship.following || relationship.requested)) {
} else if (relationship.muting) {
dispatch(unmuteAccount(accountId));
} else if (account && relationship.following) {
dispatch(
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
);
} else if (account && relationship.requested) {
dispatch(
openModal({
modalType: 'CONFIRM_WITHDRAW_REQUEST',
modalProps: { account },
}),
);
} else if (relationship.blocking) {
dispatch(
openModal({
modalType: 'CONFIRM_UNBLOCK',
modalProps: { account },
}),
);
} else {
dispatch(followAccount(accountId));
}
}, [dispatch, accountId, relationship, account, signedIn]);
const isNarrow = useBreakpoint('narrow');
const useShortLabel =
labelLength === 'short' || (labelLength === 'auto' && isNarrow);
const messages = useShortLabel ? shortMessages : longMessages;
const followMessage = account?.locked
? messages.followRequest
: messages.follow;
let label;
if (!signedIn) {
label = intl.formatMessage(messages.follow);
label = intl.formatMessage(followMessage);
} else if (accountId === me) {
label = intl.formatMessage(messages.edit_profile);
} else if (!relationship) {
label = <LoadingIndicator />;
} else if (relationship.following || relationship.requested) {
} else if (relationship.muting) {
label = intl.formatMessage(messages.unmute);
} else if (relationship.following) {
label = intl.formatMessage(messages.unfollow);
} else if (relationship.followed_by) {
} else if (relationship.blocking) {
label = intl.formatMessage(messages.unblock);
} else if (relationship.requested) {
label = intl.formatMessage(messages.followRequestCancel);
} else if (relationship.followed_by && !account?.locked) {
label = intl.formatMessage(messages.followBack);
} else {
label = intl.formatMessage(messages.follow);
label = intl.formatMessage(followMessage);
}
if (accountId === me) {
@@ -91,7 +156,7 @@ export const FollowButton: React.FC<{
href='/settings/profile'
target='_blank'
rel='noopener'
className={classNames('button button-secondary', {
className={classNames(className, 'button button-secondary', {
'button--compact': compact,
})}
>
@@ -105,13 +170,12 @@ export const FollowButton: React.FC<{
onClick={handleClick}
disabled={
relationship?.blocked_by ||
relationship?.blocking ||
(!(relationship?.following || relationship?.requested) &&
(account?.suspended || !!account?.moved))
}
secondary={following}
compact={compact}
className={following ? 'button--destructive' : undefined}
className={classNames(className, { 'button--destructive': following })}
>
{label}
</Button>

View File

@@ -33,7 +33,7 @@ function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
return (
element instanceof HTMLAnchorElement &&
// it may be a <a> starting with a hashtag
(element.textContent?.[0] === '#' ||
(element.textContent.startsWith('#') ||
// or a #<a>
element.previousSibling?.textContent?.[
element.previousSibling.textContent.length - 1

View File

@@ -105,6 +105,7 @@ const hotkeyMatcherMap = {
reply: just('r'),
favourite: just('f'),
boost: just('b'),
bookmark: just('d'),
quote: just('q'),
mention: just('m'),
open: any('enter', 'o'),
@@ -180,25 +181,24 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
if (shouldHandleEvent) {
const matchCandidates: {
handler: (event: KeyboardEvent) => void;
// A candidate will be have an undefined handler if it's matched,
// but handled in a parent component rather than this one.
handler: ((event: KeyboardEvent) => void) | undefined;
priority: number;
}[] = [];
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
(handlerName) => {
const handler = handlersRef.current[handlerName];
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
if (handler) {
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
const { isMatch, priority } = hotkeyMatcher(
event,
bufferedKeys.current,
);
const { isMatch, priority } = hotkeyMatcher(
event,
bufferedKeys.current,
);
if (isMatch) {
matchCandidates.push({ handler, priority });
}
if (isMatch) {
matchCandidates.push({ handler, priority });
}
},
);

View File

@@ -109,7 +109,14 @@ export const HoverCardAccount = forwardRef<
accountId={account.id}
className='hover-card__bio'
/>
<AccountFields fields={account.fields} limit={2} />
<div className='account-fields'>
<AccountFields
fields={account.fields.take(2)}
emojis={account.emojis}
/>
</div>
{note && note.length > 0 && (
<dl className='hover-card__note'>
<dt className='hover-card__note-label'>

View File

@@ -0,0 +1,66 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect } from 'storybook/test';
import { HTMLBlock } from './index';
const meta = {
title: 'Components/HTMLBlock',
component: HTMLBlock,
args: {
htmlString: `<p>Hello, world!</p>
<p><a href="#">A link</a></p>
<p>This should be filtered out: <button>Bye!</button></p>
<p>This also has emoji: 🖤</p>`,
},
argTypes: {
extraEmojis: {
table: {
disable: true,
},
},
onElement: {
table: {
disable: true,
},
},
onAttribute: {
table: {
disable: true,
},
},
},
render(args) {
return (
// Just for visual clarity in Storybook.
<HTMLBlock
{...args}
style={{
border: '1px solid black',
padding: '1rem',
minWidth: '300px',
}}
/>
);
},
// Force Twemoji to demonstrate emoji rendering.
parameters: {
state: {
meta: {
emoji_style: 'twemoji',
},
},
},
} satisfies Meta<typeof HTMLBlock>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
async play({ canvas }) {
const link = canvas.queryByRole('link');
await expect(link).toBeInTheDocument();
const button = canvas.queryByRole('button');
await expect(button).not.toBeInTheDocument();
},
};

View File

@@ -0,0 +1,30 @@
import { useCallback } from 'react';
import type { OnElementHandler } from '@/flavours/glitch/utils/html';
import { polymorphicForwardRef } from '@/types/polymorphic';
import type { EmojiHTMLProps } from '../emoji/html';
import { EmojiHTML } from '../emoji/html';
import { useElementHandledLink } from '../status/handled_link';
export const HTMLBlock = polymorphicForwardRef<
'div',
EmojiHTMLProps & Parameters<typeof useElementHandledLink>[0]
>(
({
onElement: onParentElement,
hrefToMention,
hashtagAccountId,
...props
}) => {
const { onElement: onLinkElement } = useElementHandledLink({
hrefToMention,
hashtagAccountId,
});
const onElement: OnElementHandler = useCallback(
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
[onLinkElement, onParentElement],
);
return <EmojiHTML {...props} onElement={onElement} />;
},
);

View File

@@ -8,13 +8,12 @@ import classNames from 'classnames';
import { animated, useSpring } from '@react-spring/web';
import escapeTextContentForBrowser from 'escape-html';
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
import { fetchPoll, vote } from 'flavours/glitch/actions/polls';
import { Icon } from 'flavours/glitch/components/icon';
import emojify from 'flavours/glitch/features/emoji/emoji';
import { useIdentity } from 'flavours/glitch/identity_context';
import { makeEmojiMap } from 'flavours/glitch/models/custom_emoji';
import type * as Model from 'flavours/glitch/models/poll';
import type { Status } from 'flavours/glitch/models/status';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
@@ -234,12 +233,11 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
if (!titleHtml) {
const emojiMap = makeEmojiMap(poll.emojis);
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
titleHtml = escapeTextContentForBrowser(title);
}
return titleHtml;
}, [option, poll, title]);
}, [option, title]);
// Handlers
const handleOptionChange = useCallback(() => {
@@ -305,10 +303,11 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
</span>
)}
<span
<EmojiHTML
className='poll__option__text translate'
lang={lang}
dangerouslySetInnerHTML={{ __html: titleHtml }}
htmlString={titleHtml}
extraEmojis={poll.emojis}
/>
{!!voted && (

View File

@@ -1,6 +1,7 @@
import type { PropsWithChildren } from 'react';
import type React from 'react';
import type { useLocation } from 'react-router';
import { Router as OriginalRouter, useHistory } from 'react-router';
import type {
@@ -18,7 +19,9 @@ interface MastodonLocationState {
mastodonModalKey?: string;
}
type LocationState = MastodonLocationState | null | undefined;
export type LocationState = MastodonLocationState | null | undefined;
export type MastodonLocation = ReturnType<typeof useLocation<LocationState>>;
type HistoryPath = Path | LocationDescriptor<LocationState>;

View File

@@ -10,7 +10,7 @@ import { connect } from 'react-redux';
import { supportsPassiveEvents } from 'detect-passive-events';
import { throttle } from 'lodash';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import { ScrollContainer } from 'flavours/glitch/containers/scroll_container';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
@@ -399,7 +399,7 @@ class ScrollableList extends PureComponent {
if (trackScroll) {
return (
<ScrollContainer scrollKey={scrollKey}>
<ScrollContainer scrollKey={scrollKey} childRef={this.setRef}>
{scrollableArea}
</ScrollContainer>
);

View File

@@ -118,6 +118,7 @@ class Status extends ImmutablePureComponent {
prepend: PropTypes.string,
withDismiss: PropTypes.bool,
isQuotedPost: PropTypes.bool,
shouldHighlightOnMount: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
expanded: PropTypes.bool,
@@ -705,6 +706,7 @@ class Status extends ImmutablePureComponent {
muted: this.props.muted,
'status--is-quote': isQuotedPost,
'status--has-quote': !!status.get('quote'),
'status--highlighted-entry': this.props.shouldHighlightOnMount,
})
}
data-id={status.get('id')}
@@ -737,7 +739,7 @@ class Status extends ImmutablePureComponent {
</header>
)}
{status.get('spoiler_text').length > 0 && <ContentWarning text={status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml')} expanded={expanded} onClick={this.handleExpandedToggle} icons={mediaIcons} />}
<ContentWarning status={status} expanded={expanded} onClick={this.handleExpandedToggle} icons={mediaIcons} />
{expanded && (
<>
@@ -748,8 +750,6 @@ class Status extends ImmutablePureComponent {
collapsible
media={media}
onCollapsedToggle={this.handleCollapsedToggle}
tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')}
{...statusContentProps}
/>

View File

@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import type { StatusVisibility } from '@/flavours/glitch/api_types/statuses';
import { statusFactoryState } from '@/testing/factories';
import { LegacyReblogButton, StatusBoostButton } from './boost_button';
import { BoostButton } from './boost_button';
interface StoryProps {
visibility: StatusVisibility;
@@ -38,10 +38,7 @@ const meta = {
},
},
render: (args) => (
<StatusBoostButton
status={argsToStatus(args)}
counters={args.reblogCount > 0}
/>
<BoostButton status={argsToStatus(args)} counters={args.reblogCount > 0} />
),
} satisfies Meta<StoryProps>;
@@ -78,12 +75,3 @@ export const Mine: Story = {
},
},
};
export const Legacy: Story = {
render: (args) => (
<LegacyReblogButton
status={argsToStatus(args)}
counters={args.reblogCount > 0}
/>
),
};

View File

@@ -8,13 +8,14 @@ import classNames from 'classnames';
import { quoteComposeById } from '@/flavours/glitch/actions/compose_typed';
import { toggleReblog } from '@/flavours/glitch/actions/interactions';
import { openModal } from '@/flavours/glitch/actions/modal';
import { fetchStatus } from '@/flavours/glitch/actions/statuses';
import { quickBoosting } from '@/flavours/glitch/initial_state';
import type { ActionMenuItem } from '@/flavours/glitch/models/dropdown_menu';
import type { Status } from '@/flavours/glitch/models/status';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import { isFeatureEnabled } from '@/flavours/glitch/utils/environment';
import type { SomeRequired } from '@/flavours/glitch/utils/types';
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
import type { RenderItemFn } from '../dropdown_menu';
import { Dropdown, DropdownMenuItemContent } from '../dropdown_menu';
import { IconButton } from '../icon_button';
@@ -25,18 +26,61 @@ import {
selectStatusState,
} from './boost_button_utils';
const renderMenuItem: RenderItemFn<ActionMenuItem> = (
item,
index,
handlers,
focusRefCallback,
) => (
const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const statusState = useAppSelector((state) =>
selectStatusState(state, status),
);
const { title, meta, iconComponent, disabled } = useMemo(
() => boostItemState(statusState),
[statusState],
);
const handleClick: MouseEventHandler = useCallback(
(event) => {
if (statusState.isLoggedIn) {
dispatch(toggleReblog(status.get('id') as string, event.shiftKey));
} else {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}
},
[dispatch, status, statusState.isLoggedIn],
);
return (
<IconButton
disabled={disabled}
active={!!status.get('reblogged')}
title={intl.formatMessage(meta ?? title)}
icon='retweet'
iconComponent={iconComponent}
onClick={!disabled ? handleClick : undefined}
counter={
counters
? (status.get('reblogs_count') as number) +
(status.get('quotes_count') as number)
: undefined
}
/>
);
};
const renderMenuItem: RenderItemFn<ActionMenuItem> = (item, index, onClick) => (
<ReblogMenuItem
index={index}
item={item}
handlers={handlers}
onClick={onClick}
key={`${item.text}-${index}`}
focusRefCallback={focusRefCallback}
/>
);
@@ -47,10 +91,7 @@ interface ReblogButtonProps {
type ActionMenuItemWithIcon = SomeRequired<ActionMenuItem, 'icon'>;
export const StatusBoostButton: FC<ReblogButtonProps> = ({
status,
counters,
}) => {
const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const statusState = useAppSelector((state) =>
@@ -71,6 +112,7 @@ export const StatusBoostButton: FC<ReblogButtonProps> = ({
const statusId = status.get('id') as string;
const wasBoosted = !!status.get('reblogged');
const quoteApproval = status.get('quote_approval');
const showLoginPrompt = useCallback(() => {
dispatch(
@@ -127,9 +169,16 @@ export const StatusBoostButton: FC<ReblogButtonProps> = ({
dispatch(toggleReblog(status.get('id'), true));
return false;
}
if (quoteApproval === null) {
dispatch(
fetchStatus(statusId, { forceFetch: true, alsoFetchContext: false }),
);
}
return true;
},
[dispatch, isLoggedIn, showLoginPrompt, status],
[dispatch, isLoggedIn, showLoginPrompt, status, quoteApproval, statusId],
);
return (
@@ -162,16 +211,10 @@ export const StatusBoostButton: FC<ReblogButtonProps> = ({
interface ReblogMenuItemProps {
item: ActionMenuItem;
index: number;
handlers: RenderItemFnHandlers;
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void;
onClick: React.MouseEventHandler;
}
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
index,
item,
handlers,
focusRefCallback,
}) => {
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({ index, item, onClick }) => {
const { text, highlighted, disabled } = item;
return (
@@ -181,76 +224,15 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
})}
key={`${text}-${index}`}
>
<button
{...handlers}
ref={focusRefCallback}
aria-disabled={disabled}
data-index={index}
>
<button onClick={onClick} aria-disabled={disabled} data-index={index}>
<DropdownMenuItemContent item={item} />
</button>
</li>
);
};
// Legacy helpers
// Switch between the legacy and new reblog button based on feature flag.
export const BoostButton: FC<ReblogButtonProps> = (props) => {
if (isFeatureEnabled('outgoing_quotes')) {
return <StatusBoostButton {...props} />;
}
return <LegacyReblogButton {...props} />;
};
export const LegacyReblogButton: FC<ReblogButtonProps> = ({
status,
counters,
}) => {
const intl = useIntl();
const statusState = useAppSelector((state) =>
selectStatusState(state, status),
);
const { title, meta, iconComponent, disabled } = useMemo(
() => boostItemState(statusState),
[statusState],
);
const dispatch = useAppDispatch();
const handleClick: MouseEventHandler = useCallback(
(event) => {
if (statusState.isLoggedIn) {
dispatch(toggleReblog(status.get('id') as string, event.shiftKey));
} else {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}
},
[dispatch, status, statusState.isLoggedIn],
);
return (
<IconButton
disabled={disabled}
active={!!status.get('reblogged')}
title={intl.formatMessage(meta ?? title)}
icon='retweet'
iconComponent={iconComponent}
onClick={!disabled ? handleClick : undefined}
counter={
counters
? (status.get('reblogs_count') as number) +
(status.get('quotes_count') as number)
: undefined
}
/>
);
};
// Switch between the standalone boost button or the
// "Boost or quote" menu based on the quickBoosting preference
export const BoostButton = quickBoosting
? StandaloneBoostButton
: BoostOrQuoteMenu;

View File

@@ -0,0 +1,106 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { HashtagMenuController } from '@/flavours/glitch/features/ui/components/hashtag_menu_controller';
import { accountFactoryState } from '@/testing/factories';
import { HoverCardController } from '../hover_card_controller';
import type { HandledLinkProps } from './handled_link';
import { HandledLink } from './handled_link';
type HandledLinkStoryProps = Pick<
HandledLinkProps,
'href' | 'text' | 'prevText'
> & {
mentionAccount: 'local' | 'remote' | 'none';
hashtagAccount: boolean;
};
const meta = {
title: 'Components/Status/HandledLink',
render({ mentionAccount, hashtagAccount, ...args }) {
let mention: HandledLinkProps['mention'] | undefined;
if (mentionAccount === 'local') {
mention = { id: '1', acct: 'testuser', username: 'testuser' };
} else if (mentionAccount === 'remote') {
mention = {
id: '2',
acct: 'remoteuser@mastodon.social',
username: 'remoteuser',
};
}
return (
<>
<HandledLink
{...args}
mention={mention}
hashtagAccountId={hashtagAccount ? '1' : undefined}
>
<span>{args.text}</span>
</HandledLink>
<HashtagMenuController />
<HoverCardController />
</>
);
},
args: {
href: 'https://example.com/path/subpath?query=1#hash',
text: 'https://example.com',
mentionAccount: 'none',
hashtagAccount: false,
},
argTypes: {
mentionAccount: {
control: { type: 'select' },
options: ['local', 'remote', 'none'],
defaultValue: 'none',
},
},
parameters: {
state: {
accounts: {
'1': accountFactoryState({ id: '1', acct: 'hashtaguser' }),
},
},
},
} satisfies Meta<HandledLinkStoryProps>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Simple: Story = {
args: {
href: 'https://example.com/test',
},
};
export const Hashtag: Story = {
args: {
text: '#example',
hashtagAccount: true,
},
};
export const Mention: Story = {
args: {
text: '@user',
mentionAccount: 'local',
},
};
export const InternalLink: Story = {
args: {
href: '/about',
text: 'About',
},
};
export const InvalidURL: Story = {
args: {
href: 'ht!tp://invalid-url',
text: 'ht!tp://invalid-url -- invalid!',
},
};

View File

@@ -0,0 +1,240 @@
import { useCallback, useEffect, useRef } from 'react';
import type { ComponentProps, FC } from 'react';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { ApiMentionJSON } from '@/flavours/glitch/api_types/statuses';
import { useAppSelector } from '@/flavours/glitch/store';
import type { OnElementHandler } from '@/flavours/glitch/utils/html';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
export interface HandledLinkProps {
href: string;
text: string;
prevText?: string;
hashtagAccountId?: string;
mention?: Pick<ApiMentionJSON, 'id' | 'acct' | 'username'>;
}
const textMatchesTarget = (text: string, origin: string, host: string) => {
return (
text === origin ||
text === host ||
text.startsWith(origin + '/') ||
text.startsWith(host + '/') ||
'www.' + text === host ||
('www.' + text).startsWith(host + '/')
);
};
export const isLinkMisleading = (link: HTMLAnchorElement) => {
const linkTextParts: string[] = [];
// Reconstruct visible text, as we do not have much control over how links
// from remote software look, and we can't rely on `innerText` because the
// `invisible` class does not set `display` to `none`.
const walk = (node: Node) => {
if (node instanceof Text) {
linkTextParts.push(node.textContent);
} else if (node instanceof HTMLElement) {
if (node.classList.contains('invisible')) return;
for (const child of node.childNodes) {
walk(child);
}
}
};
walk(link);
const linkText = linkTextParts.join('');
const targetURL = new URL(link.href);
if (targetURL.protocol === 'magnet:') {
return !linkText.startsWith('magnet:');
}
if (targetURL.protocol === 'xmpp:') {
return !(
linkText === targetURL.href || 'xmpp:' + linkText === targetURL.href
);
}
// The following may not work with international domain names
if (
textMatchesTarget(linkText, targetURL.origin, targetURL.host) ||
textMatchesTarget(linkText.toLowerCase(), targetURL.origin, targetURL.host)
) {
return false;
}
// The link hasn't been recognized, maybe it features an international domain name
const hostname = decodeIDNA(targetURL.hostname).normalize('NFKC');
const host = targetURL.host.replace(targetURL.hostname, hostname);
const origin = targetURL.origin.replace(targetURL.host, host);
const text = linkText.normalize('NFKC');
return !(
textMatchesTarget(text, origin, host) ||
textMatchesTarget(text.toLowerCase(), origin, host)
);
};
export const tagMisleadingLink = (link: HTMLAnchorElement) => {
try {
if (isLinkMisleading(link)) {
const url = new URL(link.href);
const tag = document.createElement('span');
tag.classList.add('link-origin-tag');
switch (url.protocol) {
case 'xmpp:':
tag.textContent = `[${url.href}]`;
break;
case 'magnet:':
tag.textContent = '(magnet)';
break;
default:
tag.textContent = `[${url.host}]`;
}
link.insertAdjacentText('beforeend', ' ');
link.insertAdjacentElement('beforeend', tag);
}
} catch (e) {
// The URL is invalid, remove the href just to be safe
if (e instanceof TypeError) link.removeAttribute('href');
}
};
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
href,
text,
prevText,
hashtagAccountId,
mention,
className,
children,
...props
}) => {
const rewriteMentions = useAppSelector(
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
(state) => state.local_settings.get('rewrite_mentions', 'no') as string,
);
const tagLinks = useAppSelector(
(state) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
state.local_settings.get('tag_misleading_links', false) as string,
);
const linkRef = useRef<HTMLAnchorElement>(null);
useEffect(() => {
if (tagLinks && linkRef.current) tagMisleadingLink(linkRef.current);
}, [tagLinks]);
// Handle hashtags
if (
(text.startsWith('#') ||
prevText?.endsWith('#') ||
text.startsWith('') ||
prevText?.endsWith('')) &&
!text.includes('%')
) {
const hashtag = text.slice(1).trim();
return (
<Link
className={classNames('mention hashtag', className)}
to={`/tags/${encodeURIComponent(hashtag)}`}
rel='tag'
data-menu-hashtag={hashtagAccountId}
>
{children}
</Link>
);
} else if (mention) {
// glitch-soc feature to rewrite mentions
if (rewriteMentions !== 'no') {
return (
<Link
className={classNames('mention', className)}
to={`/@${mention.acct}`}
title={`@${mention.acct}`}
data-hover-card-account={mention.id}
>
@
<span>
{rewriteMentions === 'acct' ? mention.acct : mention.username}
</span>
</Link>
);
}
// Handle mentions
return (
<Link
className={classNames('mention', className)}
to={`/@${mention.acct}`}
title={`@${mention.acct}`}
data-hover-card-account={mention.id}
>
{children}
</Link>
);
}
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
if (href.startsWith('/')) {
return (
<Link className={classNames('unhandled-link', className)} to={href}>
{children}
</Link>
);
}
return (
<a
{...props}
href={href}
title={href}
className={classNames('unhandled-link', className)}
target='_blank'
rel='noopener'
translate='no'
ref={linkRef}
>
{children}
</a>
);
};
export const useElementHandledLink = ({
hashtagAccountId,
hrefToMention,
}: {
hashtagAccountId?: string;
hrefToMention?: (href: string) => ApiMentionJSON | undefined;
} = {}) => {
const onElement = useCallback<OnElementHandler>(
(element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) {
const mention = hrefToMention?.(element.href);
return (
<HandledLink
{...props}
key={key as string} // React requires keys to not be part of spread props.
href={element.href}
text={element.innerText}
prevText={element.previousSibling?.textContent ?? undefined}
hashtagAccountId={hashtagAccountId}
mention={mention}
>
{children}
</HandledLink>
);
}
return undefined;
},
[hashtagAccountId, hrefToMention],
);
return { onElement };
};

View File

@@ -22,13 +22,13 @@ import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import { Dropdown } from 'flavours/glitch/components/dropdown_menu';
import { me } from '../../initial_state';
import { me, quickBoosting } from '../../initial_state';
import { IconButton } from '../icon_button';
import { RelativeTimestamp } from '../relative_timestamp';
import { isFeatureEnabled } from '../../utils/environment';
import { BoostButton } from '../status/boost_button';
import { RemoveQuoteHint } from './remove_quote_hint';
import { quoteItemState, selectStatusState } from '../status/boost_button_utils';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -69,6 +69,7 @@ const mapStateToProps = (state, { status }) => {
const quotedStatusId = status.getIn(['quote', 'quoted_status']);
return ({
quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null,
statusQuoteState: selectStatusState(state, status),
});
};
@@ -76,6 +77,7 @@ class StatusActionBar extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired,
statusQuoteState: PropTypes.object,
quotedAccountId: PropTypes.string,
contextType: PropTypes.string,
onReply: PropTypes.func,
@@ -123,6 +125,10 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
handleQuoteClick = () => {
this.props.onQuote(this.props.status);
};
handleShareClick = () => {
navigator.share({
url: this.props.status.get('url'),
@@ -215,7 +221,7 @@ class StatusActionBar extends ImmutablePureComponent {
};
render () {
const { status, quotedAccountId, contextType, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
const { status, statusQuoteState, quotedAccountId, contextType, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
const { signedIn, permissions } = this.props.identity;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@@ -244,6 +250,19 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
if (quickBoosting && signedIn) {
const quoteItem = quoteItemState(statusQuoteState);
menu.push(null);
menu.push({
text: intl.formatMessage(quoteItem.title),
description: quoteItem.meta
? intl.formatMessage(quoteItem.meta)
: undefined,
disabled: quoteItem.disabled,
action: this.handleQuoteClick,
});
}
if (signedIn) {
menu.push(null);
@@ -254,7 +273,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (writtenByMe || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
if (writtenByMe && isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
if (writtenByMe && !['private', 'direct'].includes(status.get('visibility'))) {
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
}
menu.push(null);
@@ -355,6 +374,7 @@ class StatusActionBar extends ImmutablePureComponent {
<Dropdown
scrollKey={scrollKey}
status={status}
needsStatusRefresh={quickBoosting && status.get('quote_approval') === null}
items={menu}
icon='ellipsis-h'
size={18}

View File

@@ -0,0 +1,3 @@
.inlineIcon {
vertical-align: middle;
}

View File

@@ -12,6 +12,8 @@ import { Button } from '../button';
import { useDismissableBannerState } from '../dismissable_banner';
import { Icon } from '../icon';
import classes from './remove_quote_hint.module.css';
const DISMISSABLE_BANNER_ID = 'notifications/remove_quote_hint';
/**
@@ -93,7 +95,7 @@ export const RemoveQuoteHint: React.FC<{
id: 'status.more',
defaultMessage: 'More',
})}
style={{ verticalAlign: 'middle' }}
className={classes.inlineIcon}
/>
),
}}

View File

@@ -3,6 +3,8 @@ import { useCallback, useRef, useId } from 'react';
import { FormattedMessage } from 'react-intl';
import { AnimateEmojiProvider } from './emoji/context';
export enum BannerVariant {
Warning = 'warning',
Filter = 'filter',
@@ -34,8 +36,7 @@ export const StatusBanner: React.FC<{
return (
// Element clicks are passed on to button
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
<AnimateEmojiProvider
className={
variant === BannerVariant.Warning
? 'content-warning'
@@ -48,6 +49,7 @@ export const StatusBanner: React.FC<{
<button
ref={buttonRef}
type='button'
className='link-button'
onClick={onClick}
aria-describedby={descriptionId}
@@ -69,6 +71,6 @@ export const StatusBanner: React.FC<{
/>
)}
</button>
</div>
</AnimateEmojiProvider>
);
};

View File

@@ -13,77 +13,19 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'
import { Icon } from 'flavours/glitch/components/icon';
import { Poll } from 'flavours/glitch/components/poll';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
import { EmojiHTML } from '../features/emoji/emoji_html';
import { isModernEmojiEnabled } from '../utils/environment';
import { languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { EmojiHTML } from './emoji/html';
import { HandledLink } from './status/handled_link';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
const textMatchesTarget = (text, origin, host) => {
return (text === origin || text === host
|| text.startsWith(origin + '/') || text.startsWith(host + '/')
|| 'www.' + text === host || ('www.' + text).startsWith(host + '/'));
};
const isLinkMisleading = (link) => {
let linkTextParts = [];
// Reconstruct visible text, as we do not have much control over how links
// from remote software look, and we can't rely on `innerText` because the
// `invisible` class does not set `display` to `none`.
const walk = (node) => {
switch (node.nodeType) {
case Node.TEXT_NODE:
linkTextParts.push(node.textContent);
break;
case Node.ELEMENT_NODE: {
if (node.classList.contains('invisible')) return;
const children = node.childNodes;
for (let i = 0; i < children.length; i++) {
walk(children[i]);
}
break;
}
}
};
walk(link);
const linkText = linkTextParts.join('');
const targetURL = new URL(link.href);
if (targetURL.protocol === 'magnet:') {
return !linkText.startsWith('magnet:');
}
if (targetURL.protocol === 'xmpp:') {
return !(linkText === targetURL.href || 'xmpp:' + linkText === targetURL.href);
}
// The following may not work with international domain names
if (textMatchesTarget(linkText, targetURL.origin, targetURL.host) || textMatchesTarget(linkText.toLowerCase(), targetURL.origin, targetURL.host)) {
return false;
}
// The link hasn't been recognized, maybe it features an international domain name
const hostname = decodeIDNA(targetURL.hostname).normalize('NFKC');
const host = targetURL.host.replace(targetURL.hostname, hostname);
const origin = targetURL.origin.replace(targetURL.host, host);
const text = linkText.normalize('NFKC');
return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host));
};
/**
*
* @param {any} status
* @returns {string}
*/
export function getStatusContent(status) {
if (isModernEmojiEnabled()) {
return status.getIn(['translation', 'content']) || status.get('content');
}
return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
}
@@ -128,6 +70,17 @@ const mapStateToProps = state => ({
languages: state.getIn(['server', 'translationLanguages', 'items']),
});
const compareUrls = (href1, href2) => {
try {
const url1 = new URL(href1);
const url2 = new URL(href2);
return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search;
} catch {
return false;
}
};
class StatusContent extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
@@ -137,8 +90,6 @@ class StatusContent extends PureComponent {
onClick: PropTypes.func,
collapsible: PropTypes.bool,
onCollapsedToggle: PropTypes.func,
tagLinks: PropTypes.bool,
rewriteMentions: PropTypes.string,
languages: ImmutablePropTypes.map,
intl: PropTypes.object,
// from react-router
@@ -147,83 +98,14 @@ class StatusContent extends PureComponent {
history: PropTypes.object.isRequired
};
static defaultProps = {
tagLinks: true,
rewriteMentions: 'no',
};
_updateStatusLinks () {
const node = this.node;
const { tagLinks, rewriteMentions } = this.props;
if (!node) {
return;
}
const { status, onCollapsedToggle } = this.props;
const links = node.querySelectorAll('a');
let link, mention;
for (var i = 0; i < links.length; ++i) {
link = links[i];
if (link.classList.contains('status-link')) {
continue;
}
link.classList.add('status-link');
mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
link.setAttribute('title', `@${mention.get('acct')}`);
link.setAttribute('data-hover-card-account', mention.get('id'));
if (rewriteMentions !== 'no') {
while (link.firstChild) link.removeChild(link.firstChild);
link.appendChild(document.createTextNode('@'));
const acctSpan = document.createElement('span');
acctSpan.textContent = rewriteMentions === 'acct' ? mention.get('acct') : mention.get('username');
link.appendChild(acctSpan);
}
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
link.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id']));
} else {
link.setAttribute('title', link.href);
link.classList.add('unhandled-link');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener nofollow');
try {
if (tagLinks && isLinkMisleading(link)) {
// Add a tag besides the link to display its origin
const url = new URL(link.href);
const tag = document.createElement('span');
tag.classList.add('link-origin-tag');
switch (url.protocol) {
case 'xmpp:':
tag.textContent = `[${url.href}]`;
break;
case 'magnet:':
tag.textContent = '(magnet)';
break;
default:
tag.textContent = `[${url.host}]`;
}
link.insertAdjacentText('beforeend', ' ');
link.insertAdjacentElement('beforeend', tag);
}
} catch (e) {
// The URL is invalid, remove the href just to be safe
if (tagLinks && e instanceof TypeError) link.removeAttribute('href');
}
}
}
if (status.get('collapsed', null) === null && onCollapsedToggle) {
const { collapsible, onClick } = this.props;
@@ -237,32 +119,6 @@ class StatusContent extends PureComponent {
}
}
handleMouseEnter = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
};
componentDidMount () {
this._updateStatusLinks();
}
@@ -271,22 +127,6 @@ class StatusContent extends PureComponent {
this._updateStatusLinks();
}
onMentionClick = (mention, e) => {
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.history.push(`/@${mention.get('acct')}`);
}
};
onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, '');
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.history.push(`/tags/${hashtag}`);
}
};
handleMouseDown = (e) => {
this.startXY = [e.clientX, e.clientY];
};
@@ -322,6 +162,27 @@ class StatusContent extends PureComponent {
this.node = c;
};
handleElement = (element, { key, ...props }, children) => {
if (element instanceof HTMLAnchorElement) {
const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url')));
return (
<HandledLink
{...props}
href={element.href}
text={element.innerText}
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
mention={mention?.toJSON()}
key={key}
>
{children}
</HandledLink>
);
} else if (element.classList.contains('quote-inline')) {
return null;
}
return undefined;
}
render () {
const { status, intl, statusContent } = this.props;
@@ -354,12 +215,19 @@ class StatusContent extends PureComponent {
if (this.props.onClick) {
return (
<>
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div
className={classNames}
ref={this.setRef}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
key='status-content'
>
<EmojiHTML
className='status__content__text status__content__text--visible translate'
lang={language}
htmlString={content}
extraEmojis={status.get('emojis')}
onElement={this.handleElement}
/>
{poll}
@@ -371,12 +239,13 @@ class StatusContent extends PureComponent {
);
} else {
return (
<div className={classNames} ref={this.setRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className={classNames} ref={this.setRef}>
<EmojiHTML
className='status__content__text status__content__text--visible translate'
lang={language}
htmlString={content}
extraEmojis={status.get('emojis')}
onElement={this.handleElement}
/>
{poll}

Some files were not shown because too many files have changed in this diff Show More