Compare commits

..

215 Commits

Author SHA1 Message Date
Claire
a602cc9126 Merge pull request #3378 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to e8045de79b into stable-4.5
2026-02-03 16:29:14 +01:00
Claire
4d4611beba Merge commit 'e8045de79bf0b445f43a7b7b7e5b38919edd93f6' into glitch-soc/merge-4.5 2026-02-03 15:33:46 +01:00
Claire
e8045de79b Bump version to v4.5.6 (#37715) 2026-02-03 15:26:52 +01:00
Claire
5f30206c5e Merge commit from fork 2026-02-03 14:59:53 +01:00
Claire
6fd034cb77 Merge pull request #3373 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 68a26ce7c6 into stable-4.5
2026-01-30 19:22:40 +01:00
PGray
527bed86b5 [Glitch] Fix quote cancel button not appearing after edit then delete-and-redraft
Port f1c00feb5c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2026-01-30 18:19:59 +01:00
Claire
a1e0fbfb67 Merge commit '68a26ce7c650ac2140c295b52f78b3d08191d6bd' into glitch-soc/merge-4.5 2026-01-30 18:19:44 +01:00
Claire
68a26ce7c6 Fix connection recycling pushing symbols to connection pool (#37674) 2026-01-30 12:18:36 +01:00
Claire
ff20ce9acf Clear affected relationship cache on Move activities (#37664) 2026-01-30 12:18:36 +01:00
PGray
1ba2b1cdc1 Fix quote cancel button not appearing after edit then delete-and-redraft (#37066) 2026-01-29 14:55:25 +01:00
Claire
4c1fbe4e2e Fix followers with profile subscription (bell icon) being notified of post edits (#37646) 2026-01-29 14:55:25 +01:00
Claire
569ff6c8ad Fix error when encountering invalid tag in updated object (#37635) 2026-01-29 14:55:25 +01:00
Claire
81716f7e27 Fix quote cache invalidation (#37592) 2026-01-29 14:55:25 +01:00
Claire
8935137526 Shorten caching of quote posts pending approval (#37570) 2026-01-29 14:55:25 +01:00
Claire
dcc5c2b6f6 Fix cross-server conversation tracking (#37559) 2026-01-29 14:55:25 +01:00
Shlee
f1c32f6a11 Unclosed connection leak when replacing pooled connection in SharedTimedStack.try_create (#37335) 2026-01-29 14:55:25 +01:00
Claire
23f04c2623 Merge pull request #3357 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to db943c43c8 into stable-4.5
2026-01-20 16:25:19 +01:00
Claire
ada1d32394 Merge commit 'db943c43c8fe834a0db6e87c020783ecf42476a9' into glitch-soc/merge-4.5 2026-01-20 15:56:06 +01:00
Claire
db943c43c8 Bump version to v4.5.5 (#37546) 2026-01-20 15:53:37 +01:00
Claire
1a74b74a40 Merge commit from fork
* Add limit on inbox payload size

The 1MB limit is consistent with the limit we use when fetching remote resources

* Add limit to number of options from federated polls

* Add a limit to the number of federated profile fields

* Add limit on federated username length

* Add hard limits for federated display name and account bio

* Add hard limits for `alsoKnownAs` and `attributionDomains`

* Add hard limit on federated custom emoji shortcode

* Highlight most destructive limits and expand on their reasoning
2026-01-20 15:14:45 +01:00
Claire
9a25b12f0c Merge commit from fork 2026-01-20 15:13:42 +01:00
Claire
6f9b32b137 Merge commit from fork 2026-01-20 15:13:10 +01:00
Claire
1b3ef035b9 Merge commit from fork 2026-01-20 15:10:38 +01:00
Claire
6698901d57 Fix potential duplicate handling of quote accept/reject/delete (#37537) 2026-01-20 08:57:46 +01:00
Claire
ba0609bbaf Skip tombstone creation on deleting from 404 (#37533) 2026-01-20 08:57:46 +01:00
Claire
d545e55b86 Merge pull request #3353 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to ded7f50f2c into stable-4.5
2026-01-19 19:30:21 +01:00
Echo
25d572e9b9 [Glitch] Remove trailing variation selector code for legacy emojis
Port f354bbe8aa to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2026-01-19 18:33:09 +01:00
diondiondion
3479b453e5 [Glitch] Fix mobile admin sidebar displaying under batch table toolbar
Port 53437c4653 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2026-01-19 18:32:48 +01:00
Claire
c96eebde37 Merge commit 'ded7f50f2c2672879292ff571b2e531ea87f77e4' into glitch-soc/merge-4.5
Conflicts:
- `app/controllers/api/v1/statuses_controller.rb`:
  Upstream refactored a bit where we had an extra argument for markdown/HTML.
  Adapted upstream change.
2026-01-19 18:29:02 +01:00
Claire
723b2601b8 Fix custom emojis not being rendered in status prepend (#3342) 2026-01-19 18:27:03 +01:00
Essem
66c06a0655 Merge pull request #3340 from TheEssem/fix/quote-cw-fallback-md
Fix quotes with only CWs not having fallback link when posting with other content types
2026-01-19 18:27:03 +01:00
Claire
ded7f50f2c Fix FeedManager#filter_from_home error when handling a reblog of a deleted status (#37486) 2026-01-19 11:37:34 +01:00
Claire
85eda5b46f Simplify status batch removal SQL query (#37469) 2026-01-19 11:37:34 +01:00
Matt Jankowski
f1c9c89c39 Add spec for quote policy update change (#37474) 2026-01-19 11:37:34 +01:00
Shlee
57e0c6562f Fix quote_approval_policy being reset to user defaults when omitted in status update (#37436) 2026-01-19 11:37:34 +01:00
Joshua Rogers
f7b6e57151 Fix Vary parsing in cache control enforcement (#37426) 2026-01-19 11:37:34 +01:00
Joshua Rogers
57f658dc5c Fix arg order for non_matching_uri_hosts? call in QuoteRequest (#37425) 2026-01-19 11:37:34 +01:00
Joshua Rogers
0cda068918 Fix thread-unsafe ActivityPub activity dispatch (#37423) 2026-01-19 11:37:34 +01:00
David Roetzel
deeaf50472 Fix URI generation for reblogs by accounts with numerical AP ids (#37415) 2026-01-19 11:37:34 +01:00
Shlee
adea0b7b31 Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2026-01-19 11:37:34 +01:00
Shlee
1eb8d1b967 SharedConnectionPool - NoMethodError: undefined method 'site' for Integer (#37374) 2026-01-19 11:37:34 +01:00
Echo
f354bbe8aa Remove trailing variation selector code for legacy emojis (#37320) 2026-01-19 11:37:34 +01:00
diondiondion
53437c4653 Fix mobile admin sidebar displaying under batch table toolbar (#37307) 2026-01-19 11:37:34 +01:00
Claire
617926742c Update SECURITY.md (#37505) 2026-01-15 14:17:38 +01:00
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
949 changed files with 17113 additions and 17893 deletions

View File

@@ -73,7 +73,7 @@ services:
hard: -1
libretranslate:
image: libretranslate/libretranslate:v1.7.3
image: libretranslate/libretranslate:v1.6.2
restart: unless-stopped
volumes:
- lt-data:/home/libretranslate/.local

View File

@@ -9,7 +9,7 @@ runs:
using: 'composite'
steps:
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'

View File

@@ -5,6 +5,7 @@
'customManagers:dockerfileVersions',
':labels(dependencies)',
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
':enableVulnerabilityAlertsWithLabel(security)',
],
rebaseWhen: 'conflicted',
@@ -22,6 +23,8 @@
// Require Dependency Dashboard Approval for major version bumps of these node packages
matchManagers: ['npm'],
matchPackageNames: [
'tesseract.js', // Requires code changes
// react-router: Requires manual upgrade
'history',
'react-router-dom',

View File

@@ -35,7 +35,7 @@ jobs:
- linux/arm64
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Prepare
env:
@@ -100,7 +100,7 @@ jobs:
- name: Upload digest
if: ${{ inputs.push_to_images != '' }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
# `hashFiles` is used to disambiguate between streaming and non-streaming images
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
@@ -119,10 +119,10 @@ jobs:
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Download digests
uses: actions/download-artifact@v6
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
# `hashFiles` is used to disambiguate between streaming and non-streaming images

View File

@@ -18,7 +18,7 @@ jobs:
steps:
# Repository needs to be cloned so `git rev-parse` below works
- name: Clone repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- id: version_vars
run: |
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT

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.5.') }}
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.5.') }}
latest=${{ needs.check-latest-stable.outputs.latest }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}

View File

@@ -28,7 +28,7 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby

View File

@@ -1,51 +1,31 @@
name: 'Chromatic'
permissions:
contents: read
on:
push:
branches-ignore:
- renovate/*
- stable-*
paths:
- 'package.json'
- 'yarn.lock'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.css'
- '**/*.scss'
- '.github/workflows/chromatic.yml'
jobs:
pathcheck:
name: Check for relevant changes
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.filter.outputs.src }}
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
src:
- 'package.json'
- 'yarn.lock'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.css'
- '**/*.scss'
- '.github/workflows/chromatic.yml'
chromatic:
name: Run Chromatic
runs-on: ubuntu-latest
needs: pathcheck
if: github.repository == 'mastodon/mastodon' && needs.pathcheck.outputs.changed == 'true'
if: github.repository == 'mastodon/mastodon'
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
@@ -53,10 +33,9 @@ jobs:
run: yarn build-storybook
- name: Run Chromatic
uses: chromaui/action@v13
uses: chromaui/action@v12
with:
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
zip: true
storybookBuildDir: 'storybook-static'
exitZeroOnChanges: false # Fail workflow if changes are found
autoAcceptChanges: 'main' # Auto-accept changes on main branch only

View File

@@ -31,11 +31,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -48,7 +48,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -61,6 +61,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
with:
category: '/language:${{matrix.language}}'

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Increase Git http.postBuffer
# This is needed due to a bug in Ubuntu's cURL version?

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Increase Git http.postBuffer
# This is needed due to a bug in Ubuntu's cURL version?

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: crowdin action
uses: crowdin/github-action@v2

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript

View File

@@ -34,7 +34,7 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript

View File

@@ -33,7 +33,7 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript

View File

@@ -35,7 +35,7 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1

View File

@@ -34,7 +34,7 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript

View File

@@ -72,7 +72,7 @@ jobs:
BUNDLE_RETRY: 3
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby

View File

@@ -32,7 +32,7 @@ jobs:
SECRET_KEY_BASE_DUMMY: 1
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
@@ -65,7 +65,7 @@ jobs:
run: |
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v4
if: matrix.mode == 'test'
with:
path: |-
@@ -128,9 +128,9 @@ jobs:
- '3.3'
- '.ruby-version'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v4
with:
path: './'
name: ${{ github.sha }}
@@ -230,9 +230,9 @@ jobs:
- '3.3'
- '.ruby-version'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v4
with:
path: './'
name: ${{ github.sha }}
@@ -309,9 +309,9 @@ jobs:
- '.ruby-version'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v4
with:
path: './'
name: ${{ github.sha }}
@@ -350,14 +350,14 @@ jobs:
- run: bin/rspec spec/system --tag streaming --tag js
- name: Archive logs
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-logs-${{ matrix.ruby-version }}
path: log/
- name: Archive test screenshots
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-screenshots-${{ matrix.ruby-version }}
@@ -447,9 +447,9 @@ jobs:
search-image: opensearchproject/opensearch:2
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v4
with:
path: './'
name: ${{ github.sha }}
@@ -469,14 +469,14 @@ jobs:
- run: bin/rspec --tag search
- name: Archive logs
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-search-logs-${{ matrix.ruby-version }}
path: log/
- name: Archive test screenshots
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-search-screenshots

2
.nvmrc
View File

@@ -1 +1 @@
24.11
24.10

View File

@@ -95,7 +95,6 @@ AUTHORS.md
# Ignore glitch-soc vendored CSS reset
app/javascript/flavours/glitch/styles/reset.scss
app/javascript/flavours/glitch/styles_new/mastodon/reset.scss
# Ignore win95 theme
app/javascript/styles/win95.scss

View File

@@ -31,7 +31,7 @@ const config: StorybookConfig = {
viteFinal(config) {
// For an unknown reason, Storybook does not use the root
// from the Vite config so we need to set it manually.
config.root = resolve(import.meta.dirname, '../app/javascript');
config.root = resolve(__dirname, '../app/javascript');
return config;
},
};

View File

@@ -1,2 +1,2 @@
<html class="no-reduce-motion theme-light">
<html class="no-reduce-motion">
</html>

View File

@@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.12.1'
const PACKAGE_VERSION = '2.11.3'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
@@ -205,7 +205,6 @@ async function resolveMainClient(event) {
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {

View File

@@ -0,0 +1,13 @@
diff --git a/lib/index.js b/lib/index.js
index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -99,7 +99,7 @@ function lodash(_ref) {
var node = _ref3;
- if ((0, _types.isModuleDeclaration)(node)) {
+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) {
isModule = true;
break;
}

View File

@@ -538,7 +538,7 @@ and provided thanks to the work of the following contributors:
* [Drew Schuster](mailto:dtschust@gmail.com)
* [Dryusdan](mailto:dryusdan@dryusdan.fr)
* [Eai](mailto:eai@mizle.net)
* [Eashwar Ranganathan](mailto:eashwar@eashwar.com)
* [Eashwar Ranganathan](mailto:eranganathan@lyft.com)
* [Ed Knutson](mailto:knutsoned@gmail.com)
* [Elizabeth Martín Campos](mailto:me@elizabeth.sh)
* [Elizabeth Myers](mailto:elizabeth@interlinked.me)

View File

@@ -2,6 +2,77 @@
All notable changes to this project will be documented in this file.
## [4.5.6] - 2026-02-03
### Security
- Fix ActivityPub collection caching logic for pinned posts and featured tags not checking blocked accounts ([GHSA-ccpr-m53r-mfwr](https://github.com/mastodon/mastodon/security/advisories/GHSA-ccpr-m53r-mfwr))
### Changed
- Shorten caching of quote posts pending approval (#37570 and #37592 by @ClearlyClaire)
### Fixed
- Fix relationship cache not being cleared when handling account migrations (#37664 by @ClearlyClaire)
- Fix quote cancel button not appearing after edit then delete-and-redraft (#37066 by @PGrayCS)
- Fix followers with profile subscription (bell icon) being notified of post edits (#37646 by @ClearlyClaire)
- Fix error when encountering invalid tag in updated object (#37635 by @ClearlyClaire)
- Fix cross-server conversation tracking (#37559 by @ClearlyClaire)
- Fix recycled connections not being immediately closed (#37335 and #37674 by @ClearlyClaire and @shleeable)
## [4.5.5] - 2026-01-20
### Security
- Fix missing limits on various federated properties [GHSA-gg8q-rcg7-p79g](https://github.com/mastodon/mastodon/security/advisories/GHSA-gg8q-rcg7-p79g)
- Fix remote user suspension bypass [GHSA-5h2f-wg8j-xqwp](https://github.com/mastodon/mastodon/security/advisories/GHSA-5h2f-wg8j-xqwp)
- Fix missing length limits on some user-provided fields [GHSA-6x3w-9g92-gvf3](https://github.com/mastodon/mastodon/security/advisories/GHSA-6x3w-9g92-gvf3)
- Fix missing access check for push notification settings update [GHSA-f3q8-7vw3-69v4](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q8-7vw3-69v4)
### Changed
- Skip tombstone creation on deleting from 404 (#37533 by @ClearlyClaire)
### Fixed
- Fix potential duplicate handling of quote accept/reject/delete (#37537 by @ClearlyClaire)
- Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486 by @ClearlyClaire)
- Fix needlessly complicated SQL query in status batch removal (#37469 by @ClearlyClaire)
- Fix `quote_approval_policy` being reset to user defaults when omitted in status update (#37436 and #37474 by @mjankowski and @shleeable)
- Fix `Vary` parsing in cache control enforcement (#37426 by @MegaManSec)
- Fix missing URI scheme test in `QuoteRequest` handling (#37425 by @MegaManSec)
- Fix thread-unsafe ActivityPub activity dispatch (#37423 by @MegaManSec)
- Fix URI generation for reblogs by accounts with numerical ActivityPub identifiers (#37415 by @oneiros)
- Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375 by @shleeable)
- Fix emoji with variant selector not being rendered properly (#37320 by @ChaosExAnima)
- Fix mobile admin sidebar displaying under batch table toolbar (#37307 by @diondiondion)
## [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

View File

@@ -48,3 +48,22 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques
### Additional documentation
- [Mastodon documentation](https://docs.joinmastodon.org/)
## Size limits
Mastodon imposes a few hard limits on federated content.
These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons.
The following table attempts to summary those limits.
| Limited property | Size limit | Consequence of exceeding the limit |
| ------------------------------------------------------------- | ---------- | ---------------------------------- |
| Serialized JSON-LD | 1MB | **Activity is rejected/dropped** |
| Profile fields (actor `PropertyValue` attachments) name/value | 2047 | Field name/value is truncated |
| Number of profile fields (actor `PropertyValue` attachments) | 50 | Fields list is truncated |
| Poll options (number of `anyOf`/`oneOf` in a `Question`) | 500 | Items list is truncated |
| Account username (actor `preferredUsername`) length | 2048 | **Actor will be rejected** |
| Account display name (actor `name`) length | 2048 | Display name will be truncated |
| Account note (actor `summary`) length | 20kB | Account note will be truncated |
| Account `attributionDomains` | 256 | List will be truncated |
| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated |
| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected |

12
Gemfile
View File

@@ -13,7 +13,7 @@ gem 'haml-rails', '~>3.0'
gem 'pg', '~> 1.5'
gem 'pghero'
gem 'aws-sdk-core', require: false
gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873
gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'blurhash', '~> 0.1'
gem 'fog-core', '<= 2.6.0'
@@ -24,7 +24,7 @@ gem 'ruby-vips', '~> 2.2', require: false
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.19.0', require: false
gem 'bootsnap', '~> 1.18.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3'
@@ -40,7 +40,7 @@ gem 'net-ldap', '~> 0.18'
gem 'omniauth', '~> 2.0'
gem 'omniauth-cas', '~> 3.0.0.beta.1'
gem 'omniauth_openid_connect', '~> 0.8.0'
gem 'omniauth-rails_csrf_protection', '~> 2.0'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-saml', '~> 2.0'
gem 'color_diff', '~> 0.1'
@@ -71,7 +71,7 @@ gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14'
gem 'parslet'
gem 'premailer-rails'
gem 'public_suffix', '~> 7.0'
gem 'public_suffix', '~> 6.0'
gem 'pundit', '~> 2.3'
gem 'rack-attack', '~> 6.6'
gem 'rack-cors', require: 'rack/cors'
@@ -114,7 +114,7 @@ group :opentelemetry do
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.34.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
@@ -138,7 +138,7 @@ group :test do
# Browser integration testing
gem 'capybara', '~> 3.39'
gem 'capybara-playwright-driver'
gem 'playwright-ruby-client', '1.56.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
gem 'playwright-ruby-client', '1.55.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
# Used to reset the database between system tests
gem 'database_cleaner-active_record'

View File

@@ -86,8 +86,8 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
android_key_attestation (0.3.0)
annotaterb (4.20.0)
@@ -96,20 +96,17 @@ GEM
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1190.0)
aws-sdk-core (3.239.2)
aws-partitions (1.1168.0)
aws-sdk-core (3.215.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.206.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-s3 (1.177.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
@@ -129,14 +126,14 @@ GEM
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.8)
bootsnap (1.19.0)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.1.1)
racc
browser (6.2.0)
builder (3.3.0)
bundler-audit (0.9.3)
bundler (>= 1.2.0)
bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
capybara (3.40.0)
addressable
@@ -167,7 +164,7 @@ GEM
cocoon (1.2.15)
color_diff (0.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.5)
connection_pool (2.5.4)
cose (1.3.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@@ -182,7 +179,7 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0)
database_cleaner-core (2.0.1)
date (3.5.0)
date (3.4.1)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
@@ -234,7 +231,7 @@ GEM
excon (1.3.0)
logger
fabrication (3.0.0)
faker (3.5.3)
faker (3.5.2)
i18n (>= 1.8.11, < 2)
faraday (2.14.0)
faraday-net_http (>= 2.0, < 3.5)
@@ -282,7 +279,7 @@ GEM
rake (>= 13)
googleapis-common-protos-types (1.22.0)
google-protobuf (~> 4.26)
haml (6.4.0)
haml (6.3.0)
temple (>= 0.8.2)
thor
tilt
@@ -291,7 +288,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.67.0)
haml_lint (0.66.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@@ -304,8 +301,8 @@ GEM
highline (3.1.2)
reline
hiredis (0.6.3)
hiredis-client (0.26.2)
redis-client (= 0.26.2)
hiredis-client (0.26.1)
redis-client (= 0.26.1)
hkdf (0.3.0)
htmlentities (4.3.4)
http (5.3.1)
@@ -324,14 +321,13 @@ GEM
rainbow (>= 2.0.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
i18n-tasks (1.1.2)
i18n-tasks (1.0.15)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
highline (>= 3.0.0)
highline (>= 2.0.0)
i18n
parser (>= 3.2.2.1)
prism
rails-i18n
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
@@ -350,7 +346,7 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
json (2.16.0)
json (2.15.1)
json-canonicalization (1.0.0)
json-jwt (1.17.0)
activesupport (>= 4.2)
@@ -447,7 +443,7 @@ GEM
mime-types-data (3.2025.0924)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.26.2)
minitest (5.26.0)
msgpack (1.8.0)
multi_json (1.17.0)
mutex_m (0.3.0)
@@ -469,7 +465,7 @@ GEM
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.13)
oj (3.16.11)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.4)
@@ -481,7 +477,7 @@ GEM
addressable (~> 2.8)
nokogiri (~> 1.12)
omniauth (~> 2.1)
omniauth-rails_csrf_protection (2.0.0)
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth-saml (2.2.4)
@@ -516,9 +512,9 @@ GEM
opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.10)
opentelemetry-semantic_conventions
opentelemetry-helpers-sql (0.3.0)
opentelemetry-helpers-sql (0.2.0)
opentelemetry-api (~> 1.7)
opentelemetry-helpers-sql-processor (0.3.1)
opentelemetry-helpers-sql-obfuscation (0.4.0)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.6.1)
opentelemetry-instrumentation-active_support (~> 0.10)
@@ -542,19 +538,19 @@ GEM
opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.24.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-excon (0.26.1)
opentelemetry-instrumentation-excon (0.26.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-faraday (0.30.1)
opentelemetry-instrumentation-faraday (0.30.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http (0.27.1)
opentelemetry-instrumentation-http (0.27.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http_client (0.26.1)
opentelemetry-instrumentation-http_client (0.26.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-net_http (0.26.1)
opentelemetry-instrumentation-net_http (0.26.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-pg (0.34.1)
opentelemetry-instrumentation-pg (0.32.0)
opentelemetry-helpers-sql
opentelemetry-helpers-sql-processor
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rack (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
@@ -569,7 +565,7 @@ GEM
opentelemetry-instrumentation-concurrent_ruby (~> 0.23)
opentelemetry-instrumentation-redis (0.28.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-sidekiq (0.28.1)
opentelemetry-instrumentation-sidekiq (0.28.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-registry (0.4.0)
opentelemetry-api (~> 1.1)
@@ -585,7 +581,7 @@ GEM
ox (2.14.23)
bigdecimal (>= 3.0)
parallel (1.27.0)
parser (3.3.10.0)
parser (3.3.9.0)
ast (~> 2.4.1)
racc
parslet (2.0.0)
@@ -594,7 +590,7 @@ GEM
pg (1.6.2)
pghero (3.7.0)
activerecord (>= 7.1)
playwright-ruby-client (1.56.0)
playwright-ruby-client (1.55.0)
concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0)
pp (0.6.3)
@@ -608,8 +604,8 @@ GEM
net-smtp
premailer (~> 1.7, >= 1.7.9)
prettyprint (0.2.0)
prism (1.6.0)
prometheus_exporter (2.3.1)
prism (1.5.2)
prometheus_exporter (2.3.0)
webrick
propshaft (1.3.1)
actionpack (>= 7.0.0)
@@ -618,7 +614,7 @@ GEM
psych (5.2.6)
date
stringio
public_suffix (7.0.0)
public_suffix (6.0.2)
puma (7.1.0)
nio4r (~> 2.0)
pundit (2.5.2)
@@ -638,7 +634,7 @@ GEM
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (4.2.1)
rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
@@ -672,7 +668,7 @@ GEM
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.1.0)
rails-i18n (8.0.2)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.3)
@@ -685,7 +681,7 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rake (13.3.0)
rdf (3.3.4)
bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5)
@@ -703,10 +699,10 @@ GEM
reline
redcarpet (3.6.1)
redis (4.8.1)
redis-client (0.26.2)
redis-client (0.26.1)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
reline (0.6.2)
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
@@ -717,10 +713,10 @@ GEM
rotp (6.3.0)
rouge (4.6.1)
rpam2 (4.0.2)
rqrcode (3.1.1)
rqrcode (3.1.0)
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.1)
rqrcode_core (2.0.0)
rspec (3.13.1)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
@@ -749,7 +745,7 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
rspec-support (3.13.6)
rubocop (1.81.7)
rubocop (1.81.6)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -760,7 +756,7 @@ GEM
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.48.0)
rubocop-ast (1.47.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@@ -779,10 +775,10 @@ GEM
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.8.0)
rubocop-rspec (3.7.0)
lint_roller (~> 1.1)
rubocop (~> 1.81)
rubocop-rspec_rails (2.32.0)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
@@ -807,9 +803,9 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.4.1)
shoulda-matchers (7.0.1)
activesupport (>= 7.1)
sidekiq (8.0.10)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
sidekiq (8.0.9)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
@@ -839,9 +835,9 @@ GEM
stackprof (0.2.27)
starry (0.2.0)
base64
stoplight (5.6.0)
stoplight (5.4.0)
zeitwerk
stringio (3.1.8)
stringio (3.1.7)
strong_migrations (2.5.1)
activerecord (>= 7.1)
swd (2.0.3)
@@ -855,7 +851,7 @@ GEM
unicode-display_width (>= 1.1.1, < 4)
terrapin (1.1.1)
climate_control
test-prof (1.5.0)
test-prof (1.4.4)
thor (1.4.0)
tilt (2.6.1)
timeout (0.4.3)
@@ -887,7 +883,7 @@ GEM
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uri (1.1.1)
uri (1.0.4)
useragent (0.16.11)
validate_url (1.0.15)
activemodel (>= 3.0.0)
@@ -915,7 +911,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.26.1)
webmock (3.26.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -937,12 +933,12 @@ DEPENDENCIES
active_model_serializers (~> 0.10)
addressable (~> 2.8)
annotaterb (~> 4.13)
aws-sdk-core
aws-sdk-core (< 3.216.0)
aws-sdk-s3 (~> 1.123)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.19.0)
bootsnap (~> 1.18.0)
brakeman (~> 7.0)
browser
bundler-audit (~> 0.9)
@@ -1009,7 +1005,7 @@ DEPENDENCIES
oj (~> 3.14)
omniauth (~> 2.0)
omniauth-cas (~> 3.0.0.beta.1)
omniauth-rails_csrf_protection (~> 2.0)
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.8.0)
opentelemetry-api (~> 1.7.0)
@@ -1022,7 +1018,7 @@ DEPENDENCIES
opentelemetry-instrumentation-http (~> 0.27.0)
opentelemetry-instrumentation-http_client (~> 0.26.0)
opentelemetry-instrumentation-net_http (~> 0.26.0)
opentelemetry-instrumentation-pg (~> 0.34.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)
@@ -1032,11 +1028,11 @@ DEPENDENCIES
parslet
pg (~> 1.5)
pghero
playwright-ruby-client (= 1.56.0)
playwright-ruby-client (= 1.55.0)
premailer-rails
prometheus_exporter (~> 2.2)
propshaft
public_suffix (~> 7.0)
public_suffix (~> 6.0)
puma (~> 7.0)
pundit (~> 2.3)
rack-attack (~> 6.6)

View File

@@ -18,5 +18,4 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| 4.5.x | Yes |
| 4.4.x | Yes |
| 4.3.x | Until 2026-05-06 |
| 4.2.x | Until 2026-01-08 |
| < 4.2 | No |
| < 4.3 | No |

View File

@@ -4,17 +4,31 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :check_authorization
before_action :set_items
before_action :set_size
before_action :set_type
def show
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
if @unauthorized
render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
else
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
end
private
def check_authorization
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
@unauthorized = authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
end
def set_items
case params[:id]
when 'featured'
@@ -57,11 +71,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
end
def for_signed_account
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
if @unauthorized
[]
else
yield

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

@@ -3,6 +3,7 @@
class ActivityPub::InboxesController < ActivityPub::BaseController
include JsonLdHelper
before_action :skip_large_payload
before_action :skip_unknown_actor_activity
before_action :require_actor_signature!
skip_before_action :authenticate_user!
@@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
private
def skip_large_payload
head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE
end
def skip_unknown_actor_activity
head 202 if unknown_affected_account?
end

View File

@@ -5,15 +5,6 @@ module Admin
def index
authorize :custom_emoji, :index?
# If filtering by local emojis, remove by_domain filter.
params.delete(:by_domain) if params[:local].present?
# If filtering by domain, ensure remote filter is set.
if params[:by_domain].present?
params.delete(:local)
params[:remote] = '1'
end
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
@form = Form::CustomEmojiBatch.new
end

View File

@@ -9,7 +9,7 @@ module Admin
@site_upload.destroy!
redirect_back_or_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
end
private

View File

@@ -1,12 +1,10 @@
# frozen_string_literal: true
class Api::V1::AnnualReportsController < Api::BaseController
include AsyncRefreshesConcern
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:read, :generate]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:read, :generate]
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!
before_action :set_annual_report, only: [:show, :read]
before_action :set_annual_report, except: :index
def index
with_read_replica do
@@ -30,59 +28,14 @@ class Api::V1::AnnualReportsController < Api::BaseController
relationships: @relationships
end
def state
render json: { state: report_state }
end
def generate
return render_empty unless year == AnnualReport.current_campaign
return render_empty if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year)
async_refresh = AsyncRefresh.new(refresh_key)
if async_refresh.running?
add_async_refresh_header(async_refresh, retry_seconds: 2)
return head 202
end
add_async_refresh_header(AsyncRefresh.create(refresh_key), retry_seconds: 2)
GenerateAnnualReportWorker.perform_async(current_account.id, year)
head 202
end
def read
@annual_report.view!
render_empty
end
def refresh_key
"wrapstodon:#{current_account.id}:#{year}"
end
private
def report_state
return 'available' if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year)
async_refresh = AsyncRefresh.new(refresh_key)
if async_refresh.running?
add_async_refresh_header(async_refresh, retry_seconds: 2)
'generating'
elsif AnnualReport.current_campaign == year && AnnualReport.new(current_account, year).eligible?
'eligible'
else
'ineligible'
end
end
def year
params[:id]&.to_i
end
def set_annual_report
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: year)
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id])
end
end

View File

@@ -107,9 +107,7 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.where(account: current_account).find(params[:id])
authorize @status, :update?
UpdateStatusService.new.call(
@status,
current_account.id,
update_options = {
text: status_params[:status],
media_ids: status_params[:media_ids],
media_attributes: status_params[:media_attributes],
@@ -117,9 +115,12 @@ class Api::V1::StatusesController < Api::BaseController
language: status_params[:language],
spoiler_text: status_params[:spoiler_text],
poll: status_params[:poll],
quote_approval_policy: quote_approval_policy,
content_type: status_params[:content_type]
)
content_type: status_params[:content_type],
}
update_options[:quote_approval_policy] = quote_approval_policy if status_params[:quote_approval_policy].present?
UpdateStatusService.new.call(@status, current_account.id, update_options)
render json: @status, serializer: REST::StatusSerializer
end
@@ -128,11 +129,10 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.where(account: current_account).find(params[:id])
authorize @status, :destroy?
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
@status.discard_with_reblogs
StatusPin.find_by(status: @status)&.destroy
@status.account.statuses_count = @status.account.statuses_count - 1
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })

View File

@@ -1,114 +0,0 @@
# frozen_string_literal: true
class Api::V1Alpha::CollectionsController < Api::BaseController
include Authorization
DEFAULT_COLLECTIONS_LIMIT = 40
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422
end
before_action :check_feature_enabled
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy]
before_action :require_user!, only: [:create, :update, :destroy]
before_action :set_account, only: [:index]
before_action :set_collections, only: [:index]
before_action :set_collection, only: [:show, :update, :destroy]
after_action :insert_pagination_headers, only: [:index]
after_action :verify_authorized
def index
cache_if_unauthenticated!
authorize Collection, :index?
render json: @collections, each_serializer: REST::BaseCollectionSerializer
end
def show
cache_if_unauthenticated!
authorize @collection, :show?
render json: @collection, serializer: REST::CollectionSerializer
end
def create
authorize Collection, :create?
@collection = CreateCollectionService.new.call(collection_creation_params, current_user.account)
render json: @collection, serializer: REST::CollectionSerializer
end
def update
authorize @collection, :update?
@collection.update!(collection_update_params) # TODO: Create a service for this to federate changes
render json: @collection, serializer: REST::CollectionSerializer
end
def destroy
authorize @collection, :destroy?
@collection.destroy
head 200
end
private
def set_account
@account = Account.find(params[:account_id])
end
def set_collections
@collections = @account.collections
.with_tag
.order(created_at: :desc)
.offset(offset_param)
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
end
def set_collection
@collection = Collection.find(params[:id])
end
def collection_creation_params
params.permit(:name, :description, :sensitive, :discoverable, :tag_name, account_ids: [])
end
def collection_update_params
params.permit(:name, :description, :sensitive, :discoverable, :tag_name)
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end
def next_path
return unless records_continue?
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def prev_path
return if offset_param.zero?
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def records_continue?
((offset_param * limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections.size) < @account.collections.size
end
def offset_param
params[:offset].to_i
end
end

View File

@@ -62,7 +62,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end
def set_push_subscription
@push_subscription = ::Web::PushSubscription.find(params[:id])
@push_subscription = ::Web::PushSubscription.where(user_id: active_session.user_id).find(params[:id])
end
def subscription_params

View File

@@ -135,7 +135,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
@accept_token = session[:accept_token] = SecureRandom.hex
@invite_code = invite_code
render :rules
set_locale { render :rules }
end
def is_flashing_format? # rubocop:disable Naming/PredicatePrefix

View File

@@ -19,7 +19,7 @@ module CacheConcern
# from being used as cache keys, while allowing to `Vary` on them (to not serve
# anonymous cached data to authenticated requests when authentication matters)
def enforce_cache_control!
vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase }
vary = response.headers['Vary'].to_s.split(',').map { |x| x.strip.downcase }.reject(&:empty?)
return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? }
response.cache_control.replace(private: true, no_store: true)

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

@@ -7,7 +7,6 @@ class FollowerAccountsController < ApplicationController
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :protect_hidden_collections, if: -> { request.format.json? }
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :limited_federation_mode?
@@ -19,6 +18,8 @@ class FollowerAccountsController < ApplicationController
end
format.json do
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: collection_presenter,
@@ -40,10 +41,6 @@ class FollowerAccountsController < ApplicationController
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
end
def protect_hidden_collections
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
end
def page_requested?
params[:page].present?
end

View File

@@ -7,7 +7,6 @@ class FollowingAccountsController < ApplicationController
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :protect_hidden_collections, if: -> { request.format.json? }
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :limited_federation_mode?
@@ -19,6 +18,11 @@ class FollowingAccountsController < ApplicationController
end
format.json do
if page_requested? && @account.hide_collections?
forbidden
next
end
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: collection_presenter,
@@ -40,10 +44,6 @@ class FollowingAccountsController < ApplicationController
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
end
def protect_hidden_collections
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
end
def page_requested?
params[:page].present?
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

@@ -29,7 +29,7 @@ class StatusesController < ApplicationController
end
format.json do
expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode?
expires_in @status.quote&.pending? ? 5.seconds : 3.minutes, public: true if @status.distributable? && public_fetch_mode?
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end
end

View File

@@ -1,23 +0,0 @@
# frozen_string_literal: true
class WrapstodonController < ApplicationController
include WebAppControllerConcern
include Authorization
include AccountOwnedConcern
vary_by 'Accept, Accept-Language, Cookie'
before_action :set_generated_annual_report
skip_before_action :require_functional!, only: :show, unless: :limited_federation_mode?
def show
expires_in 10.seconds, public: true if current_account.nil?
end
private
def set_generated_annual_report
@generated_annual_report = GeneratedAnnualReport.find_by!(account: @account, year: params[:year], share_key: params[:share_key])
end
end

View File

@@ -153,9 +153,11 @@ module ApplicationHelper
tag.meta(content: content, property: property)
end
def html_classes
def body_classes
output = []
output << content_for(:html_classes)
output << content_for(:body_classes)
output << "flavour-#{current_flavour.parameterize}"
output << "skin-#{current_skin.parameterize}"
output << 'system-font' if current_account&.user&.setting_system_font_ui
output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
@@ -163,12 +165,6 @@ module ApplicationHelper
output.compact_blank.join(' ')
end
def body_classes
output = []
output << content_for(:body_classes)
output.compact_blank.join(' ')
end
def cdn_host
Rails.configuration.action_controller.asset_host
end

View File

@@ -35,7 +35,7 @@ module LanguagesHelper
cy: ['Welsh', 'Cymraeg'].freeze,
da: ['Danish', 'dansk'].freeze,
de: ['German', 'Deutsch'].freeze,
dv: ['Divehi', 'ދިވެހި'].freeze,
dv: ['Divehi', 'Dhivehi'].freeze,
dz: ['Dzongkha', 'རྫོང་ཁ'].freeze,
ee: ['Ewe', 'Eʋegbe'].freeze,
el: ['Greek', 'Ελληνικά'].freeze,
@@ -100,7 +100,7 @@ module LanguagesHelper
lo: ['Lao', 'ລາວ'].freeze,
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
lv: ['Latvian', 'Latviski'].freeze,
lv: ['Latvian', 'latviešu valoda'].freeze,
mg: ['Malagasy', 'fiteny malagasy'].freeze,
mh: ['Marshallese', 'Kajin M̧ajeļ'].freeze,
mi: ['Māori', 'te reo Māori'].freeze,

View File

@@ -1,16 +0,0 @@
# frozen_string_literal: true
module WrapstodonHelper
def render_wrapstodon_share_data(report)
json = ActiveModelSerializers::SerializableResource.new(
AnnualReportsPresenter.new([report]),
serializer: REST::AnnualReportsSerializer,
scope: nil,
scope_name: :current_user
).to_json
# rubocop:disable Rails/OutputSafety
content_tag(:script, json_escape(json).html_safe, type: 'application/json', id: 'wrapstodon-data')
# rubocop:enable Rails/OutputSafety
end
end

View File

@@ -1,49 +1,31 @@
# frozen_string_literal: true
class DateOfBirthInput < SimpleForm::Inputs::Base
OPTIONS = {
day: { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' },
month: { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' },
year: { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' },
}.freeze
OPTIONS = [
{ autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze,
{ autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze,
{ autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze,
].freeze
def input(wrapper_options = nil)
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
merged_input_options[:inputmode] = 'numeric'
safe_join(
ordered_options.map do |option|
options = merged_input_options
.merge(OPTIONS[option])
.merge(
id: generate_id(option),
'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{param_for(option)}"),
value: values[option]
)
@builder.text_field("#{attribute_name}(#{param_for(option)})", options)
end
)
values = (object.public_send(attribute_name) || '').split('.')
safe_join(Array.new(3) do |index|
options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index]
@builder.text_field("#{attribute_name}(#{index + 1}i)", options)
end)
end
def label_target
"#{attribute_name}_#{param_for(ordered_options.first)}"
"#{attribute_name}_1i"
end
private
def ordered_options
I18n.t('date.order').map(&:to_sym)
end
def generate_id(option)
"#{object_name}_#{attribute_name}_#{param_for(option)}"
end
def param_for(option)
"#{ActionView::Helpers::DateTimeSelector::POSITION[option]}i"
end
def values
Date._parse((object.public_send(attribute_name) || '').to_s).transform_keys(mon: :month, mday: :day)
def generate_id(index)
"#{object_name}_#{attribute_name}_#{index + 1}i"
end
end

View File

@@ -1,7 +1,7 @@
import { createRoot } from 'react-dom/client';
import Rails from '@rails/ujs';
import { decode, ValidationError } from 'blurhash';
import { on } from 'delegated-events';
import ready from '../mastodon/ready';
@@ -24,9 +24,10 @@ const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
}
};
on(
'change',
Rails.delegate(
document,
'input[type="datetime-local"]#announcement_starts_at',
'change',
({ target }) => {
if (target instanceof HTMLInputElement)
setAnnouncementEndsAttributes(target);
@@ -62,7 +63,7 @@ const hideSelectAll = () => {
if (hiddenField) hiddenField.value = '0';
};
on('change', '#batch_checkbox_all', ({ target }) => {
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
const selectAllMatchingElement = document.querySelector(
@@ -84,7 +85,7 @@ on('change', '#batch_checkbox_all', ({ target }) => {
}
});
on('click', '.batch-table__select-all button', () => {
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
const hiddenField = document.querySelector<HTMLInputElement>(
'#select_all_matching',
);
@@ -112,7 +113,7 @@ on('click', '.batch-table__select-all button', () => {
}
});
on('change', batchCheckboxClassName, () => {
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
const checkAllElement = document.querySelector<HTMLInputElement>(
'input#batch_checkbox_all',
);
@@ -139,9 +140,14 @@ on('change', batchCheckboxClassName, () => {
}
});
on('change', '.filter-subset--with-select select', ({ target }) => {
if (target instanceof HTMLSelectElement) target.form?.submit();
});
Rails.delegate(
document,
'.filter-subset--with-select select',
'change',
({ target }) => {
if (target instanceof HTMLSelectElement) target.form?.submit();
},
);
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
const rejectMediaDiv = document.querySelector(
@@ -162,11 +168,11 @@ const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
}
};
on('change', '#domain_block_severity', ({ target }) => {
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
});
function onEnableBootstrapTimelineAccountsChange(target: HTMLInputElement) {
const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
const bootstrapTimelineAccountsField =
document.querySelector<HTMLInputElement>(
'#form_admin_settings_bootstrap_timeline_accounts',
@@ -188,11 +194,12 @@ function onEnableBootstrapTimelineAccountsChange(target: HTMLInputElement) {
);
}
}
}
};
on(
'change',
Rails.delegate(
document,
'#form_admin_settings_enable_bootstrap_timeline_accounts',
'change',
({ target }) => {
if (target instanceof HTMLInputElement)
onEnableBootstrapTimelineAccountsChange(target);
@@ -232,11 +239,11 @@ const onChangeRegistrationMode = (target: HTMLSelectElement) => {
});
};
function convertUTCDateTimeToLocal(value: string) {
const convertUTCDateTimeToLocal = (value: string) => {
const date = new Date(value + 'Z');
const twoChars = (x: number) => x.toString().padStart(2, '0');
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
}
};
function convertLocalDatetimeToUTC(value: string) {
const date = new Date(value);
@@ -244,9 +251,14 @@ function convertLocalDatetimeToUTC(value: string) {
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
}
on('change', '#form_admin_settings_registrations_mode', ({ target }) => {
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
});
Rails.delegate(
document,
'#form_admin_settings_registrations_mode',
'change',
({ target }) => {
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
},
);
async function mountReactComponent(element: Element) {
const componentName = element.getAttribute('data-admin-component');
@@ -293,7 +305,7 @@ ready(() => {
if (registrationMode) onChangeRegistrationMode(registrationMode);
const checkAllElement = document.querySelector<HTMLInputElement>(
'#batch_checkbox_all',
'input#batch_checkbox_all',
);
if (checkAllElement) {
const allCheckboxes = Array.from(
@@ -306,7 +318,7 @@ ready(() => {
}
document
.querySelector<HTMLAnchorElement>('a#add-instance-button')
.querySelector('a#add-instance-button')
?.addEventListener('click', (e) => {
const domain = document.querySelector<HTMLInputElement>(
'input[type="text"]#by_domain',
@@ -330,7 +342,7 @@ ready(() => {
}
});
on('submit', 'form', ({ target }) => {
Rails.delegate(document, 'form', 'submit', ({ target }) => {
if (target instanceof HTMLFormElement)
target
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')

View File

@@ -4,8 +4,8 @@ import { IntlMessageFormat } from 'intl-messageformat';
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
import { defineMessages } from 'react-intl';
import Rails from '@rails/ujs';
import axios from 'axios';
import { on } from 'delegated-events';
import { throttle } from 'lodash';
import { timeAgoString } from '../mastodon/components/relative_timestamp';
@@ -175,9 +175,10 @@ function loaded() {
});
}
on(
'input',
Rails.delegate(
document,
'input#user_account_attributes_username',
'input',
throttle(
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
@@ -201,47 +202,60 @@ function loaded() {
),
);
on('input', '#user_password,#user_password_confirmation', () => {
const password = document.querySelector<HTMLInputElement>(
'input#user_password',
);
const confirmation = document.querySelector<HTMLInputElement>(
'input#user_password_confirmation',
);
if (!confirmation || !password) return;
Rails.delegate(
document,
'#user_password,#user_password_confirmation',
'input',
() => {
const password = document.querySelector<HTMLInputElement>(
'input#user_password',
);
const confirmation = document.querySelector<HTMLInputElement>(
'input#user_password_confirmation',
);
if (!confirmation || !password) return;
if (confirmation.value && confirmation.value.length > password.maxLength) {
confirmation.setCustomValidity(
formatMessage(messages.passwordExceedsLength),
);
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(
formatMessage(messages.passwordDoesNotMatch),
);
} else {
confirmation.setCustomValidity('');
}
});
if (
confirmation.value &&
confirmation.value.length > password.maxLength
) {
confirmation.setCustomValidity(
formatMessage(messages.passwordExceedsLength),
);
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(
formatMessage(messages.passwordDoesNotMatch),
);
} else {
confirmation.setCustomValidity('');
}
},
);
}
on('change', '#edit_profile input[type=file]', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
Rails.delegate(
document,
'#edit_profile input[type=file]',
'change',
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
const avatar = document.querySelector<HTMLImageElement>(
`img#${target.id}-preview`,
);
const avatar = document.querySelector<HTMLImageElement>(
`img#${target.id}-preview`,
);
if (!avatar) return;
if (!avatar) return;
let file: File | undefined;
if (target.files) file = target.files[0];
let file: File | undefined;
if (target.files) file = target.files[0];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
if (url) avatar.src = url;
});
if (url) avatar.src = url;
},
);
on('click', '.input-copy input', ({ target }) => {
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
target.focus();
@@ -249,7 +263,7 @@ on('click', '.input-copy input', ({ target }) => {
target.setSelectionRange(0, target.value.length);
});
on('click', '.input-copy button', ({ target }) => {
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
if (!(target instanceof HTMLButtonElement)) return;
const input = target.parentNode?.querySelector<HTMLInputElement>(
@@ -298,22 +312,22 @@ const toggleSidebar = () => {
sidebar.classList.toggle('visible');
};
on('click', '.sidebar__toggle__icon', () => {
Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
toggleSidebar();
});
on('keydown', '.sidebar__toggle__icon', (e) => {
Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggleSidebar();
}
});
on('mouseover', 'img.custom-emoji', ({ target }) => {
Rails.delegate(document, 'img.custom-emoji', 'mouseover', ({ target }) => {
if (target instanceof HTMLImageElement && target.dataset.original)
target.src = target.dataset.original;
});
on('mouseout', 'img.custom-emoji', ({ target }) => {
Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => {
if (target instanceof HTMLImageElement && target.dataset.static)
target.src = target.dataset.static;
});
@@ -362,17 +376,22 @@ const setInputHint = (
}
};
on('change', '#account_statuses_cleanup_policy_enabled', ({ target }) => {
if (!(target instanceof HTMLInputElement) || !target.form) return;
Rails.delegate(
document,
'#account_statuses_cleanup_policy_enabled',
'change',
({ target }) => {
if (!(target instanceof HTMLInputElement) || !target.form) return;
target.form
.querySelectorAll<
HTMLInputElement | HTMLSelectElement
>('input:not([type=hidden], #account_statuses_cleanup_policy_enabled), select')
.forEach((input) => {
setInputDisabled(input, !target.checked);
});
});
target.form
.querySelectorAll<
HTMLInputElement | HTMLSelectElement
>('input:not([type=hidden], #account_statuses_cleanup_policy_enabled), select')
.forEach((input) => {
setInputDisabled(input, !target.checked);
});
},
);
const updateDefaultQuotePrivacyFromPrivacy = (
privacySelect: EventTarget | null,
@@ -395,13 +414,18 @@ const updateDefaultQuotePrivacyFromPrivacy = (
}
};
on('change', '#user_settings_attributes_default_privacy', ({ target }) => {
updateDefaultQuotePrivacyFromPrivacy(target);
});
Rails.delegate(
document,
'#user_settings_attributes_default_privacy',
'change',
({ target }) => {
updateDefaultQuotePrivacyFromPrivacy(target);
},
);
// Empty the honeypot fields in JS in case something like an extension
// automatically filled them.
on('submit', '#registration_new_user,#new_user', () => {
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
[
'user_website',
'user_confirm_password',
@@ -415,7 +439,7 @@ on('submit', '#registration_new_user,#new_user', () => {
});
});
on('click', '.rules-list button', ({ target }) => {
Rails.delegate(document, '.rules-list button', 'click', ({ target }) => {
if (!(target instanceof HTMLElement)) {
return;
}

View File

@@ -1,63 +0,0 @@
import { createRoot } from 'react-dom/client';
import { Provider as ReduxProvider } from 'react-redux';
import { importFetchedStatuses } from '@/mastodon/actions/importer';
import { hydrateStore } from '@/mastodon/actions/store';
import type { ApiAnnualReportResponse } from '@/mastodon/api/annual_report';
import { Router } from '@/mastodon/components/router';
import { WrapstodonSharedPage } from '@/mastodon/features/annual_report/shared_page';
import { IntlProvider, loadLocale } from '@/mastodon/locales';
import { loadPolyfills } from '@/mastodon/polyfills';
import ready from '@/mastodon/ready';
import { setReport } from '@/mastodon/reducers/slices/annual_report';
import { store } from '@/mastodon/store';
function loaded() {
const mountNode = document.getElementById('wrapstodon');
if (!mountNode) {
throw new Error('Mount node not found');
}
const propsNode = document.getElementById('wrapstodon-data');
if (!propsNode) {
throw new Error('Initial state prop not found');
}
const initialState = JSON.parse(
propsNode.textContent,
) as ApiAnnualReportResponse;
const report = initialState.annual_reports[0];
if (!report) {
throw new Error('Initial state report not found');
}
// Set up store
store.dispatch(
hydrateStore({
meta: { locale: document.documentElement.lang },
accounts: initialState.accounts,
}),
);
store.dispatch(importFetchedStatuses(initialState.statuses));
store.dispatch(setReport(report));
const root = createRoot(mountNode);
root.render(
<IntlProvider>
<ReduxProvider store={store}>
<Router>
<WrapstodonSharedPage />
</Router>
</ReduxProvider>
</IntlProvider>,
);
}
loadPolyfills()
.then(loadLocale)
.then(() => ready(loaded))
.catch((err: unknown) => {
console.error(err);
});

View File

@@ -0,0 +1,25 @@
export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
export function fetchBundleRequest(skipLoading) {
return {
type: BUNDLE_FETCH_REQUEST,
skipLoading,
};
}
export function fetchBundleSuccess(skipLoading) {
return {
type: BUNDLE_FETCH_SUCCESS,
skipLoading,
};
}
export function fetchBundleFail(error, skipLoading) {
return {
type: BUNDLE_FETCH_FAIL,
error,
skipLoading,
};
}

View File

@@ -709,7 +709,16 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
dispatch(useEmoji(suggestion));
} else if (suggestion.type === 'hashtag') {
completion = token + suggestion.name.slice(token.length - 1);
// 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'])}`;

View File

@@ -1,5 +1,3 @@
import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report';
import api from '../api';
import { importFetchedAccount } from './importer';
@@ -31,9 +29,6 @@ export const fetchServer = () => (dispatch, getState) => {
.get('/api/v2/instance').then(({ data }) => {
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
dispatch(fetchServerSuccess(data));
if (data.wrapstodon) {
void dispatch(checkAnnualReport());
}
}).catch(err => dispatch(fetchServerFail(err)));
};

View File

@@ -37,9 +37,7 @@ export function hydrateStore(rawState) {
dispatch(hydrateCompose());
dispatch(hydrateSearch());
if (rawState.accounts) {
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
}
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
dispatch(saveSettings());
};
}

View File

@@ -1,6 +1,5 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { reinsertAnnualReport, TIMELINE_WRAPSTODON } from '@/flavours/glitch/reducers/slices/annual_report';
import api, { getLinks } from 'flavours/glitch/api';
import { compareId } from 'flavours/glitch/compare_id';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
@@ -8,7 +7,7 @@ import { toServerSideType } from 'flavours/glitch/utils/filters';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers';
import { timelineDelete } from './timelines_typed';
import {timelineDelete} from './timelines_typed';
export { disconnectTimeline } from './timelines_typed';
@@ -26,16 +25,9 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
export const TIMELINE_GAP = null;
export const TIMELINE_NON_STATUS_MARKERS = [
TIMELINE_GAP,
TIMELINE_SUGGESTIONS,
TIMELINE_WRAPSTODON,
];
export const loadPending = timeline => ({
type: TIMELINE_LOAD_PENDING,
timeline,
@@ -143,7 +135,6 @@ export function expandTimeline(timelineId, path, params = {}) {
if (timelineId === 'home') {
dispatch(submitMarkers());
dispatch(reinsertAnnualReport())
}
} catch(error) {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));

View File

@@ -2,12 +2,6 @@ import { createAction } from '@reduxjs/toolkit';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import { TIMELINE_NON_STATUS_MARKERS } from './timelines';
export function isNonStatusId(value: unknown) {
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
}
export const disconnectTimeline = createAction(
'timeline/disconnect',
({ timeline }: { timeline: string }) => ({

View File

@@ -1,38 +0,0 @@
import api, { apiRequestGet, getAsyncRefreshHeader } from '../api';
import type { ApiAccountJSON } from '../api_types/accounts';
import type { ApiStatusJSON } from '../api_types/statuses';
import type { AnnualReport } from '../models/annual_report';
export type ApiAnnualReportState =
| 'available'
| 'generating'
| 'eligible'
| 'ineligible';
export const apiGetAnnualReportState = async (year: number) => {
const response = await api().get<{ state: ApiAnnualReportState }>(
`/api/v1/annual_reports/${year}/state`,
);
return {
state: response.data.state,
refresh: getAsyncRefreshHeader(response),
};
};
export const apiRequestGenerateAnnualReport = async (year: number) => {
const response = await api().post(`/api/v1/annual_reports/${year}/generate`);
return {
refresh: getAsyncRefreshHeader(response),
};
};
export interface ApiAnnualReportResponse {
annual_reports: AnnualReport[];
accounts: ApiAccountJSON[];
statuses: ApiStatusJSON[];
}
export const apiGetAnnualReport = (year: number) =>
apiRequestGet<ApiAnnualReportResponse>(`v1/annual_reports/${year}`);

View File

@@ -1,9 +1,8 @@
// See app/serializers/rest/custom_emoji_serializer.rb
// See app/serializers/rest/account_serializer.rb
export interface ApiCustomEmojiJSON {
shortcode: string;
static_url: string;
url: string;
category?: string;
featured?: boolean;
visible_in_picker: boolean;
}

View File

@@ -51,7 +51,7 @@ export interface ApiPreviewCardJSON {
html: string;
width: number;
height: number;
image: string | null;
image: string;
image_description: string;
embed_url: string;
blurhash: string;

View File

@@ -1,5 +1,9 @@
import { setupLinkListeners } from './utils/links';
import Rails from '@rails/ujs';
export function start() {
setupLinkListeners();
try {
Rails.start();
} catch {
// If called twice
}
}

View File

@@ -6,7 +6,6 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import type { Account } from 'flavours/glitch/models/account';
import { CustomEmojiProvider } from './emoji/context';
import { EmojiHTML } from './emoji/html';
import { useElementHandledLink } from './status/handled_link';
@@ -22,12 +21,13 @@ export const AccountFields: React.FC<Pick<Account, 'fields' | 'emojis'>> = ({
}
return (
<CustomEmojiProvider emojis={emojis}>
<>
{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}
/>
@@ -52,12 +52,13 @@ export const AccountFields: React.FC<Pick<Account, 'fields' | 'emojis'>> = ({
<EmojiHTML
as='span'
htmlString={pair.value_emojified}
extraEmojis={emojis}
{...htmlHandlers}
/>
</dd>
</dl>
))}
</CustomEmojiProvider>
</>
);
};

View File

@@ -49,11 +49,7 @@ export const Alert: React.FC<{
</span>
{hasAction && (
<button
className='notification-bar__action'
onClick={onActionClick}
type='button'
>
<button className='notification-bar__action' onClick={onActionClick}>
{action}
</button>
)}

View File

@@ -47,7 +47,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
rootClose
onHide={handleClose}
show={open}
target={anchorRef}
target={anchorRef.current}
placement='top-end'
flip
offset={offset}

View File

@@ -159,8 +159,8 @@ export default class AutosuggestInput extends ImmutablePureComponent {
this.input.focus();
};
componentDidUpdate (prevProps) {
if (prevProps.suggestions !== this.props.suggestions && this.props.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
}

View File

@@ -50,7 +50,6 @@ const AutosuggestTextarea = forwardRef(({
onKeyUp,
onKeyDown,
onPaste,
onDrop,
onFocus,
autoFocus = true,
lang,
@@ -154,12 +153,6 @@ const AutosuggestTextarea = forwardRef(({
onPaste(e);
}, [onPaste]);
const handleDrop = useCallback((e) => {
if (onDrop) {
onDrop(e);
}
}, [onDrop]);
// Show the suggestions again whenever they change and the textarea is focused
useEffect(() => {
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
@@ -211,7 +204,6 @@ const AutosuggestTextarea = forwardRef(({
onFocus={handleFocus}
onBlur={handleBlur}
onPaste={handlePaste}
onDrop={handleDrop}
dir='auto'
aria-autocomplete='list'
aria-label={placeholder}
@@ -243,7 +235,6 @@ AutosuggestTextarea.propTypes = {
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
onPaste: PropTypes.func.isRequired,
onDrop: PropTypes.func,
onFocus:PropTypes.func,
autoFocus: PropTypes.bool,
lang: PropTypes.string,

View File

@@ -78,7 +78,6 @@ export const Button: React.FC<Props> = ({
aria-live={loading !== undefined ? 'polite' : undefined}
onClick={handleClick}
title={title}
// eslint-disable-next-line react/button-has-type -- set correctly via TS
type={type}
{...props}
>

View File

@@ -1,126 +0,0 @@
import type { FC } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn, userEvent, expect } from 'storybook/test';
import type { CarouselProps } from './index';
import { Carousel } from './index';
interface TestSlideProps {
id: number;
text: string;
color: string;
}
const TestSlide: FC<TestSlideProps & { active: boolean }> = ({
active,
text,
color,
}) => (
<div
className='test-slide'
style={{
backgroundColor: active ? color : undefined,
}}
>
{text}
</div>
);
const slides: TestSlideProps[] = [
{
id: 1,
text: 'first',
color: 'red',
},
{
id: 2,
text: 'second',
color: 'pink',
},
{
id: 3,
text: 'third',
color: 'orange',
},
];
type StoryProps = Pick<
CarouselProps<TestSlideProps>,
'items' | 'renderItem' | 'emptyFallback' | 'onChangeSlide'
>;
const meta = {
title: 'Components/Carousel',
args: {
items: slides,
renderItem(item, active) {
return <TestSlide {...item} active={active} key={item.id} />;
},
onChangeSlide: fn(),
emptyFallback: 'No slides available',
},
render(args) {
return (
<>
<Carousel {...args} />
<style>
{`.test-slide {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: bold;
min-height: 100px;
transition: background-color 0.3s;
background-color: black;
}`}
</style>
</>
);
},
argTypes: {
emptyFallback: {
type: 'string',
},
},
tags: ['test'],
} satisfies Meta<StoryProps>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
async play({ args, canvas }) {
const nextButton = await canvas.findByRole('button', { name: /next/i });
const slides = await canvas.findAllByRole('group');
await expect(slides).toHaveLength(slides.length);
await userEvent.click(nextButton);
await expect(args.onChangeSlide).toHaveBeenCalledWith(1, slides[1]);
await userEvent.click(nextButton);
await expect(args.onChangeSlide).toHaveBeenCalledWith(2, slides[2]);
// Wrap around
await userEvent.click(nextButton);
await expect(args.onChangeSlide).toHaveBeenCalledWith(0, slides[0]);
},
};
export const DifferentHeights: Story = {
args: {
items: slides.map((props, index) => ({
...props,
styles: { height: 100 + index * 100 },
})),
},
};
export const NoSlides: Story = {
args: {
items: [],
},
};

View File

@@ -1,244 +0,0 @@
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type {
ComponentPropsWithoutRef,
ComponentType,
ReactElement,
ReactNode,
} from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { usePrevious } from '@dnd-kit/utilities';
import { animated, useSpring } from '@react-spring/web';
import { useDrag } from '@use-gesture/react';
import type { CarouselPaginationProps } from './pagination';
import { CarouselPagination } from './pagination';
import './styles.scss';
const defaultMessages = defineMessages({
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
current: {
id: 'carousel.current',
defaultMessage: '<sr>Slide</sr> {current, number} / {max, number}',
},
slide: {
id: 'carousel.slide',
defaultMessage: 'Slide {current, number} of {max, number}',
},
});
export type MessageKeys = keyof typeof defaultMessages;
export interface CarouselSlideProps {
id: string | number;
}
export type RenderSlideFn<
SlideProps extends CarouselSlideProps = CarouselSlideProps,
> = (item: SlideProps, active: boolean, index: number) => ReactElement;
export interface CarouselProps<
SlideProps extends CarouselSlideProps = CarouselSlideProps,
> {
items: SlideProps[];
renderItem: RenderSlideFn<SlideProps>;
onChangeSlide?: (index: number, ref: Element) => void;
paginationComponent?: ComponentType<CarouselPaginationProps> | null;
paginationProps?: Partial<CarouselPaginationProps>;
messages?: Record<MessageKeys, MessageDescriptor>;
emptyFallback?: ReactNode;
classNamePrefix?: string;
slideClassName?: string;
}
export const Carousel = <
SlideProps extends CarouselSlideProps = CarouselSlideProps,
>({
items,
renderItem,
onChangeSlide,
paginationComponent: Pagination = CarouselPagination,
paginationProps = {},
messages = defaultMessages,
children,
emptyFallback = null,
className,
classNamePrefix = 'carousel',
slideClassName,
...wrapperProps
}: CarouselProps<SlideProps> & ComponentPropsWithoutRef<'div'>) => {
// Handle slide change
const [slideIndex, setSlideIndex] = useState(0);
const wrapperRef = useRef<HTMLDivElement>(null);
// Handle slide heights
const [currentSlideHeight, setCurrentSlideHeight] = useState(
() => wrapperRef.current?.scrollHeight ?? 0,
);
const previousSlideHeight = usePrevious(currentSlideHeight);
const handleSlideChange = useCallback(
(direction: number) => {
setSlideIndex((prev) => {
const max = items.length - 1;
let newIndex = prev + direction;
if (newIndex < 0) {
newIndex = max;
} else if (newIndex > max) {
newIndex = 0;
}
const slide = wrapperRef.current?.children[newIndex];
if (slide) {
setCurrentSlideHeight(slide.scrollHeight);
if (slide instanceof HTMLElement) {
onChangeSlide?.(newIndex, slide);
}
}
return newIndex;
});
},
[items.length, onChangeSlide],
);
const observerRef = useRef<ResizeObserver | null>(null);
observerRef.current ??= new ResizeObserver(() => {
handleSlideChange(0);
});
const wrapperStyles = useSpring({
x: `-${slideIndex * 100}%`,
height: currentSlideHeight,
// Don't animate from zero to the height of the initial slide
immediate: !previousSlideHeight,
});
useLayoutEffect(() => {
// Update slide height when the component mounts
if (currentSlideHeight === 0) {
handleSlideChange(0);
}
}, [currentSlideHeight, handleSlideChange]);
// Handle swiping animations
const bind = useDrag(
({ swipe: [swipeX] }) => {
handleSlideChange(swipeX * -1); // Invert swipe as swiping left loads the next slide.
},
{ pointer: { capture: false } },
);
const handlePrev = useCallback(() => {
handleSlideChange(-1);
// We're focusing on the wrapper as the child slides can potentially be inert.
// Because of that, only the active slide can be focused anyway.
wrapperRef.current?.focus();
}, [handleSlideChange]);
const handleNext = useCallback(() => {
handleSlideChange(1);
wrapperRef.current?.focus();
}, [handleSlideChange]);
const intl = useIntl();
if (items.length === 0) {
return emptyFallback;
}
return (
<div
{...bind()}
aria-roledescription='carousel'
role='region'
className={classNames(classNamePrefix, className)}
{...wrapperProps}
>
<div className={`${classNamePrefix}__header`}>
{children}
{Pagination && items.length > 1 && (
<Pagination
current={slideIndex}
max={items.length}
onNext={handleNext}
onPrev={handlePrev}
className={`${classNamePrefix}__pagination`}
messages={messages}
{...paginationProps}
/>
)}
</div>
<animated.div
className={`${classNamePrefix}__slides`}
ref={wrapperRef}
style={wrapperStyles}
aria-label={intl.formatMessage(messages.slide, {
current: slideIndex + 1,
max: items.length,
})}
tabIndex={-1}
>
{items.map((itemsProps, index) => (
<CarouselSlideWrapper<SlideProps>
item={itemsProps}
renderItem={renderItem}
observer={observerRef.current}
index={index}
key={`slide-${itemsProps.id}`}
className={classNames(`${classNamePrefix}__slide`, slideClassName, {
active: index === slideIndex,
})}
active={index === slideIndex}
/>
))}
</animated.div>
</div>
);
};
type CarouselSlideWrapperProps<SlideProps extends CarouselSlideProps> = {
observer: ResizeObserver | null;
className: string;
active: boolean;
item: SlideProps;
index: number;
} & Pick<CarouselProps<SlideProps>, 'renderItem'>;
const CarouselSlideWrapper = <SlideProps extends CarouselSlideProps>({
observer,
className,
active,
renderItem,
item,
index,
}: CarouselSlideWrapperProps<SlideProps>) => {
const handleRef = useCallback(
(instance: HTMLDivElement | null) => {
if (observer && instance) {
observer.observe(instance);
}
},
[observer],
);
const children = useMemo(
() => renderItem(item, active, index),
[renderItem, item, active, index],
);
return (
<div
ref={handleRef}
className={className}
role='group'
aria-roledescription='slide'
inert={active ? undefined : ''}
data-index={index}
>
{children}
</div>
);
};

View File

@@ -1,54 +0,0 @@
import type { FC, MouseEventHandler } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { useIntl } from 'react-intl';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { IconButton } from '../icon_button';
import type { MessageKeys } from './index';
export interface CarouselPaginationProps {
onNext: MouseEventHandler;
onPrev: MouseEventHandler;
current: number;
max: number;
className?: string;
messages: Record<MessageKeys, MessageDescriptor>;
}
export const CarouselPagination: FC<CarouselPaginationProps> = ({
onNext,
onPrev,
current,
max,
className = '',
messages,
}) => {
const intl = useIntl();
return (
<div className={className}>
<IconButton
title={intl.formatMessage(messages.previous)}
icon='chevron-left'
iconComponent={ChevronLeftIcon}
onClick={onPrev}
/>
<span aria-live='polite'>
{intl.formatMessage(messages.current, {
current: current + 1,
max,
sr: (chunk) => <span className='sr-only'>{chunk}</span>,
})}
</span>
<IconButton
title={intl.formatMessage(messages.next)}
icon='chevron-right'
iconComponent={ChevronRightIcon}
onClick={onNext}
/>
</div>
);
};

View File

@@ -1,28 +0,0 @@
.carousel {
gap: 16px;
overflow: hidden;
touch-action: pan-y;
&__header {
padding: 8px 16px;
}
&__pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
&__slides {
display: flex;
flex-wrap: nowrap;
align-items: start;
}
&__slide {
flex: 0 0 100%;
width: 100%;
overflow: hidden;
}
}

View File

@@ -30,7 +30,7 @@ export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
const handleClick = useHandleClick(onClick);
const component = (
<button onClick={handleClick} className='column-back-button' type='button'>
<button onClick={handleClick} className='column-back-button'>
<Icon
id='chevron-left'
icon={ArrowBackIcon}

View File

@@ -53,7 +53,6 @@ const BackButton: React.FC<{
compact: onlyIcon,
})}
aria-label={intl.formatMessage(messages.back)}
type='button'
>
<Icon
id='chevron-left'
@@ -173,7 +172,6 @@ export const ColumnHeader: React.FC<Props> = ({
<button
className='text-btn column-header__setting-btn'
onClick={handlePin}
type='button'
>
<Icon id='times' icon={CloseIcon} />{' '}
<FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
@@ -187,7 +185,6 @@ export const ColumnHeader: React.FC<Props> = ({
aria-label={intl.formatMessage(messages.moveLeft)}
className='icon-button column-header__setting-btn'
onClick={handleMoveLeft}
type='button'
>
<Icon id='chevron-left' icon={ChevronLeftIcon} />
</button>
@@ -196,7 +193,6 @@ export const ColumnHeader: React.FC<Props> = ({
aria-label={intl.formatMessage(messages.moveRight)}
className='icon-button column-header__setting-btn'
onClick={handleMoveRight}
type='button'
>
<Icon id='chevron-right' icon={ChevronRightIcon} />
</button>
@@ -207,7 +203,6 @@ export const ColumnHeader: React.FC<Props> = ({
<button
className='text-btn column-header__setting-btn'
onClick={handlePin}
type='button'
>
<Icon id='plus' icon={AddIcon} />{' '}
<FormattedMessage id='column_header.pin' defaultMessage='Pin' />
@@ -242,7 +237,6 @@ export const ColumnHeader: React.FC<Props> = ({
collapsed ? messages.show : messages.hide,
)}
onClick={handleToggleClick}
type='button'
>
<i className='icon-with-badge'>
<Icon
@@ -265,11 +259,7 @@ export const ColumnHeader: React.FC<Props> = ({
<>
{backButton}
<button
onClick={handleTitleClick}
className='column-header__title'
type='button'
>
<button onClick={handleTitleClick} className='column-header__title'>
{!backButton && (
<Icon
id={icon}

View File

@@ -1,4 +1,4 @@
import { useCallback, useState, useRef } from 'react';
import { useCallback, useState, useEffect, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -12,15 +12,11 @@ export const ColumnSearchHeader: React.FC<{
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState('');
// Reset the component when it turns from active to inactive.
// [More on this pattern](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
const [previousActive, setPreviousActive] = useState(active);
if (active !== previousActive) {
setPreviousActive(active);
useEffect(() => {
if (!active) {
setValue('');
}
}
}, [active]);
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {

View File

@@ -41,7 +41,7 @@ export const ContentWarning: React.FC<{
<EmojiHTML
as='span'
htmlString={text}
extraEmojis={status.get('emoji') as List<CustomEmoji>}
extraEmojis={status.get('emojis') as List<CustomEmoji>}
/>
</StatusBanner>
);

View File

@@ -74,7 +74,7 @@ export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
onBlur={handleBlur}
/>
<button className='button' onClick={handleButtonClick} type='button'>
<button className='button' onClick={handleButtonClick}>
<Icon id='copy' icon={ContentCopyIcon} />{' '}
{copied ? (
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />

View File

@@ -109,7 +109,7 @@ export const Dropdown: FC<
placement='bottom-start'
onHide={handleClose}
flip
target={buttonRef}
target={buttonRef.current}
popperConfig={{
strategy: 'fixed',
modifiers: [matchWidth],

View File

@@ -216,7 +216,6 @@ export const DropdownMenu = <Item = MenuItem,>({
onClick={handleItemClick}
data-index={i}
aria-disabled={disabled}
type='button'
>
<DropdownMenuItemContent item={option} />
</button>

View File

@@ -108,7 +108,7 @@ export const EditedTimestamp: React.FC<{
onItemClick={handleItemClick}
forceDropdown
>
<button className='dropdown-menu__text-button' type='button'>
<button className='dropdown-menu__text-button'>
<FormattedMessage
id='status.edited'
defaultMessage='Edited {date}'

View File

@@ -1,14 +1,9 @@
import type { FC } from 'react';
import { useContext, useEffect, useState } from 'react';
import classNames from 'classnames';
import { EMOJI_TYPE_CUSTOM } from '@/flavours/glitch/features/emoji/constants';
import { useEmojiAppState } from '@/flavours/glitch/features/emoji/mode';
import {
emojiToInversionClassName,
unicodeHexToUrl,
} from '@/flavours/glitch/features/emoji/normalize';
import { unicodeHexToUrl } from '@/flavours/glitch/features/emoji/normalize';
import {
isStateLoaded,
loadEmojiDataToState,
@@ -46,9 +41,6 @@ export const Emoji: FC<EmojiProps> = ({
}, [appState.currentLocale, state]);
const animate = useContext(AnimateEmojiContext);
const inversionClass = emojiToInversionClassName(code);
const fallback = showFallback ? code : null;
// If the code is invalid or we otherwise know it's not valid, show the fallback.
@@ -87,7 +79,7 @@ export const Emoji: FC<EmojiProps> = ({
src={src}
alt={state.data.unicode}
title={state.data.label}
className={classNames('emojione', inversionClass)}
className='emojione'
loading='lazy'
/>
);

View File

@@ -27,23 +27,22 @@ export const ExitAnimationWrapper: React.FC<{
*/
children: (delayedIsActive: boolean) => React.ReactNode;
}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => {
const [delayedIsActive, setDelayedIsActive] = useState(
isActive && !withEntryDelay,
);
const [delayedIsActive, setDelayedIsActive] = useState(false);
useEffect(() => {
const withDelay = !isActive || withEntryDelay;
if (isActive && !withEntryDelay) {
setDelayedIsActive(true);
const timeout = setTimeout(
() => {
return () => '';
} else {
const timeout = setTimeout(() => {
setDelayedIsActive(isActive);
},
withDelay ? delayMs : 0,
);
}, delayMs);
return () => {
clearTimeout(timeout);
};
return () => {
clearTimeout(timeout);
};
}
}, [isActive, delayMs, withEntryDelay]);
if (!isActive && !delayedIsActive) {

View File

@@ -1,43 +1,38 @@
import { useCallback, useEffect, useId } from 'react';
import type { ComponentPropsWithRef } from 'react';
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
useId,
} from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import type { AnimatedProps } from '@react-spring/web';
import { animated, useSpring } from '@react-spring/web';
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 { StatusQuoteManager } from '@/flavours/glitch/components/status_quoted';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from '@/flavours/glitch/store';
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';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
import { Carousel } from './carousel';
const pinnedStatusesSelector = createAppSelector(
[
(state, accountId: string, tagged?: string) =>
(state.timelines as ImmutableMap<string, unknown>).getIn(
[`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'],
ImmutableList(),
) as ImmutableList<string>,
],
(items) => items.toArray().map((id) => ({ id })),
);
const messages = defineMessages({
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
current: {
id: 'featured_carousel.current',
defaultMessage: '<sr>Post</sr> {current, number} / {max, number}',
},
previous: { id: 'featured_carousel.previous', defaultMessage: 'Previous' },
next: { id: 'featured_carousel.next', defaultMessage: 'Next' },
slide: {
id: 'featured_carousel.slide',
defaultMessage: 'Post {current, number} of {max, number}',
defaultMessage: '{index} of {total}',
},
});
@@ -45,6 +40,7 @@ export const FeaturedCarousel: React.FC<{
accountId: string;
tagged?: string;
}> = ({ accountId, tagged }) => {
const intl = useIntl();
const accessibilityId = useId();
// Load pinned statuses
@@ -54,37 +50,175 @@ export const FeaturedCarousel: React.FC<{
void dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
}
}, [accountId, dispatch, tagged]);
const pinnedStatuses = useAppSelector((state) =>
pinnedStatusesSelector(state, accountId, tagged),
const pinnedStatuses = useAppSelector(
(state) =>
(state.timelines as ImmutableMap<string, unknown>).getIn(
[`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'],
ImmutableList(),
) as ImmutableList<string>,
);
const renderSlide = useCallback(
({ id }: { id: string }) => (
<StatusQuoteManager id={id} contextType='account' withCounters />
),
[],
// Handle slide change
const [slideIndex, setSlideIndex] = useState(0);
const wrapperRef = useRef<HTMLDivElement>(null);
const handleSlideChange = useCallback(
(direction: number) => {
setSlideIndex((prev) => {
const max = pinnedStatuses.size - 1;
let newIndex = prev + direction;
if (newIndex < 0) {
newIndex = max;
} else if (newIndex > max) {
newIndex = 0;
}
const slide = wrapperRef.current?.children[newIndex];
if (slide) {
setCurrentSlideHeight(slide.scrollHeight);
}
return newIndex;
});
},
[pinnedStatuses.size],
);
if (!accountId || pinnedStatuses.length === 0) {
// Handle slide heights
const [currentSlideHeight, setCurrentSlideHeight] = useState(
wrapperRef.current?.scrollHeight ?? 0,
);
const previousSlideHeight = usePrevious(currentSlideHeight);
const observerRef = useRef<ResizeObserver>(
new ResizeObserver(() => {
handleSlideChange(0);
}),
);
const wrapperStyles = useSpring({
x: `-${slideIndex * 100}%`,
height: currentSlideHeight,
// Don't animate from zero to the height of the initial slide
immediate: !previousSlideHeight,
});
useLayoutEffect(() => {
// Update slide height when the component mounts
if (currentSlideHeight === 0) {
handleSlideChange(0);
}
}, [currentSlideHeight, handleSlideChange]);
// Handle swiping animations
const bind = useDrag(({ swipe: [swipeX] }) => {
handleSlideChange(swipeX * -1); // Invert swipe as swiping left loads the next slide.
});
const handlePrev = useCallback(() => {
handleSlideChange(-1);
}, [handleSlideChange]);
const handleNext = useCallback(() => {
handleSlideChange(1);
}, [handleSlideChange]);
if (!accountId || pinnedStatuses.isEmpty()) {
return null;
}
return (
<Carousel
items={pinnedStatuses}
renderItem={renderSlide}
<div
className='featured-carousel'
{...bind()}
aria-roledescription='carousel'
aria-labelledby={`${accessibilityId}-title`}
classNamePrefix='featured-carousel'
messages={messages}
role='region'
>
<h4 className='featured-carousel__title' id={`${accessibilityId}-title`}>
<Icon id='thumb-tack' icon={PushPinIcon} />
<FormattedMessage
id='featured_carousel.header'
defaultMessage='{count, plural, one {Pinned Post} other {Pinned Posts}}'
values={{ count: pinnedStatuses.length }}
/>
</h4>
</Carousel>
<div className='featured-carousel__header'>
<h4
className='featured-carousel__title'
id={`${accessibilityId}-title`}
>
<Icon id='thumb-tack' icon={PushPinIcon} />
<FormattedMessage
id='featured_carousel.header'
defaultMessage='{count, plural, one {Pinned Post} other {Pinned Posts}}'
values={{ count: pinnedStatuses.size }}
/>
</h4>
{pinnedStatuses.size > 1 && (
<>
<IconButton
title={intl.formatMessage(messages.previous)}
icon='chevron-left'
iconComponent={ChevronLeftIcon}
onClick={handlePrev}
/>
<span aria-live='polite'>
<FormattedMessage
id='featured_carousel.post'
defaultMessage='Post'
>
{(text) => <span className='sr-only'>{text}</span>}
</FormattedMessage>
{slideIndex + 1} / {pinnedStatuses.size}
</span>
<IconButton
title={intl.formatMessage(messages.next)}
icon='chevron-right'
iconComponent={ChevronRightIcon}
onClick={handleNext}
/>
</>
)}
</div>
<animated.div
className='featured-carousel__slides'
ref={wrapperRef}
style={wrapperStyles}
aria-atomic='false'
aria-live='polite'
>
{pinnedStatuses.map((statusId, index) => (
<FeaturedCarouselItem
key={`f-${statusId}`}
data-index={index}
aria-label={intl.formatMessage(messages.slide, {
index: index + 1,
total: pinnedStatuses.size,
})}
statusId={statusId}
observer={observerRef.current}
active={index === slideIndex}
/>
))}
</animated.div>
</div>
);
};
interface FeaturedCarouselItemProps {
statusId: string;
active: boolean;
observer: ResizeObserver;
}
const FeaturedCarouselItem: React.FC<
FeaturedCarouselItemProps & AnimatedProps<ComponentPropsWithRef<'div'>>
> = ({ statusId, active, observer, ...props }) => {
const handleRef = useCallback(
(instance: HTMLDivElement | null) => {
if (instance) {
observer.observe(instance);
}
},
[observer],
);
return (
<animated.div
className='featured-carousel__slide'
// @ts-expect-error inert in not in this version of React
inert={!active ? 'true' : undefined}
aria-roledescription='slide'
role='group'
ref={handleRef}
{...props}
>
<StatusQuoteManager id={statusId} contextType='account' withCounters />
</animated.div>
);
};

View File

@@ -235,7 +235,7 @@ const HashtagBar: React.FC<{
))}
{!expanded && hashtags.length > VISIBLE_HASHTAGS && (
<button className='link-button' onClick={handleClick} type='button'>
<button className='link-button' onClick={handleClick}>
<FormattedMessage
id='hashtags.and_other'
defaultMessage='…and {count, plural, other {# more}}'

View File

@@ -112,7 +112,6 @@ const hotkeyMatcherMap = {
openProfile: just('p'),
moveDown: just('j'),
moveUp: just('k'),
moveToTop: just('0'),
toggleHidden: just('x'),
toggleSensitive: just('h'),
toggleComposeSpoilers: optionPlus('x'),

View File

@@ -27,6 +27,7 @@ export const HoverCardController: React.FC = () => {
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
const [setScrollTimeout] = useTimeout();
const location = useLocation();
const handleClose = useCallback(() => {
cancelEnterTimeout();
@@ -35,12 +36,9 @@ export const HoverCardController: React.FC = () => {
setAnchor(null);
}, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]);
const location = useLocation();
const [previousLocation, setPreviousLocation] = useState(location);
if (location !== previousLocation) {
setPreviousLocation(location);
useEffect(() => {
handleClose();
}
}, [handleClose, location]);
useEffect(() => {
let isScrolling = false;

View File

@@ -1,9 +1,7 @@
import { useCallback, forwardRef } from 'react';
import { useState, useEffect, useCallback, forwardRef } from 'react';
import classNames from 'classnames';
import { usePrevious } from '../hooks/usePrevious';
import { AnimatedNumber } from './animated_number';
import type { IconProp } from './icon';
import { Icon } from './icon';
@@ -61,6 +59,23 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
},
buttonRef,
) => {
const [activate, setActivate] = useState(false);
const [deactivate, setDeactivate] = useState(false);
useEffect(() => {
if (!animate) {
return;
}
if (activate && !active) {
setActivate(false);
setDeactivate(true);
} else if (!activate && active) {
setActivate(true);
setDeactivate(false);
}
}, [setActivate, setDeactivate, animate, active, activate]);
const handleClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(
(e) => {
e.preventDefault();
@@ -97,15 +112,12 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
...(active ? activeStyle : {}),
};
const previousActive = usePrevious(active) ?? active;
const shouldAnimate = animate && active !== previousActive;
const classes = classNames(className, 'icon-button', {
active,
disabled,
inverted,
activate: shouldAnimate && active,
deactivate: shouldAnimate && !active,
activate,
deactivate,
overlayed: overlay,
'icon-button--with-counter': typeof counter !== 'undefined',
});

View File

@@ -23,7 +23,6 @@ export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
type='button'
>
<FormattedMessage
id='learn_more_link.learn_more'
@@ -49,11 +48,7 @@ export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
<div className='learn-more__popout__content'>{children}</div>
<div>
<button
className='link-button'
onClick={handleClick}
type='button'
>
<button className='link-button' onClick={handleClick}>
<FormattedMessage
id='learn_more_link.got_it'
defaultMessage='Got it'

View File

@@ -32,7 +32,6 @@ export const LoadGap = <T,>({ disabled, param, onClick }: Props<T>) => {
onClick={handleClick}
aria-label={intl.formatMessage(messages.load_more)}
title={intl.formatMessage(messages.load_more)}
type='button'
>
{loading ? (
<LoadingIndicator />

View File

@@ -7,7 +7,7 @@ interface Props {
export const LoadPending: React.FC<Props> = ({ onClick, count }) => {
return (
<button className='load-more load-gap' onClick={onClick} type='button'>
<button className='load-more load-gap' onClick={onClick}>
<FormattedMessage
id='load_pending'
defaultMessage='{count, plural, one {# new item} other {# new items}}'

View File

@@ -1,5 +1,3 @@
import classNames from 'classnames';
import logo from '@/images/logo.svg';
export const WordmarkLogo: React.FC = () => (
@@ -9,12 +7,8 @@ export const WordmarkLogo: React.FC = () => (
</svg>
);
export const IconLogo: React.FC<{ className?: string }> = ({ className }) => (
<svg
viewBox='0 0 79 79'
className={classNames('logo logo--icon', className)}
role='img'
>
export const IconLogo: React.FC = () => (
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
<title>Mastodon</title>
<use xlinkHref='#logo-symbol-icon' />
</svg>

View File

@@ -251,13 +251,15 @@ class MediaGallery extends PureComponent {
window.removeEventListener('resize', this.handleResize);
}
componentDidUpdate (prevProps) {
if (!is(prevProps.media, this.props.media) && this.props.visible === undefined) {
this.setState({ visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all' });
} else if (!is(prevProps.visible, this.props.visible) && this.props.visible !== undefined) {
this.setState({ visible: this.props.visible });
UNSAFE_componentWillReceiveProps (nextProps) {
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ visible: nextProps.visible });
}
}
componentDidUpdate () {
if (this.node) {
this.handleResize();
}

View File

@@ -66,6 +66,14 @@ class ModalRoot extends PureComponent {
}
}
UNSAFE_componentWillReceiveProps (nextProps) {
if (!!nextProps.children && !this.props.children) {
this.activeElement = document.activeElement;
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
}
}
componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
@@ -82,15 +90,9 @@ class ModalRoot extends PureComponent {
this._handleModalClose();
}
if (this.props.children && !prevProps.children) {
this.activeElement = document.activeElement;
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
this._handleModalOpen();
}
if (this.props.children) {
this._ensureHistoryBuffer();
}

View File

@@ -35,9 +35,6 @@ const messages = defineMessages({
},
});
const isPollExpired = (expiresAt: Model.Poll['expires_at']) =>
new Date(expiresAt).getTime() < Date.now();
interface PollProps {
pollId: string;
status: Status;
@@ -61,7 +58,8 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
if (!poll) {
return false;
}
return poll.expired || isPollExpired(poll.expires_at);
const expiresAt = poll.expires_at;
return poll.expired || new Date(expiresAt).getTime() < Date.now();
}, [poll]);
const timeRemaining = useMemo(() => {
if (!poll) {
@@ -173,14 +171,13 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
className='button button-secondary'
disabled={voteDisabled}
onClick={handleVote}
type='button'
>
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
</button>
)}
{!showResults && (
<>
<button className='poll__link' onClick={handleReveal} type='button'>
<button className='poll__link' onClick={handleReveal}>
<FormattedMessage id='poll.reveal' defaultMessage='See results' />
</button>{' '}
·{' '}
@@ -188,11 +185,7 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
)}
{showResults && !disabled && (
<>
<button
className='poll__link'
onClick={handleRefresh}
type='button'
>
<button className='poll__link' onClick={handleRefresh}>
<FormattedMessage id='poll.refresh' defaultMessage='Refresh' />
</button>{' '}
·{' '}

View File

@@ -115,7 +115,6 @@ class Status extends ImmutablePureComponent {
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
showActions: PropTypes.bool,
prepend: PropTypes.string,
withDismiss: PropTypes.bool,
isQuotedPost: PropTypes.bool,
@@ -160,6 +159,7 @@ class Status extends ImmutablePureComponent {
'expanded',
'unread',
'pictureInPicture',
'onQuoteCancel',
'previousId',
'nextInReplyToId',
'rootId',
@@ -466,8 +466,7 @@ class Status extends ImmutablePureComponent {
onOpenMedia,
notification,
history,
showActions = true,
isQuotedPost = false,
isQuotedPost,
...other
} = this.props;
let attachments = null;
@@ -636,7 +635,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted && !status.get('quote')) {
media.push(
<Card
key={`${status.get('id')}-${status.get('edited_at')}`}
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
sensitive={status.get('sensitive')}
/>,
@@ -765,7 +764,7 @@ class Status extends ImmutablePureComponent {
{/* This is a glitch-soc addition to have a placeholder */}
{!expanded && <MentionsPlaceholder status={status} />}
{(showActions && !isQuotedPost) &&
{!isQuotedPost &&
<StatusActionBar
status={status}
account={status.get('account')}

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