Compare commits

...

309 commits

Author SHA1 Message Date
5ced66d73a Merge commit '97f6baf977212e84125ac325176ad305ad5b068a' into bark-prod
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-08-30 09:45:35 +02:00
Claire
97f6baf977
Bump version to v4.3.0-beta.1 (#30989) 2024-08-23 16:07:32 +00:00
Michael Stanclift
77c055b78c
Update Docker Compose for 4.3-beta (#31554) 2024-08-23 16:00:07 +00:00
Claire
c493689e84
Change output format of repo:changelog task (#31546) 2024-08-23 12:55:54 +00:00
github-actions[bot]
62be0234d5
New Crowdin Translations (automated) (#31559)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-23 08:59:31 +00:00
David Roetzel
0374918746
Add spec for doorkeeper behavior around issuing tokens (#31545) 2024-08-23 08:20:32 +00:00
Matt Jankowski
6ec768668e
Remove nsa statsd integration (replaced by OpenTelemetry) (#30240) 2024-08-22 20:28:54 +00:00
Claire
e08d22724d
Fix missing CSS in moderation interface (#31550) 2024-08-22 18:57:22 +00:00
Eugen Rochko
500f4925a5
Change how content warnings and filters are displayed in web UI (#31365) 2024-08-22 17:12:35 +00:00
Claire
98237207e6
Fix unnecessary “next” link when API returns fewer notification requests than requested (#31541) 2024-08-22 13:42:04 +00:00
Claire
abe292b0ec
Hide multiple selection bar for notification requests when no notificion requests are loaded (#31540) 2024-08-22 13:42:02 +00:00
renovate[bot]
61d9704f6d
Update babel monorepo to v7.25.4 (#31542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 11:51:30 +00:00
renovate[bot]
c70dc5f0d7
Update dependency node to 20.17 (#31531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 11:50:46 +00:00
Renaud Chaput
4a968cb7a9
Add icon field to instance endpoint (#30205) 2024-08-22 10:04:27 +00:00
Emelia Smith
376088f6da
Add button to view the Hashtag on the instance from Hashtags in Moderation UI (#31533) 2024-08-22 08:42:33 +00:00
renovate[bot]
170c6e9733
Update dependency opentelemetry-instrumentation-active_job to v0.7.7 (#31530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 08:27:46 +00:00
renovate[bot]
fd93f817e7
Update dependency aws-sdk-s3 to v1.159.0 (#31518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 08:15:54 +00:00
github-actions[bot]
f86d16adba
New Crowdin Translations (automated) (#31536)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-22 08:04:56 +00:00
Claire
2da687a28b
Remove dead CSS code (#31527) 2024-08-21 17:05:02 +00:00
Claire
edeae945c0
Remove fontawesome leftovers (#31525) 2024-08-21 15:55:35 +00:00
Claire
19a1acb38b
Add api_versions to /api/v2/instance (#31354) 2024-08-21 14:54:59 +00:00
Claire
d67e11733e
Add automatic notification polling for grouped notifications (#31513) 2024-08-21 14:41:31 +00:00
KMY(雪あすか)
01a757d306
Fix boost dialog visibility selection not being taken into account (#31523) 2024-08-21 13:11:36 +00:00
Claire
e48a64d3b5
Fix distracting and confusing always-showing scrollbar track in boost confirmation modal (#31524) 2024-08-21 10:25:14 +00:00
mogaminsk
8c7642cd18
Change translation strings of grouped notification label to have full context (#31486) 2024-08-21 08:56:36 +00:00
Claire
b91264b1f3
Update dependency fugit (#31522) 2024-08-21 08:15:12 +00:00
github-actions[bot]
3994195fea
New Crowdin Translations (automated) (#31521)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-21 07:19:16 +00:00
Eugen Rochko
b06c7b6b5a
Change hints for missing remote content in web UI (#31516) 2024-08-21 07:08:58 +00:00
Claire
9ba7c90151
Fix non-grouped notifications not loading on page load (#31514) 2024-08-20 16:18:33 +00:00
Matt Jankowski
f91403ccaa
Remove unused fa_icon helper method (#31511) 2024-08-20 15:59:59 +00:00
Matt Jankowski
4c4bcad866
Remove font-awesome npm package (#31512) 2024-08-20 15:59:47 +00:00
Renaud Chaput
4016fd5128
Update CSS color variables to match our latest design system (#31510) 2024-08-20 15:56:44 +00:00
renovate[bot]
6c26fb0ed6
Update dependency core-js to v3.38.1 (#31507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-20 14:36:14 +00:00
Eugen Rochko
05b4380051
Fix margin in follow notification in web UI (#31508) 2024-08-20 14:03:09 +00:00
Claire
711e1fce0a
Fix pagination parameters in GET /api/v2_alpha/notificatins (#31509) 2024-08-20 13:54:08 +00:00
Matt Jankowski
fa2e7b1708
Migrate fontawesome->material in more views (#31360) 2024-08-20 11:52:45 +00:00
Matt Jankowski
3be1b1eaae
Use material symbol for filters/statuses views (#31371) 2024-08-20 11:52:27 +00:00
Matt Jankowski
71405eabfc
Use material symbols in table link helper (#31359) 2024-08-20 11:48:15 +00:00
github-actions[bot]
c75f7262ed
New Crowdin Translations (automated) (#31504)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-20 07:59:21 +00:00
Michael Stanclift
2f01f5f4ea
Harmonize persistent scrollbar styles (#31445) 2024-08-20 07:48:51 +00:00
Renaud Chaput
98bf2fc27c
Improve the list selection UI for notification requests (#31457) 2024-08-19 22:11:58 +00:00
Michael Stanclift
a7a2ff6c1d
Use Dockerfile ADD to obtain ffmpeg and libvips source (#31498) 2024-08-19 19:44:28 +00:00
Claire
53c183f899
Reload notifications when accepted notifications are merged (streaming only) (#31419) 2024-08-19 15:59:06 +00:00
Edward Moulsdale
d4f135bc6d
Fix in memoriam accounts appearing in follow recommendations (#31474)
Co-authored-by: Utkarsh Wankar <46633523+kernal053@users.noreply.github.com>
2024-08-19 15:55:42 +00:00
Claire
d2e4be0456
Hopefully fix notifications pagination flaky test (#31494) 2024-08-19 14:53:54 +00:00
Claire
1e612c5a09
Refactor some actions to be proper async actions instead of passing a continuation (#31453) 2024-08-19 14:41:32 +00:00
Renaud Chaput
40f6631ac9
Fix Husky git hooks not being installed anymore (#31435) 2024-08-19 13:55:18 +00:00
Emelia Smith
99051479c1
Fix Trending Tags pending review having an unstable sort order (#31473)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-08-19 13:25:40 +00:00
Emelia Smith
71f257d529
Disable rule selector in ReportReasonSelector if instance has no rules (#31458) 2024-08-19 12:28:06 +00:00
renovate[bot]
31d374e5f5
Update opentelemetry-ruby (non-major) (#31451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 11:42:33 +00:00
renovate[bot]
3db3e3dae5
Update dependency postcss-preset-env to v10.0.2 (#31488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 08:56:08 +00:00
github-actions[bot]
c4e0a7dced
New Crowdin Translations (automated) (#31463)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-19 08:29:48 +00:00
renovate[bot]
9cda8c0197
Update dependency use-debounce to v10.0.3 (#31468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 08:12:01 +00:00
renovate[bot]
d57ee31413
Update devDependencies (non-major) (#31489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 08:10:15 +00:00
renovate[bot]
d6404714b6
Update eslint (non-major) (#31490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 08:10:13 +00:00
Claire
2d589a5ed4
Fix incorrect activity matching in notification unfiltering worker (#31456) 2024-08-16 17:11:23 +00:00
Claire
1acbb628b9
Update changelog for v4.2.11 (#31452) 2024-08-16 12:52:57 +00:00
renovate[bot]
367d3841af
Update dependency aws-sdk-s3 to v1.158.0 (#31447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-16 10:34:40 +00:00
renovate[bot]
a2dfe5a779
Update dependency test-prof to v1.4.0 (#31431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-16 10:33:24 +00:00
Renaud Chaput
32a78e56e0
Add a follow button & follower count on follow notifications (#31433) 2024-08-16 10:00:59 +00:00
renovate[bot]
622628a199
Update dependency rspec-rails to v6.1.4 (#31442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-16 10:00:15 +00:00
renovate[bot]
a0afbba54d
Update dependency sanitize to v6.1.3 (#31434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-16 09:54:27 +00:00
renovate[bot]
78d08da1fb
Update opentelemetry-ruby (non-major) (#31446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-16 09:53:33 +00:00
github-actions[bot]
9a24552732
New Crowdin Translations (automated) (#31440)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-16 09:11:33 +00:00
Renaud Chaput
5d04e29775
Disable Chrome's search engine selection screen when running system specs (#31437) 2024-08-15 06:45:00 +00:00
Renaud Chaput
4545b8d6cb
Add the ability to ignore specific browser errors for a spec (#31436) 2024-08-15 06:44:13 +00:00
Michael Stanclift
9194197de5
Modify profile directory page image borders (#31391) 2024-08-14 19:23:45 +00:00
Michael Stanclift
a1ddaa9ee9
Fix removal of border from account listing (#31432) 2024-08-14 17:09:23 +00:00
renovate[bot]
3cc2968344
Update dependency wicg-inert to v3.1.3 (#31423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 14:55:01 +00:00
renovate[bot]
5c91610e53
Update dependency postcss-preset-env to v10.0.1 (#31422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 14:23:17 +00:00
renovate[bot]
b5e799d165
Update dependency postcss to v8.4.41 (#31420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 13:12:16 +00:00
renovate[bot]
b5ad4aae23
Update dependency propshaft to v0.9.1 (#31421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 12:48:01 +00:00
renovate[bot]
5b2750e384
Update dependency libvips to v8.15.3 (#31416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 09:45:14 +00:00
renovate[bot]
4fdaf4513d
Update dependency oj to v3.16.5 (#31417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 09:21:51 +00:00
Jeong Arm
95c11d365d
Don't push to cache buster if attachment path is nil (#31414) 2024-08-14 08:57:42 +00:00
renovate[bot]
8d96907933
Update dependency @types/webpack to v4.41.39 (#31379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 07:35:33 +00:00
Claire
7996a9543d
Change notification request acceptance to immediately delete the request (#31256) 2024-08-14 07:34:30 +00:00
renovate[bot]
3e3450be36
Update dependency cssnano to v7.0.5 (#31413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 07:33:32 +00:00
renovate[bot]
ee6d3a83f1
Update dependency concurrent-ruby to v1.3.4 (#31412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 07:32:19 +00:00
github-actions[bot]
202f43ccc6
New Crowdin Translations (automated) (#31411)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-14 07:04:01 +00:00
Renaud Chaput
feaf2f2437
Update renovate config to fix #31379 (#31408) 2024-08-14 06:53:12 +00:00
Renaud Chaput
0d85a79f19
Fix notification screen crashing in rare cases where the status no longer exists (#31403) 2024-08-14 06:47:15 +00:00
renovate[bot]
6bd7da72e9
Update crowdin/github-action action to v2 (#31380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 22:28:13 +00:00
Michael Stanclift
7d4b602cb2
Fix collapsable column borders in Safari and improve Safari scrollbars (#31389) 2024-08-13 21:54:03 +00:00
renovate[bot]
d767439205
Update dependency axios to v1.7.4 [SECURITY] (#31409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 21:53:36 +00:00
Matt Jankowski
2e85167188
Only run PR rebase needed check on non-fork repos (#31401) 2024-08-13 21:37:26 +00:00
renovate[bot]
3a787d785c
Update docker/build-push-action action to v6 (#31381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 20:09:38 +00:00
Claire
12582d095e
Fix styling of notification browser permission denied banner (#31406) 2024-08-13 19:32:27 +00:00
Renaud Chaput
c3e1d86d58
Fix log out from user menu not working on Safari (#31402) 2024-08-13 17:49:23 +00:00
Claire
2b1670da48
Fix modal border and padding inconsistencies (#31399) 2024-08-13 17:48:28 +00:00
Claire
2ed13071ef
Fix what was intended as a comment being interpreted as strings (#31398) 2024-08-13 08:48:10 +00:00
github-actions[bot]
862e793b45
New Crowdin Translations (automated) (#31394)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-13 07:42:08 +00:00
Matt Jankowski
02df1b4e4a
Finish email allow/deny list naming migration (#30530) 2024-08-13 07:37:32 +00:00
Renaud Chaput
f6d090fdf5
Change avatars border radius (#31390) 2024-08-13 07:26:23 +00:00
Renaud Chaput
7e501c59c2
Add a border above the timeline hint for statuses with replies (#31387) 2024-08-13 07:14:41 +00:00
Emelia Smith
b42661ba95
Add missing Appeals link under Moderation in navigiation (#31071) 2024-08-12 14:45:30 +00:00
Emelia Smith
a1f723a6a4
Add support for PKCE Extension in OmniAuth OIDC (#31131) 2024-08-12 14:32:52 +00:00
Claire
a7b718c31a
Fix all streamed notification types being stored without filtering (#31384) 2024-08-12 13:40:29 +00:00
Django
709dcd07f2
Add support for incoming <s> tag (#31375) 2024-08-12 11:10:05 +00:00
github-actions[bot]
3abb5c0eaa
New Crowdin Translations (automated) (#31382)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-12 07:22:50 +00:00
Michael Stanclift
fbdda9f6c1
Fix account header avatar border (#31373) 2024-08-11 21:32:22 +00:00
Michael Stanclift
7365a13ee7
Revert Safari scrollbar changes (#31372) 2024-08-11 21:30:52 +00:00
Essem
2e03dc9fbf
Fix extra slash on notification policy request (#31378) 2024-08-11 21:28:43 +00:00
github-actions[bot]
85152af95f
New Crowdin Translations (automated) (#31377)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-11 18:51:39 +00:00
github-actions[bot]
634ab0bad0
New Crowdin Translations (automated) (#31351)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-11 18:13:10 +00:00
Renaud Chaput
19dbc74d82
Fix typos in localisation from #31342 (#31369) 2024-08-10 18:14:37 +00:00
Robin
c39a8e1061
Change “e-mail” to “email” in en.yml (#31317) 2024-08-10 09:32:57 +00:00
Emelia Smith
d3089018d5
Move Follow Recommendations to Trends menu, rename to Recommendations & Trends (#31292) 2024-08-10 09:17:10 +00:00
Matt Jankowski
885e67b2ed
Use material symbol for modal layout sign out link (#31363) 2024-08-10 09:13:08 +00:00
Matt Jankowski
7519ef2fa1
Use material symbol for responsive menu toggle (#31362) 2024-08-10 09:12:47 +00:00
Michael Stanclift
caf0164f00
Improve legibility of account/hashtag autosuggest (#31361) 2024-08-10 09:11:34 +00:00
Claire
28411acebb
Fix “Accept all”/“Dismiss all” notification requests not working (#31358) 2024-08-09 16:40:15 +00:00
Travis Briggs
bcec8f53a4
Add hint to user that other remote statuses may not be displayed (#26910) 2024-08-09 16:12:51 +00:00
Claire
658addcbf7
Add ability to report, block and mute from notification requests list (#31309)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2024-08-09 14:56:39 +00:00
Claire
eaedd52def
Fix incorrect rate limit on PUT requests (#31356) 2024-08-09 14:48:05 +00:00
Claire
1701575704
Add option to ignore filtered notifications to the web interface (#31342) 2024-08-09 14:21:55 +00:00
Claire
8a5b57f668
Revert "Support JSON-LD named graph (#31288)" (#31355) 2024-08-09 13:48:54 +00:00
Claire
cbdd8edf68
Revamp notification policy options (#31343) 2024-08-09 13:30:55 +00:00
Christian Schmidt
e29c401f77
Add lang attribute on preview card title (#31303) 2024-08-09 13:05:34 +00:00
Claire
994ef16b72
Bust CDN cache on media deletion (#31353) 2024-08-09 12:48:34 +00:00
Jeong Arm
9bae237792
Change confirmation prompt on trending management (#19626) 2024-08-09 12:47:02 +00:00
Michael Stanclift
9538d9c298
Fix post filter & report styling (#31349) 2024-08-09 12:45:39 +00:00
Claire
6ca731e9b6
Change unread notification count to only cover the selected notification type (#31326) 2024-08-08 20:20:52 +00:00
Renaud Chaput
2095d0f2b0
Update notification labels for mentions (#31304) 2024-08-08 20:20:35 +00:00
renovate[bot]
6e01a23e3b
Update dependency eslint-plugin-promise to v7 (#31120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-08 19:31:00 +00:00
renovate[bot]
f045ef8e92
Update dependency eslint-plugin-jsdoc to v50 (#31330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-08 19:30:42 +00:00
Michael Stanclift
389549e783
Fix list creation textbox styling (#31348) 2024-08-08 19:23:15 +00:00
Renaud Chaput
a207a1f7dc
Disable stylelint rules that are conflicting with Prettier (#31339) 2024-08-08 19:22:16 +00:00
Michael Stanclift
664bef3573
Fix styling issues with notification settings and mobile borders (#31346) 2024-08-08 17:31:06 +00:00
Claire
079d681ac6
Fix uninitialized grouped notifications read marker on initial load (#31347) 2024-08-08 17:00:05 +00:00
github-actions[bot]
0e4d3c0403
New Crowdin Translations (automated) (#31337)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-08 13:54:10 +00:00
Claire
2edae5ea28
Convert PrivacyDropdownMenu to Typescript and generalize it to DropdownSelector component (#31338) 2024-08-08 10:57:21 +00:00
Valtteri Laitinen
a95fe931d7
Really fix duplicate border around search results (#31305) 2024-08-08 09:22:48 +00:00
Michael Stanclift
176ba984d4
Follow up tweaking of admin UI changes part 2 (#31268) 2024-08-08 09:21:54 +00:00
Claire
ac9040b76b
Add explainer for filtered notifications from limited accounts (#31331) 2024-08-08 09:16:33 +00:00
Claire
f8629367d2
Add unchangeable filtered notification setting for limited accounts (#31324) 2024-08-07 23:09:30 +00:00
Matt Jankowski
84c3cc4e5b
Replace more font-awesome icons in views/admin area (#30961) 2024-08-07 23:08:36 +00:00
Matt Jankowski
52a626f39b
Replace more font-awesome icons in app/helpers (#30962) 2024-08-07 22:53:15 +00:00
Matt Jankowski
e4092cc73a
Replace more font-awesome icons in views/settings (#30963) 2024-08-07 22:52:35 +00:00
Matt Jankowski
abc4ed48f2
Replace more font-awesome icons in navigation sidebar area (#30974) 2024-08-07 22:52:08 +00:00
Claire
670e4655d1
Change dismissing a notification to clear existing filtered notifications for that account (#31329) 2024-08-07 15:14:37 +00:00
Claire
6f285bb2a6
Change group notifications unread markers to only be cleared when focusing/navigating again (#31325) 2024-08-07 11:12:42 +00:00
Claire
af2aec1a82
Change notification requests to only count mentions (#31322) 2024-08-07 11:12:36 +00:00
github-actions[bot]
fc964c9bfd
New Crowdin Translations (automated) (#31321)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-07 09:32:16 +00:00
Claire
719cabe024
Fix logic of block/mute bypass for mentions from moderators (#31271) 2024-08-07 06:52:10 +00:00
Claire
c8b9e60ec1
Fix mutes and blocks not immediately cleaning up notification requests in Web UI (#31316) 2024-08-06 13:35:15 +00:00
Claire
30430a62e2
Fix broken “mute” button in mutes list (#31315) 2024-08-06 13:34:55 +00:00
Renaud Chaput
57a1f86626
Remove usage of non-public RTK types (#31312) 2024-08-06 12:22:18 +00:00
Claire
438dac99d6
Add option to request partial accounts in grouped notifications API (#31299) 2024-08-06 12:09:35 +00:00
Claire
5d890ebc57
Delete pending notification requests on block and notification mutes (#31310) 2024-08-06 10:07:16 +00:00
Christian Schmidt
9d0cafd06b
Check Content-Length in ResponseWithLimitAdapter (#31285) 2024-08-06 08:23:48 +00:00
renovate[bot]
103e544cfa
fix(deps): update dependency @reduxjs/toolkit to v2.2.7 - abandoned (#31173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2024-08-06 07:54:00 +00:00
Christian Schmidt
e9e63ed203
Support JSON-LD named graph (#31288) 2024-08-06 07:52:47 +00:00
renovate[bot]
135493c0b4
chore(deps): update dependency rubocop-rspec to v3.0.4 (#31301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-06 07:47:03 +00:00
github-actions[bot]
d15f46a0b8
New Crowdin Translations (automated) (#31307)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-06 07:22:38 +00:00
Renaud Chaput
97b9e8befd
Add account notes to hover cards (#31300) 2024-08-06 06:53:59 +00:00
Valtteri Laitinen
c01a6a6eeb
Improve look of verified header fields in profile (#31275) 2024-08-05 11:35:16 +00:00
renovate[bot]
656373817e
chore(deps): update dependency bootsnap to v1.18.4 (#31297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 11:30:35 +00:00
github-actions[bot]
cb56a6dc7e
New Crowdin Translations (automated) (#31276)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-05 07:43:04 +00:00
renovate[bot]
a1eaf12d63
chore(deps): update dependency fog-core to '<= 2.5.0' (#31267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 07:14:05 +00:00
renovate[bot]
02c2222879
fix(deps): update dependency postcss-preset-env to v10 (#31279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 07:13:47 +00:00
renovate[bot]
29d6310b80
chore(deps): update dependency sidekiq-scheduler to v5.0.6 (#31245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 07:13:14 +00:00
renovate[bot]
9e440bee19
chore(deps): update dependency aws-sdk-s3 to v1.157.0 (#31252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 07:13:00 +00:00
renovate[bot]
176d1feadf
chore(deps): update yarn to v4.4.0 (#31274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 07:12:47 +00:00
renovate[bot]
3a368933b4
fix(deps): update dependency core-js to v3.38.0 (#31290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 07:11:57 +00:00
renovate[bot]
f33049ffe4
chore(deps): update dependency ffmpeg to v7.0.2 (#31289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 06:29:38 +00:00
renovate[bot]
e99c0e61ea
chore(deps): update dependency @types/ws to v8.5.12 (#31293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 06:29:03 +00:00
renovate[bot]
3fec1baefd
chore(deps): update devdependencies (non-major) (#31294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 06:28:22 +00:00
Valtteri Laitinen
315308b10c
Fix duplicate border around search results (#31284) 2024-08-04 15:14:11 +00:00
Valtteri Laitinen
b6c1b8f748
Fix missing border around tab/filter bar (#31286) 2024-08-04 15:13:15 +00:00
Christian Schmidt
3d6e8d6834
Disable irrelevant fields unless cleanup is enabled (#26562) 2024-08-04 08:57:46 +00:00
Renaud Chaput
19f4aa1147
Fix typo in english message (filtred => filtered) (#31273) 2024-08-02 19:55:13 +00:00
Claire
ad95c98054
Further de-emphasize filtered notifications banner and add setting to minimize it (#31250) 2024-08-02 14:59:37 +00:00
Claire
2ec1181ee5
Fix contrast between background and form elements on some pages (#31266) 2024-08-02 13:55:31 +00:00
Renaud Chaput
cc453f2221
Change the filtered notification count to be in the account avatar (#31262) 2024-08-02 13:18:19 +00:00
Renaud Chaput
99c446b7cf
Change PostCSS config to allow logical properties (#31264) 2024-08-02 13:17:36 +00:00
Claire
bb3941f885
Change DropdownMenu component's items prop type (#31263) 2024-08-02 13:15:18 +00:00
Claire
2a704add90
Fix status processing failing halfway when a remote post has a malformed replies attribute (#31246) 2024-08-02 10:39:12 +00:00
renovate[bot]
6a27a8c3e7
fix(deps): update babel monorepo (#31167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-02 10:01:41 +00:00
github-actions[bot]
caeae25b55
New Crowdin Translations (automated) (#31261)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-02 07:01:04 +00:00
Emelia Smith
7490019535
Fix support for IPv6 redis connections in streaming (#31229) 2024-08-02 06:56:09 +00:00
Emelia Smith
f55099fea3
Increase maximum page size for Admin Domain Management APIs (#31253) 2024-08-02 06:54:48 +00:00
renovate[bot]
d19edc4c4a
fix(deps): update dependency axios to v1.7.3 (#31248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-02 06:53:41 +00:00
Claire
afdfeb5856
Fix click event handling when clicking outside of an open dropdown menu (#31251) 2024-08-01 20:50:36 +00:00
Claire
da5b45a573
Add endpoints to accept and dismiss multiple notification requests at once (#31242) 2024-08-01 12:36:54 +00:00
github-actions[bot]
584b1524dd
New Crowdin Translations (automated) (#31237)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-08-01 09:27:24 +00:00
Christian Schmidt
4853a2bada
Add Accept-Language header when fetching preview card (#31232) 2024-08-01 09:14:24 +00:00
renovate[bot]
06c3a41faf
chore(deps): update dependency rubocop to v1.65.1 (#31239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-01 09:13:10 +00:00
Claire
f6dc8ae8e3
Fix handling of long display names in notification requests (#31225) 2024-08-01 07:41:32 +00:00
Claire
a50c8e951f
Fix issue with grouped notifications UI due to recent API change (#31224) 2024-07-31 13:23:08 +00:00
Claire
2c1e75727d
Change filtered notification banner design to take up less space (#31222) 2024-07-31 12:36:08 +00:00
Claire
549ab089ee
Change grouped notifications API shape (take 2) (#31214) 2024-07-31 10:50:13 +00:00
Claire
288961bbb9
Change private mention icon for consistency in grouped notifications (#31223) 2024-07-31 10:26:43 +00:00
github-actions[bot]
520ff04248
New Crowdin Translations (automated) (#31219)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-31 08:09:49 +00:00
renovate[bot]
66a16bf49c
chore(deps): update dependency opentelemetry-instrumentation-active_job to v0.7.4 (#31218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-31 08:01:07 +00:00
Claire
fd1bade135
Fix inconsistent dismiss icon for notification requests (#31220) 2024-07-31 07:52:59 +00:00
Claire
4eab6aef95
Change User-Agent to use Mastodon as the product, and http.rb as platform details (#31192) 2024-07-30 21:40:42 +00:00
Claire
598ae4f2da
Add endpoints for unread notifications count (#31191) 2024-07-30 08:39:11 +00:00
Tim Rogers
2ce99c51dd
Fixed Rails route covering %40-encoded profile URL paths to not 404 (#31184) 2024-07-30 08:18:00 +00:00
github-actions[bot]
7d11a6c9b0
New Crowdin Translations (automated) (#31206)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-30 08:15:40 +00:00
renovate[bot]
e1f7ca7ced
chore(deps): update dependency json-ld to v3.3.2 (#31205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-30 08:00:33 +00:00
Matt Jankowski
7e33583daa
Dont run crowdin upload workflow on forks (#31195) 2024-07-29 17:19:05 +00:00
renovate[bot]
1229d2907a
chore(deps): update docker/dockerfile docker tag to v1.9 (#31187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 16:00:55 +00:00
Emelia Smith
c40e481169
Implement UI for Admin Search of Hashtags (#30880) 2024-07-29 15:49:44 +00:00
renovate[bot]
6d2ed0dcba
chore(deps): update dependency pg to v1.5.7 (#31176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 15:00:01 +00:00
Michael Stanclift
91fbd5b422
Adjust magick version detection in admin panel (#30845) 2024-07-29 14:32:29 +00:00
renovate[bot]
8edd77a755
chore(deps): update dependency typescript to v5.5.4 (#31186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 09:07:22 +00:00
Matt Jankowski
95eadabe44
Extrude fill_in_auth_details method to ProfileStories spec support module (#31163) 2024-07-29 08:31:42 +00:00
kyori19
6ad477d146
Fix Codespaces startup fails due to Corepack download prompt (#31189) 2024-07-29 08:27:11 +00:00
github-actions[bot]
ec5a860924
New Crowdin Translations (automated) (#31169)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-29 08:24:15 +00:00
Michael Stanclift
848a5ca53d
Follow up tweaking of admin UI changes (#31168) 2024-07-29 08:10:23 +00:00
renovate[bot]
37f5718482
chore(deps): update dependency sanitize to v6.1.2 (#31172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 08:08:07 +00:00
renovate[bot]
4cc634dc44
fix(deps): update dependency use-debounce to v10.0.2 (#31177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 08:07:28 +00:00
renovate[bot]
cefd8b5484
chore(deps): update dependency husky to v9.1.3 (#31185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 08:06:53 +00:00
renovate[bot]
90d6297191
chore(deps): update eslint (non-major) (#31188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 08:06:12 +00:00
David Roetzel
dd53792714
Move UI tests from controller to system spec (#31158) 2024-07-26 14:53:49 +00:00
Michael Stanclift
b120792f27
Adjust ffmpeg version detection in admin panel (#31130)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-07-26 14:13:56 +00:00
David Roetzel
dfd43869c9
Decrease count of filtered notifications when notification requests are accepted or dismissed (#31149) 2024-07-26 09:36:54 +00:00
renovate[bot]
c091fa7105
chore(deps): update devdependencies (non-major) (#31096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 09:27:48 +00:00
Emelia Smith
693d9b03ed
Enable OAuth PKCE Extension (#31129) 2024-07-26 08:53:10 +00:00
github-actions[bot]
3793c845c9
New Crowdin Translations (automated) (#31154)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-26 07:51:37 +00:00
Eugen Rochko
8818748b90
Change design of confirmation modals in web UI (#30884)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2024-07-25 17:05:54 +00:00
renovate[bot]
ff6d2ec343
fix(deps): update dependency pino to v9.3.2 (#31148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 15:38:05 +00:00
Adam Niedzielski
887e64efd4
Allow @ at the end of an URL (#31124) 2024-07-25 15:02:58 +00:00
Matt Jankowski
5f5373397f
Remove duplicate omniauth_only? helper method (#31066) 2024-07-25 14:59:56 +00:00
Adam Niedzielski
fbf95216c5
Fix LinkCrawlWorker NoMethodError: undefined method `[]' for nil (#31144) 2024-07-25 14:51:44 +00:00
Matt Jankowski
bfaa53343a
Improve failure message on pagination matcher (#31146) 2024-07-25 14:28:08 +00:00
Adam Niedzielski
337a7fcf30
Fix ß bug in regexp for mentions and tags (#31122) 2024-07-25 14:24:19 +00:00
Matt Jankowski
e881a59671
Add User.unconfirmed scope, reduce factories in scheduler/user_cleanup spec (#31063) 2024-07-25 14:18:24 +00:00
Matt Jankowski
34626b8291
Update schema to reflect full db:migrate run results (#31128) 2024-07-25 10:37:48 +00:00
renovate[bot]
4d374cf730
chore(deps): update dependency opentelemetry-exporter-otlp to v0.28.1 (#31139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 10:00:06 +00:00
renovate[bot]
efa7ad9d75
chore(deps): update dependency node to 20.16 (#31127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 07:44:54 +00:00
github-actions[bot]
ef0e3e447a
New Crowdin Translations (automated) (#31119)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-25 07:44:00 +00:00
renovate[bot]
9c39fb52f3
chore(deps): update opentelemetry-ruby (non-major) (#31138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 07:21:01 +00:00
renovate[bot]
8117977c88
fix(deps): update dependency postcss to v8.4.40 (#31137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 07:20:55 +00:00
renovate[bot]
9e04007020
chore(deps): update opentelemetry-ruby (non-major) (#31126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 19:27:39 +00:00
Claire
3a49687ca0
Fix performance issue by using LATERAL in group notification CTE (#31123) 2024-07-24 18:59:15 +00:00
renovate[bot]
06f070d86d
fix(deps): update dependency fuzzysort to v3 (#30315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2024-07-24 08:13:09 +00:00
Michael Stanclift
8b7f93cc7f
Recolor Preferences/Admin UI to better match main design (#31034) 2024-07-23 15:30:45 +00:00
Adam Niedzielski
cd0ca4b994
Select correct self link when parsing Webfinger response (#31110) 2024-07-23 14:42:31 +00:00
Claire
a8330be93e
Widen the clickable area for statuses in grouped notifications (#31111) 2024-07-23 12:11:08 +00:00
renovate[bot]
871b6197df
chore(deps): update dependency public_suffix to v6.0.1 (#31109)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-23 08:59:18 +00:00
Emelia Smith
bb2e5a4b58
Add audit logs to Admin::InstancesController (#27386) 2024-07-23 08:12:30 +00:00
Matt Jankowski
1b839d2cba
Fix mastodon:stats decoration of stats rake task (#31104) 2024-07-23 06:51:57 +00:00
Nick Schonning
9a015a2a15
Update Prettier to 3.3.3 (#31106) 2024-07-23 06:46:09 +00:00
renovate[bot]
44bdfe1fed
chore(deps): update dependency opentelemetry-instrumentation-active_job to v0.7.3 (#31107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-23 06:42:28 +00:00
Claire
af06d74574
Fix keyboard shortcuts and navigation in grouped notifications (#31076) 2024-07-23 06:20:17 +00:00
Claire
55705d8191
Fix reblogging after refactor (#31105) 2024-07-22 19:30:06 +00:00
Claire
9cb9427133
Refactor CW handling in an action (#31103) 2024-07-22 16:03:58 +00:00
Claire
03dea64b79
Refactor code for reblogging and favouriting (#31102) 2024-07-22 15:45:07 +00:00
renovate[bot]
80d9bff6df
fix(deps): update dependency immutable to v4.3.7 (#31100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 13:39:37 +00:00
Claire
ced5e853c7
Fix duplicate orderedItems in user archive's outbox.json (#31099) 2024-07-22 08:56:05 +00:00
Matt Jankowski
6e4305de69
Fix spec descriptions around configurable limit values (#31079) 2024-07-22 08:02:31 +00:00
github-actions[bot]
5a60a3b80c
New Crowdin Translations (automated) (#31087)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-22 08:01:24 +00:00
Matt Jankowski
ef796446e5
Ignore manifest JS parse/interrupt error on CI (#31080) 2024-07-22 07:46:41 +00:00
renovate[bot]
6ee5d3b559
fix(deps): update dependency jsdom to v24.1.1 (#31088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 07:39:56 +00:00
renovate[bot]
22d2fba279
chore(deps): update definitelytyped types (non-major) (#31095)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 07:39:38 +00:00
renovate[bot]
77ec5e11e4
chore(deps): update dependency email_spec to v2.3.0 (#31097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 07:38:52 +00:00
Claire
2774980589
Fix syntax error in grouped notifications CTE on some PostgreSQL versions (#31098) 2024-07-22 07:37:27 +00:00
Matt Jankowski
0397df9bef
Resolve TODO in api/v1/admin/domain_* controllers (#31067) 2024-07-19 15:32:42 +00:00
renovate[bot]
65a8dc7869
chore(deps): update dependency selenium-webdriver to v4.23.0 (#31073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-19 15:32:06 +00:00
kyori19
3a00f89aa1
Respect Account::DISPLAY_NAME_LENGTH_LIMIT in account spec correctly (#31075) 2024-07-19 15:31:48 +00:00
Claire
e768b23aa9
Remove routerHistory parameters from composer actions (#31077) 2024-07-19 15:26:44 +00:00
Matt Jankowski
adadfdbc03
Update strong_migrations to version 2.0.0 (#31061) 2024-07-19 08:32:43 +00:00
Matt Jankowski
05342529dd
Reduce factory creation in AP activity move spec (#31064) 2024-07-19 08:31:34 +00:00
Michael Stanclift
784e088533
Fix double border on column filter on narrow screens (#31068) 2024-07-19 08:30:26 +00:00
github-actions[bot]
8e0aed8ac7
New Crowdin Translations (automated) (#31072)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-19 08:21:09 +00:00
Matt Jankowski
82344342c1
Add link to org-level contribution guidelines to contributing doc (#31043) 2024-07-18 15:45:40 +00:00
Matt Jankowski
848b59c8ae
Reduce factory creation in MediaAttachment model spec (#31058) 2024-07-18 15:23:46 +00:00
renovate[bot]
41b7281b56
fix(deps): update dependency use-debounce to v10.0.1 (#31060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 15:23:43 +00:00
Claire
6e47637dd4
Fix grouping across hourly buckets happening in a 12 seconds window instead of 12 hours window (#31062) 2024-07-18 15:23:40 +00:00
Renaud Chaput
f587ff643f
Grouped Notifications UI (#30440)
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-07-18 14:36:09 +00:00
github-actions[bot]
7d090b2ab6
New Crowdin Translations (automated) (#31055)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-18 08:00:29 +00:00
Matt Jankowski
47ea83d246
Reduce factory creation in AP::ProcessStatusUpdateService spec (#31051) 2024-07-18 08:00:19 +00:00
renovate[bot]
64c7ffdc65
chore(deps): update dependency ruby-vips to v2.2.2 (#31050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 07:53:36 +00:00
Matt Jankowski
2616fde9e6
Use change-requiring records in admin/reports controller spec (#31052) 2024-07-18 07:49:44 +00:00
Matt Jankowski
c5f8256801
Reduce extra factories in FanOutOnWriteService spec (#31053) 2024-07-18 07:45:59 +00:00
renovate[bot]
aa4d8987a0
chore(deps): update dependency faker to v3.4.2 (#31046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 10:42:54 +00:00
Claire
4957771a8a
Update rexml to version 3.3.2 (#31048) 2024-07-17 09:18:25 +00:00
Matt Jankowski
76c2c5c748
Reduce extra round trips in activitypub controller specs (#31041) 2024-07-17 08:09:34 +00:00
Matt Jankowski
f5e90f3de3
Reduce extra round trips in AP::SynchronizeFollowersService spec (#31044) 2024-07-17 07:33:08 +00:00
Matt Jankowski
81877e7950
Reduce extra round trips in AP::FetchRemoteStatusService spec (#31045) 2024-07-17 07:24:14 +00:00
github-actions[bot]
00c110ac7b
New Crowdin Translations (automated) (#31047)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-17 07:23:07 +00:00
github-actions[bot]
66430cb25c
New Crowdin Translations (automated) (#31038)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-16 15:24:56 +00:00
Adam Niedzielski
a609940ec9
Fix test description in hashtag_bar (#30827) 2024-07-16 13:23:26 +00:00
Emelia Smith
aa88aca0ad
Add optional OAuth application to reports (#30539) 2024-07-16 13:23:08 +00:00
Adam Niedzielski
fa54b61216
Handle missing links in Webfinger response (#31030) 2024-07-16 13:21:16 +00:00
renovate[bot]
c27d194eb2
fix(deps): update dependency pino to v9.3.1 (#31032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 13:20:34 +00:00
renovate[bot]
c3a2f9d993
chore(deps): update dependency json-schema to v4.3.1 (#31012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 12:14:25 +00:00
Michael Stanclift
905921b2ef
Add missing "Update Report" string in audit log (#31033) 2024-07-16 07:06:19 +00:00
David Roetzel
9d0bce4072
Move filtered notifications bar in scrollable area (#30996)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-07-15 13:42:47 +00:00
renovate[bot]
0b97ec3f7b
fix(deps): update dependency @babel/core to v7.24.9 (#31029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-15 13:03:57 +00:00
Matt Jankowski
543b59fa14
Clean up theme_helper style builders (#30617) 2024-07-15 13:03:23 +00:00
Matt Jankowski
91ef4a6fc5
Add assertion about key columns to account key generate spec (#30635) 2024-07-15 10:42:18 +00:00
Claire
1dd8262071
Add optional filtered attribute to notification entities in REST API (#31011) 2024-07-15 09:29:57 +00:00
github-actions[bot]
17117109ad
New Crowdin Translations (automated) (#31018)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-15 07:44:37 +00:00
mogaminsk
d61b536df3
Add i18n strings for instance favicon and logo settings label (#31016) 2024-07-15 07:38:48 +00:00
Matt Jankowski
00cb4a0313
Avoid repeated factory creation in media_attachments_vacuum_spec (#31000) 2024-07-12 14:09:16 +00:00
Emelia Smith
c953dca1de
Streaming: use pgPool.query instead of manually acquiring & releasing a connection (#30964) 2024-07-12 12:23:09 +00:00
renovate[bot]
b87c41115e
chore(deps): update dependency rubocop-rspec to v3.0.3 (#31009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-12 12:20:32 +00:00
David Roetzel
35a437a03f
Destroy NotificationRequests that are dismissed (#31008) 2024-07-12 12:09:52 +00:00
renovate[bot]
c929b4cace
chore(deps): update dependency pghero to v3.6.0 (#30994)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-12 09:29:43 +00:00
renovate[bot]
9cc0607358
chore(deps): update dependency database_cleaner-active_record to v2.2.0 (#31007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-12 09:29:10 +00:00
Matt Jankowski
43e24dbb13
Use include_pagination_headers in more places (#30999) 2024-07-12 08:30:52 +00:00
github-actions[bot]
28ad3588e4
New Crowdin Translations (automated) (#31006)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-07-12 08:25:27 +00:00
renovate[bot]
fdad4dc918
fix(deps): update babel monorepo to v7.24.8 (#31002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-12 08:23:04 +00:00
Nick Schonning
cd1bc94cd2
Regen RuboCop TODO for 1.65.0 (#31003) 2024-07-12 08:22:31 +00:00
Matt Jankowski
73d53827ea
Add change assertion to invites destroy spec (#31004) 2024-07-12 08:19:15 +00:00
renovate[bot]
bb702e6b20
fix(deps): update dependency sass to v1.77.8 (#31005)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-12 08:18:35 +00:00
797 changed files with 21301 additions and 10490 deletions

View file

@ -39,7 +39,7 @@
},
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"postCreateCommand": "bin/setup",
"postCreateCommand": "COREPACK_ENABLE_DOWNLOAD_PROMPT=0 bin/setup",
"waitFor": "postCreateCommand",
"customizations": {

View file

@ -87,6 +87,7 @@
},
{
// Update devDependencies every week, with one grouped PR
matchManagers: ['npm'],
matchDepTypes: 'devDependencies',
matchUpdateTypes: ['patch', 'minor'],
groupName: 'devDependencies (non-major)',
@ -95,8 +96,7 @@
{
// Group all eslint-related packages with `eslint` in the same PR
matchManagers: ['npm'],
matchPackageNames: ['eslint'],
matchPackagePrefixes: ['eslint-', '@typescript-eslint/'],
matchPackageNames: ['eslint', 'eslint-*', '@typescript-eslint/*'],
matchUpdateTypes: ['patch', 'minor'],
groupName: 'eslint (non-major)',
},
@ -112,7 +112,8 @@
},
{
// Update @types/* packages every week, with one grouped PR
matchPackagePrefixes: '@types/',
matchManagers: ['npm'],
matchPackageNames: '@types/*',
matchUpdateTypes: ['patch', 'minor'],
groupName: 'DefinitelyTyped types (non-major)',
extends: ['schedule:weekly'],
@ -129,23 +130,21 @@
{
// Group all RuboCop packages with `rubocop` in the same PR
matchManagers: ['bundler'],
matchPackageNames: ['rubocop'],
matchPackagePrefixes: ['rubocop-'],
matchPackageNames: ['rubocop', 'rubocop-*'],
matchUpdateTypes: ['patch', 'minor'],
groupName: 'RuboCop (non-major)',
},
{
// Group all RSpec packages with `rspec` in the same PR
matchManagers: ['bundler'],
matchPackageNames: ['rspec'],
matchPackagePrefixes: ['rspec-'],
matchPackageNames: ['rspec', 'rspec-*'],
matchUpdateTypes: ['patch', 'minor'],
groupName: 'RSpec (non-major)',
},
{
// Group all opentelemetry-ruby packages in the same PR
matchManagers: ['bundler'],
matchPackagePrefixes: ['opentelemetry-'],
matchPackageNames: ['opentelemetry-*'],
matchUpdateTypes: ['patch', 'minor'],
groupName: 'opentelemetry-ruby (non-major)',
},

View file

@ -85,7 +85,7 @@ jobs:
tags: ${{ inputs.tags }}
labels: ${{ inputs.labels }}
- uses: docker/build-push-action@v5
- uses: docker/build-push-action@v6
with:
context: .
file: ${{ inputs.file_to_build }}

View file

@ -26,7 +26,7 @@ jobs:
# Download the translation files from Crowdin
- name: crowdin action
uses: crowdin/github-action@v1
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false

View file

@ -19,13 +19,14 @@ on:
jobs:
upload-translations:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: crowdin action
uses: crowdin/github-action@v1
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false

View file

@ -10,6 +10,7 @@ permissions:
jobs:
label-rebase-needed:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}

2
.nvmrc
View file

@ -1 +1 @@
20.15
20.17

View file

@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.64.1.
# using RuboCop version 1.65.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@ -14,7 +14,7 @@ Lint/NonLocalExitFromIterator:
Metrics/AbcSize:
Max: 82
# Configuration parameters: CountBlocks, Max.
# Configuration parameters: CountBlocks, CountModifierForms, Max.
Metrics/BlockNesting:
Exclude:
- 'lib/tasks/mastodon.rake'
@ -40,7 +40,6 @@ Style/FetchEnvVar:
- 'config/environments/production.rb'
- 'config/initializers/2_limited_federation_mode.rb'
- 'config/initializers/3_omniauth.rb'
- 'config/initializers/blacklists.rb'
- 'config/initializers/cache_buster.rb'
- 'config/initializers/devise.rb'
- 'config/initializers/paperclip.rb'

View file

@ -2,6 +2,334 @@
All notable changes to this project will be documented in this file.
## [4.3.0] - UNRELEASED
The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by @mjankowski.
### Security
- **Add confirmation interstitial instead of silently redirecting logged-out visitors to remote resources** (#27792, #28902, and #30651 by @ClearlyClaire and @Gargron)\
This fixes a longstanding open redirect in Mastodon, at the cost of added friction when local links to remote resources are shared.
### Added
- **Add experimental server-side notification grouping** (#29889, #30576, #30685, #30688, #30707, #30776, #30779, #30781, #30440, #31062, #31098, #31076, #31111, #31123, #31223, #31214, #31224, #31299, #31325, #31347, #31304, #31326, #31384, #31403, #31433, #31509, #31486, and #31513 by @ClearlyClaire, @mgmn, and @renchap)\
Group notifications of the same type for the same target, so that your notifications no longer get cluttered by boost and favorite notifications as soon as a couple of your posts get traction.\
This is done server-side so that clients can efficiently get relevant groups without having to go through numerous pages of individual notifications.\
As part of this, the visual design of the entire notifications feature has been revamped.\
This feature is intended to eventually replace the existing notifications column, but for this first beta, users will have to enable it in the “Experimental features” section of the notifications column settings.\
The API is not final yet, but it consists of:
- a new `group_key` attribute to `Notification` entities
- `GET /api/v2_alpha/notifications`: https://docs.joinmastodon.org/methods/notifications_alpha/#get-grouped
- `GET /api/v2_alpha/notifications/:group_key`: https://docs.joinmastodon.org/methods/notifications_alpha/#get-notification-group
- `POST /api/v2_alpha/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/notifications_alpha/#dismiss-group
- `GET /api/v2_alpha/notifications/:unread_count`: https://docs.joinmastodon.org/methods/notifications_alpha/#unread-group-count
- **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, and #31541 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\
The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\
This adds the following REST API endpoints:
- `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy
- `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications
- `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests
- `GET /api/v1/notifications/requests/:id`: https://docs.joinmastodon.org/methods/notifications/#get-one-request
- `POST /api/v1/notifications/requests/:id/accept`: https://docs.joinmastodon.org/methods/notifications/#accept-request
- `POST /api/v1/notifications/requests/:id/dismiss`: https://docs.joinmastodon.org/methods/notifications/#dismiss-request
- `POST /api/v1/notifications/requests/accept`: https://docs.joinmastodon.org/methods/notifications/#accept-multiple-requests
- `POST /api/v1/notifications/requests/dismiss`: https://docs.joinmastodon.org/methods/notifications/#dismiss-multiple-requests
- `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged
In addition, accepting one or more notification requests generates a new streaming event:
- `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\
Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\
Note that this does not notify remote users.\
This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`relationship_severance_event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event).
- **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\
Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\
This can be disabled in the “Animations and accessibility” section of the preferences.
- **Add "system" theme setting (light/dark theme depending on user system preference)** (#29748, #29553, #29795, #29918, #30839, and #30861 by @nshki, @ErikUden, @mjankowski, @renchap, and @vmstan)\
Add a “system” theme that automatically switch between default dark and light themes depending on the user's system preferences.\
Also changes the default server theme to this new “system” theme so that automatic theme selection happens even when logged out.
- **Add timeline of public posts about a trending link** (#30381 and #30840 by @Gargron)\
You can now see public posts mentioning currently-trending articles from people who have opted into discovery features.\
This adds a new REST API endpoint: https://docs.joinmastodon.org/methods/timelines/#link
- **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, and #30846 by @Gargron)\
This adds a mechanism to [highlight the author of news articles](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/) shared on Mastodon.\
Articles hosted outside the fediverse can indicate a fediverse author with a meta tag:
```html
<meta name="fediverse:creator" content="username@domain" />
```
On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors\
Note that this feature is still work in progress and the tagging format and verification mechanisms may change in future releases.
- **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\
In addition to email notifications, also notify users of moderation actions or warnings against them directly within the app, so they are less likely to miss important communication from their moderators.\
This adds the `moderation_warning` notification type to the REST API and streaming, with a new [`moderation_warning` attribute](https://docs.joinmastodon.org/entities/Notification/#moderation_warning).
- **Add domain information to profiles in web UI** (#29602 by @Gargron)\
Clicking the domain of a user in their profile will now open a tooltip with a short explanation about servers and federation.
- Add ability to reorder uploaded media before posting in web UI (#28456 by @Gargron)
- Add moderation interface for searching hashtags (#30880 by @ThisIsMissEm)
- Add ability for admins to configure instance favicon and logo (#30040, #30208, #30259, #30375, #30734, #31016, and #30205 by @ClearlyClaire, @FawazFarid, @JasonPunyon, @mgmn, and @renchap)\
This is also exposed through the REST API: https://docs.joinmastodon.org/entities/Instance/#icon
- Add `api_versions` to `/api/v2/instance` (#31354 by @ClearlyClaire)\
Add API version number to make it easier for clients to detect compatible features going forward.\
See API documentation at https://docs.joinmastodon.org/entities/Instance/#api-versions
- Add recent audit log entries in federation moderation interface (#27386 by @ThisIsMissEm)
- Add profile setup to onboarding in web UI (#27829, #27876, and #28453 by @Gargron)
- Add prominent share/copy button on profiles in web UI (#27865 and #27889 by @ClearlyClaire and @Gargron)
- Add optional hints for server rules (#29539 and #29758 by @ClearlyClaire and @Gargron)\
Server rules can now be broken into a short rule name and a longer explanation of the rule.\
This adds a new [`hint` attribute](https://docs.joinmastodon.org/entities/Rule/#hint) to `Rule` entities in the REST API.
- Add support for PKCE in OAuth flow (#31129 by @ThisIsMissEm)
- Add CDN cache busting on media deletion (#31353 and #31414 by @ClearlyClaire and @tribela)
- Add the OAuth application used in local reports (#30539 by @ThisIsMissEm)
- Add hint to user that other remote statuses may be missing (#26910, #31387, and #31516 by @Gargron, @audiodude, and @renchap)
- Add lang attribute on preview card title (#31303 by @c960657)
- Add check for `Content-Length` in `ResponseWithLimitAdapter` (#31285 by @c960657)
- Add `Accept-Language` header to fetch preview cards in the server's default language (#31232 by @c960657)
- Add support for PKCE Extension in OmniAuth OIDC through the `OIDC_USE_PKCE` environment variable (#31131 by @ThisIsMissEm)
- Add API endpoints for unread notifications count (#31191 by @ClearlyClaire)\
This adds the following REST API endpoints:
- `GET /api/v1/notifications/unread_count`: https://docs.joinmastodon.org/methods/notifications/#unread-count
- Add `/` keyboard shortcut to focus the search field (#29921 by @ClearlyClaire)
- Add button to view the Hashtag on the instance from Hashtags in Moderation UI (#31533 by @ThisIsMissEm)
- Add list of pending releases directly in mail notifications for version updates (#29436 and #30035 by @ClearlyClaire)
- Add “Appeals” link under “Moderation” navigation category in moderation interface (#31071 by @ThisIsMissEm)
- Add badge on account card in report moderation interface when account is already suspended (#29592 by @ClearlyClaire)
- Add admin comments directly to the `admin/instances` page (#29240 by @tribela)
- Add ability to require approval when users sign up using specific email domains (#28468, #28732, #28607, and #28608 by @ClearlyClaire)
- Add banner for forwarded reports made by remote users about remote content (#27549 by @ClearlyClaire)
- Add support HTML ruby tags in remote posts for east-asian languages (#30897 by @ThisIsMissEm)
- Add link to manage warning presets in admin navigation (#26199 by @vmstan)
- Add volume saving/reuse to video player (#27488 by @thehydrogen)
- Add Elasticsearch index size, ffmpeg and ImageMagick versions to the admin dashboard (#27301, #30710, #31130, and #30845 by @vmstan)
- Add `MASTODON_SIDEKIQ_READY_FILENAME` environment variable to use a file for Sidekiq to signal it is ready to process jobs (#30971 and #30988 by @renchap)\
In the official Docker image, this is set to `sidekiq_process_has_started_and_will_begin_processing_jobs` so that Sidekiq will touch `tmp/sidekiq_process_has_started_and_will_begin_processing_jobs` to signal readiness.
- Add `S3_RETRY_LIMIT` environment variable to make S3 retries configurable (#23215 by @smiba)
- Add `S3_KEY_PREFIX` environment variable (#30181 by @S0yKaf)
- Add support for multiple `redirect_uris` when creating OAuth 2.0 Applications (#29192 by @ThisIsMissEm)
- Add Interlingue and Interlingua to interface languages (#28630 and #30828 by @Dhghomon and @renchap)
- Add Kashubian, Pennsylvania Dutch, Vai, Jawi Malay, Mohawk and Low German to posting languages (#26024, #26634, #27136, #29098, #27115, and #27434 by @EngineerDali, @HelgeKrueger, and @gunchleoc)
- Add validations to `Web::PushSubscription` (#30540 and #30542 by @ThisIsMissEm)
- Add option to use native Ruby driver for Redis through `REDIS_DRIVER=ruby` (#30717 by @vmstan)
- Add support for libvips in addition to ImageMagick (#30090, #30590, #30597, #30632, #30857, #30869, and #30858 by @ClearlyClaire, @Gargron, and @mjankowski)\
Server admins can now use libvips as a faster and lighter alternative to ImageMagick for processing user-uploaded images.\
This requires libvips 8.13 or newer, and needs to be enabled with `MASTODON_USE_LIBVIPS=true`.\
This is enabled by default in the official Docker images, and is intended to completely replace ImageMagick in the future.
- Add active animation to header settings button (#30221, #30307, and #30388 by @daudix)
- Add OpenTelemetry instrumentation (#30130, #30322, #30353, and #30350 by @julianocosta89, @renchap, and @robbkidd)\
See https://docs.joinmastodon.org/admin/config/#otel for documentation
- Add API to get multiple accounts and statuses (#27871 and #30465 by @ClearlyClaire)\
This adds `GET /api/v1/accounts` and `GET /api/v1/statuses` to the REST API, see https://docs.joinmastodon.org/methods/accounts/#index and https://docs.joinmastodon.org/methods/statuses/#index
- Add redirection back to previous page after site upload deletion (#30141 by @FawazFarid)
- Add RFC8414 OAuth 2.0 server metadata (#29191 by @ThisIsMissEm)
- Add loading indicator and empty result message to advanced interface search (#30085 by @ClearlyClaire)
- Add `profile` OAuth 2.0 scope, allowing more limited access to user data (#29087 and #30357 by @ThisIsMissEm)
- Add the role ID to the badge component (#29707 by @renchap)
- Add diagnostic message for failure during CLI search deploy (#29462 by @mjankowski)
- Add pagination `Link` headers on API accounts/statuses when pinned true (#29442 by @mjankowski)
- Add support for specifying custom CA cert for Elasticsearch through `ES_CA_FILE` (#29122 and #29147 by @ClearlyClaire)
- Add groundwork for annual reports for accounts (#28693 by @Gargron)\
This lays the groundwork for a “year-in-review”/“wrapped” style report for local users, but is currently not in use.
- Add notification email on invalid second authenticator (#28822 by @ClearlyClaire)
- Add new emojis from `jdecked/twemoji` 15.0 (#28404 by @TheEssem)
- Add configurable error handling in attachment batch deletion (#28184 by @vmstan)\
This makes the S3 batch size configurable through the `S3_BATCH_DELETE_LIMIT` environment variable (defaults to 1000), and adds some retry logic, configurable through the `S3_BATCH_DELETE_RETRY` environment variable (defaults to 3).
- Add VAPID public key to instance serializer (#28006 by @ThisIsMissEm)
- Add `nodeName` and `nodeDescription` to nodeinfo `metadata` (#28079 by @6543)
- Add Thai diacritics and tone marks in `HASHTAG_INVALID_CHARS_RE` (#26576 by @ppnplus)
- Add variable delay before link verification of remote account links (#27774 by @ClearlyClaire)
- Add support for invite codes in the registration API (#27805 by @ClearlyClaire)
- Add HTML lang attribute to preview card descriptions (#27503 by @srapilly)
- Add display of relevant account warnings to report action logs (#27425 by @ClearlyClaire)
- Add validation of allowed schemes on preview card URLs (#27485 by @mjankowski)
- Add token introspection without read scope to `/api/v1/apps/verify_credentials` (#27142 by @ThisIsMissEm)
- Add support for cross-origin request to `/nodeinfo/2.0` (#27413 by @palant)
- Add variable delay before link verification of remote account links (#27351 by @ClearlyClaire)
- Add PWA shortcut to `/explore` page (#27235 by @jake-anto)
### Changed
- **Change icons throughout the web interface** (#27385, #27539, #27555, #27579, #27700, #27817, #28519, #28709, #28064, #28775, #28780, #27924, #29294, #29395, #29537, #29569, #29610, #29612, #29649, #29844, #27780, #30974, #30963, #30962, #30961, #31362, #31363, #31359, #31371, #31360, #31512, #31511, and #31525 by @ClearlyClaire, @Gargron, @arbolitoloco1, @mjankowski, @nclm, @renchap, @ronilaukkarinen, and @zunda)\
This changes all the interface icons from FontAwesome to Material Symbols for a more modern look, consistent with the official Mastodon Android app.\
In addition, better care is given to pixel alignment, and icon variants are used to better highlight active/inactive state.
- **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, and #29659 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\
The compose form has been completely redesigned for a more modern and consistent look, as well as spelling out the chosen privacy setting and language name at all times.\
As part of this, the “Unlisted” privacy setting has been renamed to “Quiet public”.
- **Change design of confirmation modals in the web UI** (#29576, #29614, #29640, #29644, #30131, #30884, and #31399 by @ClearlyClaire, @Gargron, and @tribela)\
The mute, block, and domain block confirmation modals have been completely redesigned to be clearer and include more detailed information on the action to be performed.\
They also have a more modern and consistent design, along with other confirmation modals in the application.
- **Change colors throughout the web UI** (#29522, #29584, #29653, #29779, #29803, #29809, #29808, #29828, #31034, #31168, #31266, #31348, #31349, #31361, and #31510 by @ClearlyClaire, @Gargron, @renchap, and @vmstan)
- **Change onboarding prompt to follow suggestions carousel in web UI** (#28878 and #29272 by @Gargron)
- **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, and #29879 by @ClearlyClaire, @Gargron, @hteumeuleu, and @mjankowski)\
All emails to end-users have been completely redesigned with a fresh new look, providing more information while making them easier to reand and keeping maximum compatibility across mail clients.
- **Change follow recommendations algorithm** (#28314, #28433, #29017, #29108, #29306, #29550, #29619, and #31474 by @ClearlyClaire, @Gargron, @kernal053, @mjankowski, and @wheatear-dev)\
This replaces the “past interactions” recommendation algorithm with a “friends of friends” algorithm that suggests accounts followed by people you follow, and a “similar profiles” algorithm that suggests accounts with a profile similar to your most recent follows.\
In addition, the implementation has been significantly reworked, and all follow recommendations are now dismissable.\
This change deprecates the `source` attribute in `Suggestion` entities in the REST API, and replaces it with the new [`sources` attribute](https://docs.joinmastodon.org/entities/Suggestion/#sources).
- Change account search algorithm (#30803 by @Gargron)
- **Change streaming server to use its own dependencies and its own docker image** (#24702, #27967, #26850, #28112, #28115, #28137, #28138, #28497, #28548, and #30795 by @TheEssem, @ThisIsMissEm, @jippi, @timetinytim, and @vmstan)\
In order to reduce the amount of runtime dependencies, the streaming server has been moved into a separate package and Docker image.\
The `mastodon` image does not contain the streaming server anymore, as it has been moved to its own `mastodon-streaming` image.\
Administrators may need to update their setup accordingly.
- Change how content warnings and filters are displayed in web UI (#31365 by @Gargron)
- Change Web UI to allow viewing and severing relationships with suspended accounts (#27667 by @ClearlyClaire)\
This also adds a `with_suspended` parameter to `GET /api/v1/accounts/relationships` in the REST API.
- Change avatars border radius (#31390 by @renchap)
- Change counters to be displayed on profile timelines in web UI (#30525 by @Gargron)
- Change disabled buttons color in light mode to make the difference more visible (#30998 by @renchap)
- Change design of people tab on explore in web UI (#30059 by @Gargron)
- Change sidebar text in web UI (#30696 by @Gargron)
- Change "Follow" to "Follow back" and "Mutual" when appropriate in web UI (#28452 and #28465 by @Gargron and @renchap)
- Change media to be hidden/blurred by default in report modal (#28522 by @ClearlyClaire)
- Change order of the "muting" and "blocking" list options in “Data Exports” (#26088 by @fixermark)
- Change admin and moderation notes character limit from 500 to 2000 characters (#30288 by @ThisIsMissEm)
- Change mute options to be in dropdown on muted users list in web UI (#30049 and #31315 by @ClearlyClaire and @Gargron)
- Change out-of-band hashtags design in web UI (#29732 by @Gargron)
- Change design of metadata underneath detailed posts in web UI (#29585, #29605, and #29648 by @ClearlyClaire and @Gargron)
- Change action button to be last on profiles in web UI (#29533 and #29923 by @ClearlyClaire and @Gargron)
- Change confirmation prompts in trending moderation interface to be more specific (#19626 by @tribela)
- Change “Trends” moderation menu to “Recommendations & Trends” and move follow recommendations there (#31292 by @ThisIsMissEm)
- Change irrelevant fields in account cleanup settings to be disabled unless automatic cleanup is enabled (#26562 by @c960657)
- Change dropdown menu icon to not be replaced by close icon when open in web UI (#29532 by @Gargron)
- Change back button to always appear in advanced web UI (#29551 and #29669 by @Gargron)
- Change border of active compose field search inputs (#29832 and #29839 by @vmstan)
- Change link detection to allow `@` at the end of an URL (#31124 by @adamniedzielski)
- Change User-Agent to use Mastodon as the product, and http.rb as platform details (#31192 by @ClearlyClaire)
- Change layout and wording of the Content Retention server settings page (#27733 by @vmstan)
- Change unconfirmed users to be kept for one week instead of two days (#30285 by @renchap)
- Change maximum page size for Admin Domain Management APIs from 200 to 500 (#31253 by @ThisIsMissEm)
- Change database pool size to default to Sidekiq concurrency settings in Sidekiq processes (#26488 by @sinoru)
- Change alt text to empty string for avatars (#21875 by @jasminjohal)
- Change Docker images to use custom-built libvips and ffmpeg (#30571, #30569, and #31498 by @vmstan)
- Change external links in the admin audit log to plain text or local administration pages (#27139 and #27150 by @ClearlyClaire and @ThisIsMissEm)
- Change YJIT to be enabled when available (#30310 and #27283 by @ClearlyClaire and @mjankowski)\
Enable Ruby's built-in just-in-time compiler. This improves performances substantially, at the cost of a slightly increased memory usage.
- Change `.env` file loading from deprecated `dotenv-rails` gem to `dotenv` gem (#29173 and #30121 by @mjankowski)\
This should have no effect except in the unlikely case an environment variable included a newline.
- Change “Panjabi” language name to the more common spelling “Punjabi” (#27117 by @gunchleoc)
- Change encryption of OTP secrets to use ActiveRecord Encryption (#29831, #28325, #30151, #30202, #30340, and #30344 by @ClearlyClaire and @mjankowski)\
This requires a manual step from administrators of existing servers. Indeed, they need to generate new secrets, which can be done using `bundle exec rails db:encryption:init`.\
Furthermore, there is a risk that the introduced migration fails if the server was misconfigured in the past. If that happens, the migration error will include the relevant information.
- Change `/api/v1/announcements` to return regular `Status` entities (#26736 by @ClearlyClaire)
- Change imports to convert case-insensitive fields to lowercase (#29739 and #29740 by @ThisIsMissEm)
- Change stats in the admin interface to be inclusive of the full selected range, from beginning of day to end of day (#29416 and #29841 by @mjankowski)
- Change materialized views to be refreshed concurrently to avoid locks (#29015 by @Gargron)
- Change compose form to use server-provided post character and poll options limits (#28928 and #29490 by @ClearlyClaire and @renchap)
- Change streaming server logging from `npmlog` to `pino` and `pino-http` (#27828 by @ThisIsMissEm)\
This changes the Mastodon streaming server log format, so this might be considered a breaking change if you were parsing the logs.
- Change media “ALT” label to use a specific CSS class (#28777 by @ClearlyClaire)
- Change streaming API host to not be overridden to localhost in development mode (#28557 by @ClearlyClaire)
- Change cookie rotator to use SHA1 digest for new cookies (#27392 by @ClearlyClaire)\
Note that this requires that no pre-4.2.0 Mastodon web server is running when this code is deployed, as those would not understand the new cookies.\
Therefore, zero-downtime updates are only supported if you're coming from 4.2.0 or newer. If you want to skip Mastodon 4.2, you will need to completely stop Mastodon services before updating.
- Change preview card deletes to be done using batch method (#28183 by @vmstan)
- Change `img-src` and `media-src` CSP directives to not include `https:` (#28025 and #28561 by @ClearlyClaire)
- Change self-destruct procedure (#26439, #29049, and #29420 by @ClearlyClaire and @zunda)\
Instead of enqueuing deletion jobs immediately, `tootctl self-destruct` now outputs a value for the `SELF_DESTRUCT` environment variable, which puts a server in self-destruct mode, processing deletions in the background, while giving users access to their export archives.
### Removed
- Remove StatsD integration (replaced by OpenTelemetry) (#30240 by @mjankowski)
- Remove `CacheBuster` default options (#30718 by @mjankowski)
- Remove home marker updates from the Web UI (#22721 by @davbeck)\
The web interface was unconditionally updating the home marker to the most recent received post, discarding any value set by other clients, thus making the feature unreliable.
- Remove support for Ruby 3.0 (reaching EOL) (#29702 by @mjankowski)
- Remove setting for unfollow confirmation modal (#29373 by @ClearlyClaire)\
Instead, the unfollow confirmation modal will always be displayed.
- Remove support for Capistrano (#27295 and #30009 by @mjankowski and @renchap)
### Fixed
- **Fix link preview cards not always preserving the original URL from the status** (#27312 by @Gargron)
- Fix log out from user menu not working on Safari (#31402 by @renchap)
- Fix various issues when in link preview card generation (#28748, #30017, #30362, #30173, #30853, #30929, #30933, #30957, #30987, and #31144 by @adamniedzielski, @oneiros, @phocks, @timothyjrogers, and @tribela)
- Fix handling of missing links in Webfinger responses (#31030 by @adamniedzielski)
- Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil)
- Fix cross-origin loading of `inert.css` polyfill (#30687 by @louis77)
- Fix cutoff of instance name in sign-up form (#30598 by @oneiros)
- Fix empty `aria-hidden` attribute value in logo resources area (#30570 by @mjankowski)
- Fix “Redirect URI” field not being marked as required in “New application” form (#30311 by @ThisIsMissEm)
- Fix right-to-left text in preview cards (#30930 by @ClearlyClaire)
- Fix rack attack `match_type` value typo in logging config (#30514 by @mjankowski)
- Fix various cases of duplicate, missing, or inconsistent borders or scrollbar styles (#31068, #31286, #31268, #31275, #31284, #31305, #31346, #31372, #31373, #31389, #31432, #31391, and #31445 by @valtlai and @vmstan)
- Fix race condition in `POST /api/v1/push/subscription` (#30166 by @ClearlyClaire)
- Fix post deletion not being delayed when those are part of an account warning (#30163 by @ClearlyClaire)
- Fix rendering error on `/start` when not logged in (#30023 by @timothyjrogers)
- Fix logo pushing header buttons out of view on certain conditions in mobile layout (#29787 by @ClearlyClaire)
- Fix notification-related records not being reattributed when merging accounts (#29694 by @ClearlyClaire)
- Fix results/query in `api/v1/featured_tags/suggestions` (#29597 by @mjankowski)
- Fix distracting and confusing always-showing scrollbar track in boost confirmation modal (#31524 by @ClearlyClaire)
- Fix being able to upload more than 4 media attachments in some cases (#29183 by @mashirozx)
- Fix preview card player getting embedded when clicking on the external link button (#29457 by @ClearlyClaire)
- Fix full date display not respecting the locale 12/24h format (#29448 by @renchap)
- Fix filters title and keywords overflow (#29396 by @GeopJr)
- Fix incorrect date format in “Follows and followers” (#29390 by @JasonPunyon)
- Fix “Edit media” modal sizing and layout when space-constrained (#27095 by @ronilaukkarinen)
- Fix modal container bounds (#29185 by @nico3333fr)
- Fix inefficient HTTP signature parsing using regexps and `StringScanner` (#29133 by @ClearlyClaire)
- Fix moderation report updates through `PUT /api/v1/admin/reports/:id` not being logged in the audit log (#29044, #30342, and #31033 by @mjankowski, @tribela, and @vmstan)
- Fix moderation interface allowing to select rule violation when there are no server rules (#31458 by @ThisIsMissEm)
- Fix redirection from paths with url-encoded `@` to their decoded form (#31184 by @timothyjrogers)
- Fix Trending Tags pending review having an unstable sort order (#31473 by @ThisIsMissEm)
- Fix the emoji dropdown button always opening the dropdown instead of behaving like a toggle (#29012 by @jh97uk)
- Fix processing of incoming posts with bearcaps (#26527 by @kmycode)
- Fix support for IPv6 redis connections in streaming (#31229 by @ThisIsMissEm)
- Fix search form re-rendering spuriously in web UI (#28876 by @Gargron)
- Fix `RedownloadMediaWorker` not being called on transient S3 failure (#28714 by @ClearlyClaire)
- Fix ISO code for Canadian French from incorrect `fr-QC` to `fr-CA` (#26015 by @gunchleoc)
- Fix `.opus` file uploads being misidentified by Paperclip (#28580 by @vmstan)
- Fix loading local accounts with extraneous domain part in WebUI (#28559 by @ClearlyClaire)
- Fix destructive actions in dropdowns not using error color in light theme (#28484 by @logicalmoody)
- Fix call to inefficient `delete_matched` cache method in domain blocks (#28374 by @ClearlyClaire)
- Fix status edits not always being streamed to mentioned users (#28324 by @ClearlyClaire)
- Fix onboarding step descriptions being truncated on narrow screens (#28021 by @ClearlyClaire)
- Fix duplicate IDs in relationships and familiar_followers APIs (#27982 by @KevinBongart)
- Fix modal content not being selectable (#27813 by @pajowu)
- Fix Web UI not displaying appropriate explanation when a user hides their follows/followers (#27791 by @ClearlyClaire)
- Fix format-dependent redirects being cached regardless of requested format (#27632 by @ClearlyClaire)
- Fix confusing screen when visiting a confirmation link for an already-confirmed email (#27368 by @ClearlyClaire)
- Fix explore page reloading when you navigate back to it in web UI (#27489 by @Gargron)
- Fix missing redirection from `/home` to `/deck/home` in the advanced interface (#27378 by @Signez)
- Fix empty environment variables not using default nil value (#27400 by @renchap)
- Fix language sorting in settings (#27158 by @gunchleoc)
## |4.2.11] - 2024-08-16
### Added
- Add support for incoming `<s>` tag ([mediaformat](https://github.com/mastodon/mastodon/pull/31375))
### Changed
- Change logic of block/mute bypass for mentions from moderators to only apply to visible roles with moderation powers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31271))
### Fixed
- Fix incorrect rate limit on PUT requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31356))
- Fix presence of `ß` in adjacent word preventing mention and hashtag matching ([adamniedzielski](https://github.com/mastodon/mastodon/pull/31122))
- Fix processing of webfinger responses with multiple `self` links ([adamniedzielski](https://github.com/mastodon/mastodon/pull/31110))
- Fix duplicate `orderedItems` in user archive's `outbox.json` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31099))
- Fix click event handling when clicking outside of an open dropdown menu ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31251))
- Fix status processing failing halfway when a remote post has a malformed `replies` attribute ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31246))
- Fix `--verbose` option of `tootctl media remove`, which was previously erroneously removed ([mjankowski](https://github.com/mastodon/mastodon/pull/30536))
- Fix division by zero on some video/GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30600))
- Fix Web UI trying to save user settings despite being logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30324))
- Fix hashtag regexp matching some link anchors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30190))
- Fix local account search on LDAP login being case-sensitive ([raucao](https://github.com/mastodon/mastodon/pull/30113))
- Fix development environment admin account not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29958))
- Fix report reason selector in moderation interface not unselecting rules when changing category ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29026))
- Fix already-invalid reports failing to resolve ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29027))
- Fix OCR when using S3/CDN for assets ([vmstan](https://github.com/mastodon/mastodon/pull/28551))
- Fix error when encountering malformed `Tag` objects from Kbin ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28235))
- Fix not all allowed image formats showing in file picker when uploading custom emoji ([june128](https://github.com/mastodon/mastodon/pull/28076))
- Fix search popout listing unusable search options when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27918))
- Fix processing of featured collections lacking an `items` attribute ([tribela](https://github.com/mastodon/mastodon/pull/27581))
- Fix `mastodon:stats` decoration of stats rake task ([mjankowski](https://github.com/mastodon/mastodon/pull/31104))
## [4.2.10] - 2024-07-04
### Security

View file

@ -11,6 +11,11 @@ You can contribute in the following ways:
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
Please review the org-level [contribution guidelines] for high-level acceptance
criteria guidance.
[contribution guidelines]: https://github.com/mastodon/.github/blob/main/CONTRIBUTING.md
## API Changes and Additions
Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation).

View file

@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.8
# syntax=docker/dockerfile:1.9
# This file is designed for production server deployment, not local development work
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker
@ -191,16 +191,19 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.15.2
ARG VIPS_VERSION=8.15.3
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
WORKDIR /usr/local/libvips/src
# Download and extract libvips source code
ADD ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz /usr/local/libvips/src/
RUN tar xf vips-${VIPS_VERSION}.tar.xz;
WORKDIR /usr/local/libvips/src/vips-${VIPS_VERSION}
# Configure and compile libvips
RUN \
curl -sSL -o vips-${VIPS_VERSION}.tar.xz ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz; \
tar xf vips-${VIPS_VERSION}.tar.xz; \
cd vips-${VIPS_VERSION}; \
meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \
cd build; \
ninja; \
@ -211,16 +214,19 @@ FROM build AS ffmpeg
# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"]
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
ARG FFMPEG_VERSION=7.0.1
ARG FFMPEG_VERSION=7.0.2
# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"]
ARG FFMPEG_URL=https://ffmpeg.org/releases
WORKDIR /usr/local/ffmpeg/src
# Download and extract ffmpeg source code
ADD ${FFMPEG_URL}/ffmpeg-${FFMPEG_VERSION}.tar.xz /usr/local/ffmpeg/src/
RUN tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz;
WORKDIR /usr/local/ffmpeg/src/ffmpeg-${FFMPEG_VERSION}
# Configure and compile ffmpeg
RUN \
curl -sSL -o ffmpeg-${FFMPEG_VERSION}.tar.xz ${FFMPEG_URL}/ffmpeg-${FFMPEG_VERSION}.tar.xz; \
tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz; \
cd ffmpeg-${FFMPEG_VERSION}; \
./configure \
--prefix=/usr/local/ffmpeg \
--toolchain=hardened \

View file

@ -16,7 +16,7 @@ gem 'pghero'
gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'blurhash', '~> 0.1'
gem 'fog-core', '<= 2.4.0'
gem 'fog-core', '<= 2.5.0'
gem 'fog-openstack', '~> 1.0', require: false
gem 'kt-paperclip', '~> 7.2'
gem 'md-paperclip-azure', '~> 2.2', require: false
@ -64,7 +64,6 @@ gem 'link_header', '~> 0.0'
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15'
gem 'nsa'
gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14'
gem 'parslet'
@ -88,7 +87,7 @@ gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'simple_form', '~> 5.2'
gem 'simple-navigation', '~> 4.4'
gem 'stoplight', '~> 4.1'
gem 'strong_migrations', '1.8.0'
gem 'strong_migrations'
gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023'
@ -100,7 +99,7 @@ gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2'
gem 'rdf-normalize', '~> 0.5'
gem 'opentelemetry-api', '~> 1.2.5'
gem 'opentelemetry-api', '~> 1.3.0'
group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.28.0', require: false
@ -112,7 +111,7 @@ group :opentelemetry do
gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.27.1', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false

View file

@ -100,20 +100,20 @@ GEM
attr_required (1.0.2)
awrence (1.2.1)
aws-eventstream (1.3.0)
aws-partitions (1.950.0)
aws-sdk-core (3.201.0)
aws-partitions (1.966.0)
aws-sdk-core (3.201.5)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.156.0)
aws-sdk-s3 (1.159.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.8.0)
aws-sigv4 (1.9.1)
aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
@ -135,7 +135,7 @@ GEM
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.7)
bootsnap (1.18.3)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (6.1.2)
racc
@ -168,7 +168,7 @@ GEM
climate_control (1.2.0)
cocoon (1.2.15)
color_diff (0.1)
concurrent-ruby (1.3.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
cose (1.3.0)
cbor (~> 0.5.9)
@ -180,7 +180,7 @@ GEM
css_parser (1.17.1)
addressable
csv (3.3.0)
database_cleaner-active_record (2.1.0)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
@ -222,16 +222,16 @@ GEM
elasticsearch-transport (7.17.10)
faraday (>= 1, < 3)
multi_json
email_spec (2.2.2)
email_spec (2.3.0)
htmlentities (~> 4.3.3)
launchy (~> 2.1)
launchy (>= 2.1, < 4.0)
mail (~> 2.7)
erubi (1.13.0)
et-orbi (1.2.11)
tzinfo
excon (0.110.0)
excon (0.111.0)
fabrication (2.31.0)
faker (3.4.1)
faker (3.4.2)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
@ -269,7 +269,7 @@ GEM
flatware-rspec (2.3.2)
flatware (= 2.3.2)
rspec (>= 3.6)
fog-core (2.4.0)
fog-core (2.5.0)
builder
excon (~> 0.71)
formatador (>= 0.2, < 2.0)
@ -281,15 +281,15 @@ GEM
fog-core (~> 2.1)
fog-json (>= 1.0)
formatador (1.1.0)
fugit (1.10.1)
et-orbi (~> 1, >= 1.2.7)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
fuubar (2.5.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (3.25.3)
google-protobuf (3.25.4)
googleapis-common-protos-types (1.14.0)
google-protobuf (~> 3.18)
haml (6.3.0)
@ -357,17 +357,18 @@ GEM
aes_key_wrap
bindata
httpclient
json-ld (3.3.1)
json-ld (3.3.2)
htmlentities (~> 4.3)
json-canonicalization (~> 1.0)
link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.15)
rack (>= 2.2, < 4)
rdf (~> 3.3)
rexml (~> 3.2)
json-ld-preloaded (3.3.0)
json-ld (~> 3.3)
rdf (~> 3.3)
json-schema (4.3.0)
json-schema (4.3.1)
addressable (>= 2.8)
jsonapi-renderer (0.2.2)
jwt (2.7.1)
@ -428,7 +429,7 @@ GEM
memory_profiler (1.0.2)
mime-types (3.5.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2024.0604)
mime-types-data (3.2024.0702)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.24.1)
@ -440,7 +441,7 @@ GEM
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.12)
net-imap (0.4.14)
date
net-protocol
net-ldap (0.19.0)
@ -451,16 +452,12 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.3)
nokogiri (1.16.6)
nokogiri (1.16.7)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nsa (0.3.0)
activesupport (>= 4.2, < 7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.4)
oj (3.16.5)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
@ -492,10 +489,10 @@ GEM
openssl (3.2.0)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.2.5)
opentelemetry-api (1.3.0)
opentelemetry-common (0.20.1)
opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.28.0)
opentelemetry-exporter-otlp (0.28.1)
google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
@ -512,17 +509,17 @@ GEM
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (~> 0.21)
opentelemetry-instrumentation-action_view (0.7.0)
opentelemetry-instrumentation-action_view (0.7.2)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_job (0.7.2)
opentelemetry-instrumentation-active_job (0.7.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_model_serializers (0.20.1)
opentelemetry-instrumentation-active_model_serializers (0.20.2)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_record (0.7.2)
opentelemetry-instrumentation-active_record (0.7.3)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_support (0.6.0)
@ -531,32 +528,32 @@ GEM
opentelemetry-instrumentation-base (0.22.3)
opentelemetry-api (~> 1.0)
opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.21.3)
opentelemetry-instrumentation-concurrent_ruby (0.21.4)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-excon (0.22.3)
opentelemetry-instrumentation-excon (0.22.4)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-faraday (0.24.5)
opentelemetry-instrumentation-faraday (0.24.6)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http (0.23.3)
opentelemetry-instrumentation-http (0.23.4)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http_client (0.22.6)
opentelemetry-instrumentation-http_client (0.22.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-net_http (0.22.6)
opentelemetry-instrumentation-net_http (0.22.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-pg (0.27.3)
opentelemetry-instrumentation-pg (0.28.0)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (0.24.5)
opentelemetry-instrumentation-rack (0.24.6)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rails (0.31.0)
opentelemetry-instrumentation-rails (0.31.2)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
opentelemetry-instrumentation-action_pack (~> 0.9.0)
@ -565,22 +562,23 @@ GEM
opentelemetry-instrumentation-active_record (~> 0.7.0)
opentelemetry-instrumentation-active_support (~> 0.6.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-redis (0.25.6)
opentelemetry-instrumentation-redis (0.25.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-sidekiq (0.25.6)
opentelemetry-instrumentation-sidekiq (0.25.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-registry (0.3.1)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.4.1)
opentelemetry-sdk (1.5.0)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.10.0)
opentelemetry-semantic_conventions (1.10.1)
opentelemetry-api (~> 1.0)
orm_adapter (0.5.0)
ostruct (0.6.0)
ox (2.14.18)
parallel (1.25.1)
parser (3.3.4.0)
@ -589,9 +587,9 @@ GEM
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.5.6)
pghero (3.5.0)
activerecord (>= 6)
pg (1.5.7)
pghero (3.6.0)
activerecord (>= 6.1)
premailer (1.23.0)
addressable
css_parser (>= 1.12.0)
@ -600,20 +598,20 @@ GEM
actionmailer (>= 3)
net-smtp
premailer (~> 1.7, >= 1.7.9)
propshaft (0.9.0)
propshaft (0.9.1)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
railties (>= 7.0.0)
psych (5.1.2)
stringio
public_suffix (6.0.0)
public_suffix (6.0.1)
puma (6.4.2)
nio4r (~> 2.0)
pundit (2.3.2)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.0)
racc (1.8.1)
rack (2.2.9)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
@ -675,8 +673,9 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdf (3.3.1)
rdf (3.3.2)
bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.7.0)
rdf (~> 3.3)
@ -696,7 +695,7 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.3.1)
rexml (3.3.4)
strscan
rotp (6.3.0)
rouge (4.2.1)
@ -719,7 +718,7 @@ GEM
rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.3)
rspec-rails (6.1.4)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
@ -733,7 +732,7 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.13.1)
rubocop (1.65.0)
rubocop (1.65.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@ -756,7 +755,7 @@ GEM
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.2)
rubocop-rspec (3.0.4)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
@ -766,21 +765,22 @@ GEM
ruby-saml (1.16.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.1)
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
sanitize (6.1.1)
sanitize (6.1.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
scenic (1.8.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
selenium-webdriver (4.22.0)
selenium-webdriver (4.23.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@ -793,7 +793,7 @@ GEM
redis (>= 4.5.0, < 5)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (5.0.5)
sidekiq-scheduler (5.0.6)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0, < 3)
@ -816,12 +816,11 @@ GEM
simplecov-lcov (0.8.0)
simplecov_json_formatter (0.1.4)
stackprof (0.2.26)
statsd-ruby (1.5.0)
stoplight (4.1.0)
redlock (~> 1.0)
stringio (3.1.1)
strong_migrations (1.8.0)
activerecord (>= 5.2)
strong_migrations (2.0.0)
activerecord (>= 6.1)
strscan (3.1.0)
swd (1.3.0)
activesupport (>= 3)
@ -833,7 +832,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1)
climate_control
test-prof (1.3.3.1)
test-prof (1.4.0)
thor (1.3.1)
tilt (2.3.0)
timeout (0.4.1)
@ -893,7 +892,7 @@ GEM
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.8.1)
websocket (1.2.10)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@ -901,7 +900,7 @@ GEM
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.16)
zeitwerk (2.6.17)
PLATFORMS
ruby
@ -942,7 +941,7 @@ DEPENDENCIES
fast_blank (~> 1.0)
fastimage
flatware-rspec
fog-core (<= 2.4.0)
fog-core (<= 2.5.0)
fog-openstack (~> 1.0)
fuubar (~> 2.5)
haml-rails (~> 2.0)
@ -975,14 +974,13 @@ DEPENDENCIES
net-http (~> 0.4.0)
net-ldap (~> 0.18)
nokogiri (~> 1.15)
nsa
oj (~> 3.14)
omniauth (~> 2.0)
omniauth-cas (~> 3.0.0.beta.1)
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1)
opentelemetry-api (~> 1.2.5)
opentelemetry-api (~> 1.3.0)
opentelemetry-exporter-otlp (~> 0.28.0)
opentelemetry-instrumentation-active_job (~> 0.7.1)
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)
@ -992,7 +990,7 @@ DEPENDENCIES
opentelemetry-instrumentation-http (~> 0.23.2)
opentelemetry-instrumentation-http_client (~> 0.22.3)
opentelemetry-instrumentation-net_http (~> 0.22.4)
opentelemetry-instrumentation-pg (~> 0.27.1)
opentelemetry-instrumentation-pg (~> 0.28.0)
opentelemetry-instrumentation-rack (~> 0.24.1)
opentelemetry-instrumentation-rails (~> 0.31.0)
opentelemetry-instrumentation-redis (~> 0.25.3)
@ -1045,7 +1043,7 @@ DEPENDENCIES
simplecov-lcov (~> 0.8)
stackprof
stoplight (~> 4.1)
strong_migrations (= 1.8.0)
strong_migrations
test-prof
thor (~> 1.2)
tty-prompt (~> 0.23)

View file

@ -13,6 +13,7 @@ module Admin
def show
authorize :instance, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
end
def destroy

View file

@ -2,7 +2,15 @@
module Admin
class TagsController < BaseController
before_action :set_tag
before_action :set_tag, except: [:index]
PER_PAGE = 20
def index
authorize :tag, :index?
@tags = filtered_tags.page(params[:page]).per(PER_PAGE)
end
def show
authorize @tag, :show?
@ -31,5 +39,13 @@ module Admin
def tag_params
params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable)
end
def filtered_tags
TagFilter.new(filter_params.with_defaults(order: 'newest')).results
end
def filter_params
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
end
end
end

View file

@ -30,10 +30,10 @@ class Api::BaseController < ApplicationController
protected
def limit_param(default_limit)
def limit_param(default_limit, max_limit = nil)
return default_limit unless params[:limit]
[params[:limit].to_i.abs, default_limit * 2].min
[params[:limit].to_i.abs, max_limit || (default_limit * 2)].min
end
def params_slice(*keys)

View file

@ -5,6 +5,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
include AccountableConcern
LIMIT = 100
MAX_LIMIT = 500
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show]
@ -47,18 +48,13 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
private
def set_domain_allows
@domain_allows = filtered_domain_allows.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
@domain_allows = DomainAllow.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT, MAX_LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_domain_allow
@domain_allow = DomainAllow.find(params[:id])
end
def filtered_domain_allows
# TODO: no filtering yet
DomainAllow.all
end
def next_path
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
@ -72,7 +68,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
end
def records_continue?
@domain_allows.size == limit_param(LIMIT)
@domain_allows.size == limit_param(LIMIT, MAX_LIMIT)
end
def resource_params

View file

@ -5,6 +5,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
include AccountableConcern
LIMIT = 100
MAX_LIMIT = 500
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show]
@ -59,18 +60,13 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
end
def set_domain_blocks
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
@domain_blocks = DomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT, MAX_LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_domain_block
@domain_block = DomainBlock.find(params[:id])
end
def filtered_domain_blocks
# TODO: no filtering yet
DomainBlock.all
end
def domain_block_params
params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
end
@ -88,7 +84,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
end
def records_continue?
@domain_blocks.size == limit_param(LIMIT)
@domain_blocks.size == limit_param(LIMIT, MAX_LIMIT)
end
def resource_params

View file

@ -8,12 +8,12 @@ class Api::V1::Notifications::PoliciesController < Api::BaseController
before_action :set_policy
def show
render json: @policy, serializer: REST::NotificationPolicySerializer
render json: @policy, serializer: REST::V1::NotificationPolicySerializer
end
def update
@policy.update!(resource_params)
render json: @policy, serializer: REST::NotificationPolicySerializer
render json: @policy, serializer: REST::V1::NotificationPolicySerializer
end
private

View file

@ -1,11 +1,14 @@
# frozen_string_literal: true
class Api::V1::Notifications::RequestsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index
include Redisable
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: [:index, :show, :merged?]
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: [:index, :show, :merged?]
before_action :require_user!
before_action :set_request, except: :index
before_action :set_request, only: [:show, :accept, :dismiss]
before_action :set_requests, only: [:accept_bulk, :dismiss_bulk]
after_action :insert_pagination_headers, only: :index
@ -18,6 +21,10 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships
end
def merged?
render json: { merged: redis.get("notification_unfilter_jobs:#{current_account.id}").to_i <= 0 }
end
def show
render json: @request, serializer: REST::NotificationRequestSerializer
end
@ -28,14 +35,24 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
end
def dismiss
@request.update!(dismissed: true)
DismissNotificationRequestService.new.call(@request)
render_empty
end
def accept_bulk
@requests.each { |request| AcceptNotificationRequestService.new.call(request) }
render_empty
end
def dismiss_bulk
@requests.each(&:destroy!)
render_empty
end
private
def load_requests
requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed) || false).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
requests = NotificationRequest.where(account: current_account).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
@ -53,14 +70,22 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
@request = NotificationRequest.where(account: current_account).find(params[:id])
end
def set_requests
@requests = NotificationRequest.where(account: current_account, id: Array(params[:id]).uniq.map(&:to_i))
end
def next_path
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty?
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty?
end
def records_continue?
@requests.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_max_id
@requests.last.id
end
@ -68,8 +93,4 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
def pagination_since_id
@requests.first.id
end
def pagination_params(core_params)
params.slice(:dismissed).permit(:dismissed).merge(core_params)
end
end

View file

@ -7,6 +7,8 @@ class Api::V1::NotificationsController < Api::BaseController
after_action :insert_pagination_headers, only: :index
DEFAULT_NOTIFICATIONS_LIMIT = 40
DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100
MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000
def index
with_read_replica do
@ -17,6 +19,14 @@ class Api::V1::NotificationsController < Api::BaseController
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
end
def unread_count
limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT)
with_read_replica do
render json: { count: browserable_account_notifications.paginate_by_min_id(limit, notification_marker&.last_read_id).count }
end
end
def show
@notification = current_account.notifications.without_suspended.find(params[:id])
render json: @notification, serializer: REST::NotificationSerializer
@ -54,6 +64,10 @@ class Api::V1::NotificationsController < Api::BaseController
)
end
def notification_marker
current_user.markers.find_by(timeline: 'notifications')
end
def target_statuses_from_notifications
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
end

View file

@ -10,7 +10,7 @@ class Api::V1::ReportsController < Api::BaseController
@report = ReportService.new.call(
current_account,
reported_account,
report_params
report_params.merge(application: doorkeeper_token.application)
)
render json: @report, serializer: REST::ReportSerializer

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
class Api::V2::Notifications::PoliciesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :show
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: :update
before_action :require_user!
before_action :set_policy
def show
render json: @policy, serializer: REST::NotificationPolicySerializer
end
def update
@policy.update!(resource_params)
render json: @policy, serializer: REST::NotificationPolicySerializer
end
private
def set_policy
@policy = NotificationPolicy.find_or_initialize_by(account: current_account)
with_read_replica do
@policy.summarize!
end
end
def resource_params
params.permit(
:for_not_following,
:for_not_followers,
:for_new_accounts,
:for_private_mentions,
:for_limited_accounts
)
end
end

View file

@ -7,20 +7,49 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
after_action :insert_pagination_headers, only: :index
DEFAULT_NOTIFICATIONS_LIMIT = 40
DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100
MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000
def index
with_read_replica do
@notifications = load_notifications
@group_metadata = load_group_metadata
@grouped_notifications = load_grouped_notifications
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
@presenter = GroupedNotificationsPresenter.new(@grouped_notifications, expand_accounts: expand_accounts_param)
# Preload associations to avoid N+1s
ActiveRecord::Associations::Preloader.new(records: @presenter.accounts, associations: [:account_stat, { user: :role }]).call
end
render json: @notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#index rendering') do |span|
statuses = @grouped_notifications.filter_map { |group| group.target_status&.id }
span.add_attributes(
'app.notification_grouping.count' => @grouped_notifications.size,
'app.notification_grouping.account.count' => @presenter.accounts.size,
'app.notification_grouping.partial_account.count' => @presenter.partial_accounts.size,
'app.notification_grouping.status.count' => statuses.size,
'app.notification_grouping.status.unique_count' => statuses.uniq.size,
'app.notification_grouping.expand_accounts_param' => expand_accounts_param
)
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata, expand_accounts: expand_accounts_param
end
end
def unread_count
limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT)
with_read_replica do
render json: { count: browserable_account_notifications.paginate_groups_by_min_id(limit, min_id: notification_marker&.last_read_id).count }
end
end
def show
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id])
render json: NotificationGroup.from_notification(@notification), serializer: REST::NotificationGroupSerializer
presenter = GroupedNotificationsPresenter.new([NotificationGroup.from_notification(@notification)])
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
end
def clear
@ -36,25 +65,35 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
private
def load_notifications
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_notifications') do
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
preload_collection(target_statuses, Status)
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
preload_collection(target_statuses, Status)
end
end
end
def load_group_metadata
return {} if @notifications.empty?
browserable_account_notifications
.where(group_key: @notifications.filter_map(&:group_key))
.where(id: (@notifications.last.id)..(@notifications.first.id))
.group(:group_key)
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_group_metadata') do
browserable_account_notifications
.where(group_key: @notifications.filter_map(&:group_key))
.where(id: (@notifications.last.id)..(@notifications.first.id))
.group(:group_key)
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
end
end
def load_grouped_notifications
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do
@notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }
end
end
def browserable_account_notifications
@ -65,6 +104,10 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
)
end
def notification_marker
current_user.markers.find_by(timeline: 'notifications')
end
def target_statuses_from_notifications
@notifications.filter_map(&:target_status)
end
@ -88,4 +131,15 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
def pagination_params(core_params)
params.slice(:limit, :types, :exclude_types, :include_filtered).permit(:limit, :include_filtered, types: [], exclude_types: []).merge(core_params)
end
def expand_accounts_param
case params[:expand_accounts]
when nil, 'full'
'full'
when 'partial_avatars'
'partial_avatars'
else
raise Mastodon::InvalidParameterError, "Invalid value for 'expand_accounts': '#{params[:expand_accounts]}', allowed values are 'full' and 'partial_avatars'"
end
end
end

View file

@ -20,7 +20,6 @@ class ApplicationController < ActionController::Base
helper_method :current_theme
helper_method :single_user_mode?
helper_method :use_seamless_external_login?
helper_method :omniauth_only?
helper_method :sso_account_settings
helper_method :limited_federation_mode?
helper_method :body_class_string
@ -137,10 +136,6 @@ class ApplicationController < ActionController::Base
Devise.pam_authentication || Devise.ldap_authentication
end
def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end
def sso_account_settings
ENV.fetch('SSO_ACCOUNT_SETTINGS', nil)
end

View file

@ -5,7 +5,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth'
before_action :set_body_classes
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
before_action :redirect_confirmed_user, if: :signed_in_confirmed_user?
@ -73,10 +72,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
end
def set_body_classes
@body_classes = 'lighter'
end
def after_resending_confirmation_instructions_path_for(_resource_name)
if user_signed_in?
if current_user.confirmed? && current_user.approved?

View file

@ -3,7 +3,6 @@
class Auth::PasswordsController < Devise::PasswordsController
skip_before_action :check_self_destruct!
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
before_action :set_body_classes
layout 'auth'
@ -24,10 +23,6 @@ class Auth::PasswordsController < Devise::PasswordsController
redirect_to new_password_path(resource_name)
end
def set_body_classes
@body_classes = 'lighter'
end
def reset_password_token_is_valid?
resource_class.with_reset_password_token(params[:reset_password_token]).present?
end

View file

@ -105,7 +105,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
private
def set_body_classes
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
@body_classes = 'admin' if %w(edit update).include?(action_name)
end
def set_invite

View file

@ -16,8 +16,6 @@ class Auth::SessionsController < Devise::SessionsController
include Auth::TwoFactorAuthenticationConcern
before_action :set_body_classes
content_security_policy only: :new do |p|
p.form_action(false)
end
@ -103,10 +101,6 @@ class Auth::SessionsController < Devise::SessionsController
private
def set_body_classes
@body_classes = 'lighter'
end
def home_paths(resource)
paths = [about_path, '/explore']
@ -193,4 +187,15 @@ class Auth::SessionsController < Devise::SessionsController
def second_factor_attempts_key(user)
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
end
def respond_to_on_destroy
respond_to do |format|
format.json do
render json: {
redirect_to: after_sign_out_path_for(resource_name),
}, status: 200
end
format.all { super }
end
end
end

View file

@ -5,7 +5,6 @@ class Auth::SetupController < ApplicationController
before_action :authenticate_user!
before_action :require_unconfirmed_or_pending!
before_action :set_body_classes
before_action :set_user
skip_before_action :require_functional!
@ -35,10 +34,6 @@ class Auth::SetupController < ApplicationController
@user = current_user
end
def set_body_classes
@body_classes = 'lighter'
end
def user_params
params.require(:user).permit(:email)
end

View file

@ -83,7 +83,6 @@ module Auth::TwoFactorAuthenticationConcern
def prompt_for_two_factor(user)
register_attempt_in_session(user)
@body_classes = 'lighter'
@webauthn_enabled = user.webauthn_enabled?
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
'webauthn'

View file

@ -42,7 +42,6 @@ module ChallengableConcern
end
def render_challenge
@body_classes = 'lighter'
render 'auth/challenges/new', layout: 'auth'
end

View file

@ -5,7 +5,6 @@ class MailSubscriptionsController < ApplicationController
skip_before_action :require_functional!
before_action :set_body_classes
before_action :set_user
before_action :set_type
@ -25,10 +24,6 @@ class MailSubscriptionsController < ApplicationController
not_found unless @user
end
def set_body_classes
@body_classes = 'lighter'
end
def set_type
@type = email_type_from_param
end

View file

@ -25,7 +25,7 @@ module Admin::FilterHelper
end
def table_link_to(icon, text, path, **options)
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
link_to safe_join([material_symbol(icon), text]), path, options.merge(class: 'table-action-link')
end
def selected?(more_params)

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Admin::TagsHelper
def admin_tags_moderation_options
[
[t('admin.tags.moderation.reviewed'), 'reviewed'],
[t('admin.tags.moderation.review_requested'), 'review_requested'],
[t('admin.tags.moderation.unreviewed'), 'unreviewed'],
[t('admin.tags.moderation.trendable'), 'trendable'],
[t('admin.tags.moderation.not_trendable'), 'not_trendable'],
[t('admin.tags.moderation.usable'), 'usable'],
[t('admin.tags.moderation.not_usable'), 'not_usable'],
]
end
end

View file

@ -105,19 +105,12 @@ module ApplicationHelper
policy(record).public_send(:"#{action}?")
end
def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split || []
class_names << 'fa'
class_names += icon.split.map { |cl| "fa-#{cl}" }
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
def material_symbol(icon, attributes = {})
inline_svg_tag(
"400-24px/#{icon}.svg",
class: %w(icon).concat(attributes[:class].to_s.split),
role: :img
class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split),
role: :img,
data: attributes[:data]
)
end
@ -127,23 +120,23 @@ module ApplicationHelper
def visibility_icon(status)
if status.public_visibility?
fa_icon('globe', title: I18n.t('statuses.visibilities.public'))
material_symbol('globe', title: I18n.t('statuses.visibilities.public'))
elsif status.unlisted_visibility?
fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted'))
material_symbol('lock_open', title: I18n.t('statuses.visibilities.unlisted'))
elsif status.private_visibility? || status.limited_visibility?
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
material_symbol('lock', title: I18n.t('statuses.visibilities.private'))
elsif status.direct_visibility?
fa_icon('at', title: I18n.t('statuses.visibilities.direct'))
material_symbol('alternate_email', title: I18n.t('statuses.visibilities.direct'))
end
end
def interrelationships_icon(relationships, account_id)
if relationships.following[account_id] && relationships.followed_by[account_id]
fa_icon('exchange', title: I18n.t('relationships.mutual'), class: 'fa-fw active passive')
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')
elsif relationships.following[account_id]
fa_icon(locale_direction == 'ltr' ? 'arrow-right' : 'arrow-left', title: I18n.t('relationships.following'), class: 'fa-fw active')
material_symbol(locale_direction == 'ltr' ? 'arrow_right_alt' : 'arrow_left_alt', title: I18n.t('relationships.following'), class: 'active')
elsif relationships.followed_by[account_id]
fa_icon(locale_direction == 'ltr' ? 'arrow-left' : 'arrow-right', title: I18n.t('relationships.followers'), class: 'fa-fw passive')
material_symbol(locale_direction == 'ltr' ? 'arrow_left_alt' : 'arrow_right_alt', title: I18n.t('relationships.followers'), class: 'passive')
end
end
@ -244,22 +237,6 @@ module ApplicationHelper
full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg'))
end
def instance_presenter
@instance_presenter ||= InstancePresenter.new
end
def favicon_path(size = '48')
instance_presenter.favicon&.file&.url(size)
end
def app_icon_path(size = '48')
instance_presenter.app_icon&.file&.url(size)
end
def use_mask_icon?
instance_presenter.app_icon.blank?
end
private
def storage_host_var

View file

@ -13,6 +13,22 @@ module InstanceHelper
safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ')
end
def instance_presenter
@instance_presenter ||= InstancePresenter.new
end
def favicon_path(size = '48')
instance_presenter.favicon&.file&.url(size)
end
def app_icon_path(size = '48')
instance_presenter.app_icon&.file&.url(size)
end
def use_mask_icon?
instance_presenter.app_icon.blank?
end
private
def description_prefix(invite)

View file

@ -26,11 +26,11 @@ module SettingsHelper
device = session.detection.device
if device.mobile?
'mobile'
'smartphone'
elsif device.tablet?
'tablet'
else
'desktop'
'desktop_mac'
end
end

View file

@ -60,13 +60,13 @@ module StatusesHelper
def fa_visibility_icon(status)
case status.visibility
when 'public'
fa_icon 'globe fw'
material_symbol 'globe'
when 'unlisted'
fa_icon 'unlock fw'
material_symbol 'lock_open'
when 'private'
fa_icon 'lock fw'
material_symbol 'lock'
when 'direct'
fa_icon 'at fw'
material_symbol 'alternate_email'
end
end

View file

@ -3,8 +3,10 @@
module ThemeHelper
def theme_style_tags(theme)
if theme == 'system'
stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') +
stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
''.html_safe.tap do |tags|
tags << stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
end
else
stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous'
end
@ -12,8 +14,10 @@ module ThemeHelper
def theme_color_tags(theme)
if theme == 'system'
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') +
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
''.html_safe.tap do |tags|
tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)')
tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
end
else
tag.meta name: 'theme-color', content: theme_color_for(theme)
end

View file

@ -316,8 +316,8 @@ function loaded() {
const message =
statusEl.dataset.spoiler === 'expanded'
? localeData['status.show_less'] ?? 'Show less'
: localeData['status.show_more'] ?? 'Show more';
? (localeData['status.show_less'] ?? 'Show less')
: (localeData['status.show_more'] ?? 'Show more');
spoilerLink.textContent = new IntlMessageFormat(
message,
locale,
@ -431,6 +431,42 @@ Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => {
target.src = target.dataset.static;
});
const setInputDisabled = (
input: HTMLInputElement | HTMLSelectElement,
disabled: boolean,
) => {
input.disabled = disabled;
const wrapper = input.closest('.with_label');
if (wrapper) {
wrapper.classList.toggle('disabled', input.disabled);
const hidden =
input.type === 'checkbox' &&
wrapper.querySelector<HTMLInputElement>('input[type=hidden][value="0"]');
if (hidden) {
hidden.disabled = input.disabled;
}
}
};
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);
});
},
);
// Empty the honeypot fields in JS in case something like an extension
// automatically filled them.
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {

View file

@ -0,0 +1,24 @@
<svg width="5" height="80" viewBox="0 0 5 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_954_5239)">
<rect width="5" height="80" transform="matrix(1 0 0 -1 0 80)" fill="#2F0C7A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 84C6.46234 83.9609 6.57112 83.9208 6.68172 83.8795C6.79039 83.839 7.16116 83.7 7.21197 83.681C10.0919 82.6043 11.8941 82.2 15 82.2C18.0813 82.2 19.6086 82.5665 22.3886 83.6785C22.6753 83.7932 22.9494 83.9003 23.2136 84H25.0812C24.3279 83.7808 23.5395 83.4927 22.6114 83.1215C19.7664 81.9835 18.1687 81.6 15 81.6C11.8122 81.6 9.94344 82.0192 7.00185 83.119C6.95066 83.1381 6.58016 83.277 6.47198 83.3174C5.72954 83.5944 5.0738 83.8198 4.45483 84H6.35524ZM6.35524 78C3.97726 78.8676 2.42302 79.2 0 79.2L0 78.6C1.72318 78.6 2.98542 78.4277 4.45483 78H6.35524ZM23.2136 78C25.5716 78.8899 27.1507 79.2 30 79.2V78.6C27.9473 78.6 26.5843 78.4373 25.0812 78L23.2136 78ZM0 82.2C2.53215 82.2 4.1155 81.837 6.68172 80.8795C6.79039 80.839 7.16116 80.7 7.21197 80.681C10.0919 79.6043 11.8941 79.2 15 79.2C18.0813 79.2 19.6086 79.5665 22.3886 80.6785C25.2336 81.8165 26.8313 82.2 30 82.2V81.6C26.9187 81.6 25.3914 81.2335 22.6114 80.1215C19.7664 78.9835 18.1687 78.6 15 78.6C11.8122 78.6 9.94344 79.0192 7.00185 80.119C6.95066 80.1381 6.58016 80.277 6.47198 80.3174C3.96706 81.2519 2.44905 81.6 0 81.6L0 82.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 78C6.46234 77.9609 6.57112 77.9208 6.68172 77.8795C6.79039 77.839 7.16116 77.7 7.21197 77.681C10.0919 76.6043 11.8941 76.2 15 76.2C18.0813 76.2 19.6086 76.5665 22.3886 77.6785C22.6753 77.7932 22.9494 77.9003 23.2136 78H25.0812C24.3279 77.7808 23.5395 77.4927 22.6114 77.1215C19.7664 75.9835 18.1687 75.6 15 75.6C11.8122 75.6 9.94344 76.0192 7.00185 77.119C6.95066 77.1381 6.58016 77.277 6.47198 77.3174C5.72954 77.5944 5.0738 77.8198 4.45483 78H6.35524ZM6.35524 72C3.97726 72.8676 2.42302 73.2 0 73.2L0 72.6C1.72318 72.6 2.98542 72.4277 4.45483 72H6.35524ZM23.2136 72C25.5716 72.8899 27.1507 73.2 30 73.2V72.6C27.9473 72.6 26.5843 72.4373 25.0812 72L23.2136 72ZM0 76.2C2.53215 76.2 4.1155 75.837 6.68172 74.8795C6.79039 74.839 7.16116 74.7 7.21197 74.681C10.0919 73.6043 11.8941 73.2 15 73.2C18.0813 73.2 19.6086 73.5665 22.3886 74.6785C25.2336 75.8165 26.8313 76.2 30 76.2V75.6C26.9187 75.6 25.3914 75.2335 22.6114 74.1215C19.7664 72.9835 18.1687 72.6 15 72.6C11.8122 72.6 9.94344 73.0192 7.00185 74.119C6.95066 74.1381 6.58016 74.277 6.47198 74.3174C3.96706 75.2519 2.44905 75.6 0 75.6L0 76.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 72C6.46234 71.9609 6.57112 71.9208 6.68172 71.8795C6.79039 71.839 7.16116 71.7 7.21197 71.681C10.0919 70.6043 11.8941 70.2 15 70.2C18.0813 70.2 19.6086 70.5665 22.3886 71.6785C22.6753 71.7932 22.9494 71.9003 23.2136 72H25.0812C24.3279 71.7808 23.5395 71.4927 22.6114 71.1215C19.7664 69.9835 18.1687 69.6 15 69.6C11.8122 69.6 9.94344 70.0192 7.00185 71.119C6.95066 71.1381 6.58016 71.277 6.47198 71.3174C5.72954 71.5944 5.0738 71.8198 4.45483 72H6.35524ZM6.35524 66C3.97726 66.8676 2.42302 67.2 0 67.2L0 66.6C1.72318 66.6 2.98542 66.4277 4.45483 66H6.35524ZM23.2136 66C25.5716 66.8899 27.1507 67.2 30 67.2V66.6C27.9473 66.6 26.5843 66.4373 25.0812 66L23.2136 66ZM0 70.2C2.53215 70.2 4.1155 69.837 6.68172 68.8795C6.79039 68.839 7.16116 68.7 7.21197 68.681C10.0919 67.6043 11.8941 67.2 15 67.2C18.0813 67.2 19.6086 67.5665 22.3886 68.6785C25.2336 69.8165 26.8313 70.2 30 70.2V69.6C26.9187 69.6 25.3914 69.2335 22.6114 68.1215C19.7664 66.9835 18.1687 66.6 15 66.6C11.8122 66.6 9.94344 67.0192 7.00185 68.119C6.95066 68.1381 6.58016 68.277 6.47198 68.3174C3.96706 69.2519 2.44905 69.6 0 69.6L0 70.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 66C6.46234 65.9609 6.57112 65.9208 6.68172 65.8795C6.79039 65.839 7.16116 65.7 7.21197 65.681C10.0919 64.6043 11.8941 64.2 15 64.2C18.0813 64.2 19.6086 64.5665 22.3886 65.6785C22.6753 65.7932 22.9494 65.9003 23.2136 66H25.0812C24.3279 65.7808 23.5395 65.4927 22.6114 65.1215C19.7664 63.9835 18.1687 63.6 15 63.6C11.8122 63.6 9.94344 64.0192 7.00185 65.119C6.95066 65.1381 6.58016 65.277 6.47198 65.3174C5.72954 65.5944 5.0738 65.8198 4.45483 66H6.35524ZM6.35524 60C3.97726 60.8676 2.42302 61.2 0 61.2L0 60.6C1.72318 60.6 2.98542 60.4277 4.45483 60H6.35524ZM23.2136 60C25.5716 60.8899 27.1507 61.2 30 61.2V60.6C27.9473 60.6 26.5843 60.4373 25.0812 60L23.2136 60ZM0 64.2C2.53215 64.2 4.1155 63.837 6.68172 62.8795C6.79039 62.839 7.16116 62.7 7.21197 62.681C10.0919 61.6043 11.8941 61.2 15 61.2C18.0813 61.2 19.6086 61.5665 22.3886 62.6785C25.2336 63.8165 26.8313 64.2 30 64.2V63.6C26.9187 63.6 25.3914 63.2335 22.6114 62.1215C19.7664 60.9835 18.1687 60.6 15 60.6C11.8122 60.6 9.94344 61.0192 7.00185 62.119C6.95066 62.1381 6.58016 62.277 6.47198 62.3174C3.96706 63.2519 2.44905 63.6 0 63.6L0 64.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 60C6.46234 59.9609 6.57112 59.9208 6.68172 59.8795C6.79039 59.839 7.16116 59.7 7.21197 59.681C10.0919 58.6043 11.8941 58.2 15 58.2C18.0813 58.2 19.6086 58.5665 22.3886 59.6785C22.6753 59.7932 22.9494 59.9003 23.2136 60H25.0812C24.3279 59.7808 23.5395 59.4927 22.6114 59.1215C19.7664 57.9835 18.1687 57.6 15 57.6C11.8122 57.6 9.94344 58.0192 7.00185 59.119C6.95066 59.1381 6.58016 59.277 6.47198 59.3174C5.72954 59.5944 5.0738 59.8198 4.45483 60H6.35524ZM6.35524 54C3.97726 54.8676 2.42302 55.2 0 55.2L0 54.6C1.72318 54.6 2.98542 54.4277 4.45483 54H6.35524ZM23.2136 54C25.5716 54.8899 27.1507 55.2 30 55.2V54.6C27.9473 54.6 26.5843 54.4373 25.0812 54L23.2136 54ZM0 58.2C2.53215 58.2 4.1155 57.837 6.68172 56.8795C6.79039 56.839 7.16116 56.7 7.21197 56.681C10.0919 55.6043 11.8941 55.2 15 55.2C18.0813 55.2 19.6086 55.5665 22.3886 56.6785C25.2336 57.8165 26.8313 58.2 30 58.2V57.6C26.9187 57.6 25.3914 57.2335 22.6114 56.1215C19.7664 54.9835 18.1687 54.6 15 54.6C11.8122 54.6 9.94344 55.0192 7.00185 56.119C6.95066 56.1381 6.58016 56.277 6.47198 56.3174C3.96706 57.2519 2.44905 57.6 0 57.6L0 58.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 54C6.46234 53.9609 6.57112 53.9208 6.68172 53.8795C6.79039 53.839 7.16116 53.7 7.21197 53.681C10.0919 52.6043 11.8941 52.2 15 52.2C18.0813 52.2 19.6086 52.5665 22.3886 53.6785C22.6753 53.7932 22.9494 53.9003 23.2136 54H25.0812C24.3279 53.7808 23.5395 53.4927 22.6114 53.1215C19.7664 51.9835 18.1687 51.6 15 51.6C11.8122 51.6 9.94344 52.0192 7.00185 53.119C6.95066 53.1381 6.58016 53.277 6.47198 53.3174C5.72954 53.5944 5.0738 53.8198 4.45483 54H6.35524ZM6.35524 48C3.97726 48.8676 2.42302 49.2 0 49.2L0 48.6C1.72318 48.6 2.98542 48.4277 4.45483 48H6.35524ZM23.2136 48C25.5716 48.8899 27.1507 49.2 30 49.2V48.6C27.9473 48.6 26.5843 48.4373 25.0812 48L23.2136 48ZM0 52.2C2.53215 52.2 4.1155 51.837 6.68172 50.8795C6.79039 50.839 7.16116 50.7 7.21197 50.681C10.0919 49.6043 11.8941 49.2 15 49.2C18.0813 49.2 19.6086 49.5665 22.3886 50.6785C25.2336 51.8165 26.8313 52.2 30 52.2V51.6C26.9187 51.6 25.3914 51.2335 22.6114 50.1215C19.7664 48.9835 18.1687 48.6 15 48.6C11.8122 48.6 9.94344 49.0192 7.00185 50.119C6.95066 50.1381 6.58016 50.277 6.47198 50.3174C3.96706 51.2519 2.44905 51.6 0 51.6L0 52.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 48C6.46234 47.9609 6.57112 47.9208 6.68172 47.8795C6.79039 47.839 7.16116 47.7 7.21197 47.681C10.0919 46.6043 11.8941 46.2 15 46.2C18.0813 46.2 19.6086 46.5665 22.3886 47.6785C22.6753 47.7932 22.9494 47.9003 23.2136 48H25.0812C24.3279 47.7808 23.5395 47.4927 22.6114 47.1215C19.7664 45.9835 18.1687 45.6 15 45.6C11.8122 45.6 9.94344 46.0192 7.00185 47.119C6.95066 47.1381 6.58016 47.277 6.47198 47.3174C5.72954 47.5944 5.0738 47.8198 4.45483 48H6.35524ZM6.35524 42C3.97726 42.8676 2.42302 43.2 0 43.2L0 42.6C1.72318 42.6 2.98542 42.4277 4.45483 42H6.35524ZM23.2136 42C25.5716 42.8899 27.1507 43.2 30 43.2V42.6C27.9473 42.6 26.5843 42.4373 25.0812 42L23.2136 42ZM0 46.2C2.53215 46.2 4.1155 45.837 6.68172 44.8795C6.79039 44.839 7.16116 44.7 7.21197 44.681C10.0919 43.6043 11.8941 43.2 15 43.2C18.0813 43.2 19.6086 43.5665 22.3886 44.6785C25.2336 45.8165 26.8313 46.2 30 46.2V45.6C26.9187 45.6 25.3914 45.2335 22.6114 44.1215C19.7664 42.9835 18.1687 42.6 15 42.6C11.8122 42.6 9.94344 43.0192 7.00185 44.119C6.95066 44.1381 6.58016 44.277 6.47198 44.3174C3.96706 45.2519 2.44905 45.6 0 45.6L0 46.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 42C6.46234 41.9609 6.57112 41.9208 6.68172 41.8795C6.79039 41.839 7.16116 41.7 7.21197 41.681C10.0919 40.6043 11.8941 40.2 15 40.2C18.0813 40.2 19.6086 40.5665 22.3886 41.6785C22.6753 41.7932 22.9494 41.9003 23.2136 42H25.0812C24.3279 41.7808 23.5395 41.4927 22.6114 41.1215C19.7664 39.9835 18.1687 39.6 15 39.6C11.8122 39.6 9.94344 40.0192 7.00185 41.119C6.95066 41.1381 6.58016 41.277 6.47198 41.3174C5.72954 41.5944 5.0738 41.8198 4.45483 42H6.35524ZM6.35524 36C3.97726 36.8676 2.42302 37.2 0 37.2L0 36.6C1.72318 36.6 2.98542 36.4277 4.45483 36H6.35524ZM23.2136 36C25.5716 36.8899 27.1507 37.2 30 37.2V36.6C27.9473 36.6 26.5843 36.4373 25.0812 36L23.2136 36ZM0 40.2C2.53215 40.2 4.1155 39.837 6.68172 38.8795C6.79039 38.839 7.16116 38.7 7.21197 38.681C10.0919 37.6043 11.8941 37.2 15 37.2C18.0813 37.2 19.6086 37.5665 22.3886 38.6785C25.2336 39.8165 26.8313 40.2 30 40.2V39.6C26.9187 39.6 25.3914 39.2335 22.6114 38.1215C19.7664 36.9835 18.1687 36.6 15 36.6C11.8122 36.6 9.94344 37.0192 7.00185 38.119C6.95066 38.1381 6.58016 38.277 6.47198 38.3174C3.96706 39.2519 2.44905 39.6 0 39.6L0 40.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 36C6.46234 35.9609 6.57112 35.9208 6.68172 35.8795C6.79039 35.839 7.16116 35.7 7.21197 35.681C10.0919 34.6043 11.8941 34.2 15 34.2C18.0813 34.2 19.6086 34.5665 22.3886 35.6785C22.6753 35.7932 22.9494 35.9003 23.2136 36H25.0812C24.3279 35.7808 23.5395 35.4927 22.6114 35.1215C19.7664 33.9835 18.1687 33.6 15 33.6C11.8122 33.6 9.94344 34.0192 7.00185 35.119C6.95066 35.1381 6.58016 35.277 6.47198 35.3174C5.72954 35.5944 5.0738 35.8198 4.45483 36H6.35524ZM6.35524 30C3.97726 30.8676 2.42302 31.2 0 31.2L0 30.6C1.72318 30.6 2.98542 30.4277 4.45483 30H6.35524ZM23.2136 30C25.5716 30.8899 27.1507 31.2 30 31.2V30.6C27.9473 30.6 26.5843 30.4373 25.0812 30L23.2136 30ZM0 34.2C2.53215 34.2 4.1155 33.837 6.68172 32.8795C6.79039 32.839 7.16116 32.7 7.21197 32.681C10.0919 31.6043 11.8941 31.2 15 31.2C18.0813 31.2 19.6086 31.5665 22.3886 32.6785C25.2336 33.8165 26.8313 34.2 30 34.2V33.6C26.9187 33.6 25.3914 33.2335 22.6114 32.1215C19.7664 30.9835 18.1687 30.6 15 30.6C11.8122 30.6 9.94344 31.0192 7.00185 32.119C6.95066 32.1381 6.58016 32.277 6.47198 32.3174C3.96706 33.2519 2.44905 33.6 0 33.6L0 34.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 30C6.46234 29.9609 6.57112 29.9208 6.68172 29.8795C6.79039 29.839 7.16116 29.7 7.21197 29.681C10.0919 28.6043 11.8941 28.2 15 28.2C18.0813 28.2 19.6086 28.5665 22.3886 29.6785C22.6753 29.7932 22.9494 29.9003 23.2136 30H25.0812C24.3279 29.7808 23.5395 29.4927 22.6114 29.1215C19.7664 27.9835 18.1687 27.6 15 27.6C11.8122 27.6 9.94344 28.0192 7.00185 29.119C6.95066 29.1381 6.58016 29.277 6.47198 29.3174C5.72954 29.5944 5.0738 29.8198 4.45483 30H6.35524ZM6.35524 24C3.97726 24.8676 2.42302 25.2 0 25.2L0 24.6C1.72318 24.6 2.98542 24.4277 4.45483 24H6.35524ZM23.2136 24C25.5716 24.8899 27.1507 25.2 30 25.2V24.6C27.9473 24.6 26.5843 24.4373 25.0812 24L23.2136 24ZM0 28.2C2.53215 28.2 4.1155 27.837 6.68172 26.8795C6.79039 26.839 7.16116 26.7 7.21197 26.681C10.0919 25.6043 11.8941 25.2 15 25.2C18.0813 25.2 19.6086 25.5665 22.3886 26.6785C25.2336 27.8165 26.8313 28.2 30 28.2V27.6C26.9187 27.6 25.3914 27.2335 22.6114 26.1215C19.7664 24.9835 18.1687 24.6 15 24.6C11.8122 24.6 9.94344 25.0192 7.00185 26.119C6.95066 26.1381 6.58016 26.277 6.47198 26.3174C3.96706 27.2519 2.44905 27.6 0 27.6L0 28.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 24C6.46234 23.9609 6.57112 23.9208 6.68172 23.8795C6.79039 23.839 7.16116 23.7 7.21197 23.681C10.0919 22.6043 11.8941 22.2 15 22.2C18.0813 22.2 19.6086 22.5665 22.3886 23.6785C22.6753 23.7932 22.9494 23.9003 23.2136 24H25.0812C24.3279 23.7808 23.5395 23.4927 22.6114 23.1215C19.7664 21.9835 18.1687 21.6 15 21.6C11.8122 21.6 9.94344 22.0192 7.00185 23.119C6.95066 23.1381 6.58016 23.277 6.47198 23.3174C5.72954 23.5944 5.0738 23.8198 4.45483 24H6.35524ZM6.35524 18C3.97726 18.8676 2.42302 19.2 0 19.2L0 18.6C1.72318 18.6 2.98542 18.4277 4.45483 18H6.35524ZM23.2136 18C25.5716 18.8899 27.1507 19.2 30 19.2V18.6C27.9473 18.6 26.5843 18.4373 25.0812 18L23.2136 18ZM0 22.2C2.53215 22.2 4.1155 21.837 6.68172 20.8795C6.79039 20.839 7.16116 20.7 7.21197 20.681C10.0919 19.6043 11.8941 19.2 15 19.2C18.0813 19.2 19.6086 19.5665 22.3886 20.6785C25.2336 21.8165 26.8313 22.2 30 22.2V21.6C26.9187 21.6 25.3914 21.2335 22.6114 20.1215C19.7664 18.9835 18.1687 18.6 15 18.6C11.8122 18.6 9.94344 19.0192 7.00185 20.119C6.95066 20.1381 6.58016 20.277 6.47198 20.3174C3.96706 21.2519 2.44905 21.6 0 21.6L0 22.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 18C6.46234 17.9609 6.57112 17.9208 6.68172 17.8795C6.79039 17.839 7.16116 17.7 7.21197 17.681C10.0919 16.6043 11.8941 16.2 15 16.2C18.0813 16.2 19.6086 16.5665 22.3886 17.6785C22.6753 17.7932 22.9494 17.9003 23.2136 18H25.0812C24.3279 17.7808 23.5395 17.4927 22.6114 17.1215C19.7664 15.9835 18.1687 15.6 15 15.6C11.8122 15.6 9.94344 16.0192 7.00185 17.119C6.95066 17.1381 6.58016 17.277 6.47198 17.3174C5.72954 17.5944 5.0738 17.8198 4.45483 18H6.35524ZM6.35524 12C3.97726 12.8676 2.42302 13.2 0 13.2L0 12.6C1.72318 12.6 2.98542 12.4277 4.45483 12H6.35524ZM23.2136 12C25.5716 12.8899 27.1507 13.2 30 13.2V12.6C27.9473 12.6 26.5843 12.4373 25.0812 12L23.2136 12ZM0 16.2C2.53215 16.2 4.1155 15.837 6.68172 14.8795C6.79039 14.839 7.16116 14.7 7.21197 14.681C10.0919 13.6043 11.8941 13.2 15 13.2C18.0813 13.2 19.6086 13.5665 22.3886 14.6785C25.2336 15.8165 26.8313 16.2 30 16.2V15.6C26.9187 15.6 25.3914 15.2335 22.6114 14.1215C19.7664 12.9835 18.1687 12.6 15 12.6C11.8122 12.6 9.94344 13.0192 7.00185 14.119C6.95066 14.1381 6.58016 14.277 6.47198 14.3174C3.96706 15.2519 2.44905 15.6 0 15.6L0 16.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 12C6.46234 11.9609 6.57112 11.9208 6.68172 11.8795C6.79039 11.839 7.16116 11.7 7.21197 11.681C10.0919 10.6043 11.8941 10.2 15 10.2C18.0813 10.2 19.6086 10.5665 22.3886 11.6785C22.6753 11.7932 22.9494 11.9003 23.2136 12H25.0812C24.3279 11.7808 23.5395 11.4927 22.6114 11.1215C19.7664 9.98347 18.1687 9.6 15 9.6C11.8122 9.6 9.94344 10.0192 7.00185 11.119C6.95066 11.1381 6.58016 11.277 6.47198 11.3174C5.72954 11.5944 5.0738 11.8198 4.45483 12H6.35524ZM6.35524 6C3.97726 6.86758 2.42302 7.2 0 7.2L0 6.6C1.72318 6.6 2.98542 6.42769 4.45483 6H6.35524ZM23.2136 6C25.5716 6.88993 27.1507 7.2 30 7.2V6.6C27.9473 6.6 26.5843 6.43734 25.0812 6L23.2136 6ZM0 10.2C2.53215 10.2 4.1155 9.83697 6.68172 8.8795C6.79039 8.83895 7.16116 8.7 7.21197 8.681C10.0919 7.60427 11.8941 7.2 15 7.2C18.0813 7.2 19.6086 7.56653 22.3886 8.67854C25.2336 9.81653 26.8313 10.2 30 10.2V9.6C26.9187 9.6 25.3914 9.23347 22.6114 8.12146C19.7664 6.98347 18.1687 6.6 15 6.6C11.8122 6.6 9.94344 7.01922 7.00185 8.119C6.95066 8.13814 6.58016 8.27699 6.47198 8.31735C3.96706 9.25195 2.44905 9.6 0 9.6L0 10.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35524 6C6.46234 5.96092 6.57112 5.92076 6.68172 5.8795C6.79039 5.83895 7.16116 5.7 7.21197 5.681C10.0919 4.60427 11.8941 4.2 15 4.2C18.0813 4.2 19.6086 4.56653 22.3886 5.67854C22.6753 5.79323 22.9494 5.90026 23.2136 6H25.0812C24.3279 5.78083 23.5395 5.49269 22.6114 5.12146C19.7664 3.98347 18.1687 3.6 15 3.6C11.8122 3.6 9.94344 4.01922 7.00185 5.119C6.95066 5.13814 6.58016 5.27699 6.47198 5.31735C5.72954 5.59436 5.0738 5.81984 4.45483 6H6.35524ZM6.35524 0C3.97726 0.867585 2.42302 1.2 0 1.2L0 0.6C1.72318 0.6 2.98542 0.427692 4.45483 0L6.35524 0ZM23.2136 0C25.5716 0.88993 27.1507 1.2 30 1.2V0.6C27.9473 0.6 26.5843 0.437343 25.0812 0L23.2136 0ZM0 4.2C2.53215 4.2 4.1155 3.83697 6.68172 2.8795C6.79039 2.83895 7.16116 2.7 7.21197 2.681C10.0919 1.60427 11.8941 1.2 15 1.2C18.0813 1.2 19.6086 1.56653 22.3886 2.67854C25.2336 3.81653 26.8313 4.2 30 4.2V3.6C26.9187 3.6 25.3914 3.23347 22.6114 2.12146C19.7664 0.983469 18.1687 0.6 15 0.6C11.8122 0.6 9.94344 1.01922 7.00185 2.119C6.95066 2.13814 6.58016 2.27699 6.47198 2.31735C3.96706 3.25195 2.44905 3.6 0 3.6L0 4.2Z" fill="#858AFA" stroke="#858AFA" stroke-width="0.5"/>
</g>
<defs>
<clipPath id="clip0_954_5239">
<rect width="5" height="80" fill="white" transform="matrix(1 0 0 -1 0 80)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,3 +1,5 @@
import { browserHistory } from 'mastodon/components/router';
import api, { getLinks } from '../api';
import {
@ -676,3 +678,13 @@ export const updateAccount = ({ displayName, note, avatar, header, discoverable,
dispatch(importFetchedAccount(response.data));
});
};
export const navigateToProfile = (accountId) => {
return (_dispatch, getState) => {
const acct = getState().accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}`);
}
};
};

View file

@ -4,6 +4,7 @@ import axios from 'axios';
import { throttle } from 'lodash';
import api from 'mastodon/api';
import { browserHistory } from 'mastodon/components/router';
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'mastodon/settings';
@ -88,9 +89,9 @@ const messages = defineMessages({
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
export const ensureComposeIsVisible = (getState) => {
if (!getState().getIn(['compose', 'mounted'])) {
routerHistory.push('/publish');
browserHistory.push('/publish');
}
};
@ -110,14 +111,26 @@ export function changeCompose(text) {
};
}
export function replyCompose(status, routerHistory) {
export function replyCompose(status) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_REPLY,
status: status,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function replyComposeById(statusId) {
return (dispatch, getState) => {
const state = getState();
const status = state.statuses.get(statusId);
if (status) {
const account = state.accounts.get(status.get('account'));
dispatch(replyCompose(status.set('account', account)));
}
};
}
@ -133,38 +146,44 @@ export function resetCompose() {
};
}
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
export const focusCompose = (defaultText) => (dispatch, getState) => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
export function mentionCompose(account, routerHistory) {
export function mentionCompose(account) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_MENTION,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function directCompose(account, routerHistory) {
export function mentionComposeById(accountId) {
return (dispatch, getState) => {
dispatch(mentionCompose(getState().accounts.get(accountId)));
};
}
export function directCompose(account) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_DIRECT,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
};
}
export function submitCompose(routerHistory) {
export function submitCompose() {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
@ -214,8 +233,8 @@ export function submitCompose(routerHistory) {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
if (routerHistory && (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new') && window.history.state) {
routerHistory.goBack();
if ((browserHistory.location.pathname === '/publish' || browserHistory.location.pathname === '/statuses/new') && window.history.state) {
browserHistory.goBack();
}
dispatch(insertIntoTagHistory(response.data.tags, status));
@ -249,7 +268,7 @@ export function submitCompose(routerHistory) {
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}).catch(function (error) {
dispatch(submitComposeFail(error));

View file

@ -1,7 +1,11 @@
import { boostModal } from 'mastodon/initial_state';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatus } from './importer';
import { unreblog, reblog } from './interactions_typed';
import { openModal } from './modal';
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
@ -432,3 +436,49 @@ export function unpinFail(status, error) {
skipLoading: true,
};
}
function toggleReblogWithoutConfirmation(status, visibility) {
return (dispatch) => {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
dispatch(reblog({ statusId: status.get('id'), visibility }));
}
};
}
export function toggleReblog(statusId, skipModal = false) {
return (dispatch, getState) => {
const state = getState();
let status = state.statuses.get(statusId);
if (!status)
return;
// The reblog modal expects a pre-filled account in status
// TODO: fix this by having the reblog modal get a statusId and do the work itself
status = status.set('account', state.accounts.get(status.get('account')));
if (boostModal && !skipModal) {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)) } }));
} else {
dispatch(toggleReblogWithoutConfirmation(status));
}
};
}
export function toggleFavourite(statusId) {
return (dispatch, getState) => {
const state = getState();
const status = state.statuses.get(statusId);
if (!status)
return;
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
};
}

View file

@ -75,9 +75,17 @@ interface MarkerParam {
}
function getLastNotificationId(state: RootState): string | undefined {
// @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return state.getIn(['notifications', 'lastReadId']);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const enableBeta = state.settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return enableBeta
? state.notificationGroups.lastReadId
: // @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
state.getIn(['notifications', 'lastReadId']);
}
const buildPostMarkersParams = (state: RootState) => {

View file

@ -0,0 +1,231 @@
import { createAction } from '@reduxjs/toolkit';
import {
apiClearNotifications,
apiFetchNotifications,
} from 'mastodon/api/notifications';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
} from 'mastodon/api_types/notifications';
import { allNotificationTypes } from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
import { usePendingItems } from 'mastodon/initial_state';
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
import {
selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsShows,
} from 'mastodon/selectors/settings';
import type { AppDispatch } from 'mastodon/store';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from 'mastodon/store/typed_functions';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings';
function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter((item) => item !== filter);
}
function dispatchAssociatedRecords(
dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
notifications.forEach((notification) => {
if (notification.type === 'admin.report') {
fetchedAccounts.push(notification.report.target_account);
}
if (notification.type === 'moderation_warning') {
fetchedAccounts.push(notification.moderation_warning.target_account);
}
if ('status' in notification && notification.status) {
fetchedStatuses.push(notification.status);
}
});
if (fetchedAccounts.length > 0)
dispatch(importFetchedAccounts(fetchedAccounts));
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
}
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) => {
const activeFilter =
selectSettingsNotificationsQuickFilterActive(getState());
return apiFetchNotifications({
exclude_types:
activeFilter === 'all'
? selectSettingsNotificationsExcludedTypes(getState())
: excludeAllTypesExcept(activeFilter),
});
},
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
notifications;
// TODO: might be worth not using gaps for that…
// if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
if (notifications.length > 1)
payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id });
return payload;
// dispatch(submitMarkers());
},
);
export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }) =>
apiFetchNotifications({ max_id: params.gap.maxId }),
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotifications({
max_id: undefined,
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
since_id: usePendingItems
? getState().notificationGroups.groups.find(
(group) => group.type !== 'gap',
)?.page_max_id
: undefined,
});
},
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const processNewNotificationForGroups = createAppAsyncThunk(
'notificationGroups/processNew',
(notification: ApiNotificationJSON, { dispatch, getState }) => {
const state = getState();
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
const notificationShows = selectSettingsNotificationsShows(state);
const showInColumn =
activeFilter === 'all'
? notificationShows[notification.type]
: activeFilter === notification.type;
if (!showInColumn) return;
if (
(notification.type === 'mention' || notification.type === 'update') &&
notification.status?.filtered
) {
const filters = notification.status.filtered.filter((result) =>
result.filter.context.includes('notifications'),
);
if (filters.some((result) => result.filter.filter_action === 'hide')) {
return;
}
}
dispatchAssociatedRecords(dispatch, [notification]);
return notification;
},
);
export const loadPending = createAction('notificationGroups/loadPending');
export const updateScrollPosition = createAppAsyncThunk(
'notificationGroups/updateScrollPosition',
({ top }: { top: boolean }, { dispatch, getState }) => {
if (
top &&
getState().notificationGroups.mergedNotifications === 'needs-reload'
) {
void dispatch(fetchNotifications());
}
return { top };
},
);
export const setNotificationsFilter = createAppAsyncThunk(
'notifications/filter/set',
({ filterType }: { filterType: string }, { dispatch }) => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ['notifications', 'quickFilter', 'active'],
value: filterType,
});
// dispatch(expandNotifications({ forceLoad: true }));
void dispatch(fetchNotifications());
dispatch(saveSettings());
},
);
export const clearNotifications = createDataLoadingThunk(
'notifications/clear',
() => apiClearNotifications(),
);
export const markNotificationsAsRead = createAction(
'notificationGroups/markAsRead',
);
export const mountNotifications = createAppAsyncThunk(
'notificationGroups/mount',
(_, { dispatch, getState }) => {
const state = getState();
if (
state.notificationGroups.mounted === 0 &&
state.notificationGroups.mergedNotifications === 'needs-reload'
) {
void dispatch(fetchNotifications());
}
},
);
export const unmountNotifications = createAction('notificationGroups/unmount');
export const refreshStaleNotificationGroups = createAppAsyncThunk<{
deferredRefresh: boolean;
}>('notificationGroups/refreshStale', (_, { dispatch, getState }) => {
const state = getState();
if (
state.notificationGroups.scrolledToTop ||
!state.notificationGroups.mounted
) {
void dispatch(fetchNotifications());
return { deferredRefresh: false };
}
return { deferredRefresh: true };
});

View file

@ -1,3 +1,5 @@
import { createAction } from '@reduxjs/toolkit';
import {
apiGetNotificationPolicy,
apiUpdateNotificationsPolicy,
@ -14,3 +16,7 @@ export const updateNotificationsPolicy = createDataLoadingThunk(
'notificationPolicy/update',
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
);
export const decreasePendingNotificationsCount = createAction<number>(
'notificationPolicy/decreasePendingNotificationCount',
);

View file

@ -18,6 +18,7 @@ import {
importFetchedStatuses,
} from './importer';
import { submitMarkers } from './markers';
import { decreasePendingNotificationsCount } from './notification_policies';
import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings';
@ -32,7 +33,6 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
@ -64,6 +64,14 @@ export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMIS
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST';
export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
@ -85,6 +93,12 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
}
};
const selectNotificationCountForRequest = (state, id) => {
const requests = state.getIn(['notificationRequests', 'items']);
const thisRequest = requests.find(request => request.get('id') === id);
return thisRequest ? thisRequest.get('notifications_count') : 0;
};
export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING,
});
@ -174,8 +188,8 @@ const noOp = () => {};
let expandNotificationsController = new AbortController();
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
return (dispatch, getState) => {
export function expandNotifications({ maxId = undefined, forceLoad = false }) {
return async (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const notifications = getState().get('notifications');
const isLoadingMore = !!maxId;
@ -185,7 +199,6 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
expandNotificationsController.abort();
expandNotificationsController = new AbortController();
} else {
done();
return;
}
}
@ -212,7 +225,8 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
dispatch(expandNotificationsRequest(isLoadingMore));
api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => {
try {
const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal });
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
@ -222,11 +236,9 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
fetchRelatedRelationships(dispatch, response.data);
dispatch(submitMarkers());
}).catch(error => {
} catch(error) {
dispatch(expandNotificationsFail(error, isLoadingMore));
}).finally(() => {
done();
});
}
};
}
@ -257,16 +269,6 @@ export function expandNotificationsFail(error, isLoadingMore) {
};
}
export function clearNotifications() {
return (dispatch) => {
dispatch({
type: NOTIFICATIONS_CLEAR,
});
api().post('/api/v1/notifications/clear');
};
}
export function scrollTopNotifications(top) {
return {
type: NOTIFICATIONS_SCROLL_TOP,
@ -444,11 +446,13 @@ export const fetchNotificationRequestFail = (id, error) => ({
error,
});
export const acceptNotificationRequest = id => (dispatch) => {
export const acceptNotificationRequest = (id) => (dispatch, getState) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(acceptNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
dispatch(acceptNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(id, err));
});
@ -470,11 +474,13 @@ export const acceptNotificationRequestFail = (id, error) => ({
error,
});
export const dismissNotificationRequest = id => (dispatch) => {
export const dismissNotificationRequest = (id) => (dispatch, getState) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(dismissNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
dispatch(dismissNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(id, err));
});
@ -496,6 +502,62 @@ export const dismissNotificationRequestFail = (id, error) => ({
error,
});
export const acceptNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => {
dispatch(acceptNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(ids, err));
});
};
export const acceptNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
ids,
});
export const acceptNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS,
ids,
});
export const acceptNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_ACCEPT_FAIL,
ids,
error,
});
export const dismissNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => {
dispatch(dismissNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(ids, err));
});
};
export const dismissNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_REQUEST,
ids,
});
export const dismissNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS,
ids,
});
export const dismissNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_DISMISS_FAIL,
ids,
error,
});
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
const params = { account_id: accountId };

View file

@ -0,0 +1,18 @@
import { createAppAsyncThunk } from 'mastodon/store';
import { fetchNotifications } from './notification_groups';
import { expandNotifications } from './notifications';
export const initializeNotifications = createAppAsyncThunk(
'notifications/initialize',
(_, { dispatch, getState }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const enableBeta = getState().settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;
if (enableBeta) void dispatch(fetchNotifications());
else void dispatch(expandNotifications({}));
},
);

View file

@ -1,11 +1,6 @@
import { createAction } from '@reduxjs/toolkit';
import type { ApiAccountJSON } from '../api_types/accounts';
// To be replaced once ApiNotificationJSON type exists
interface FakeApiNotificationJSON {
type: string;
account: ApiAccountJSON;
}
import type { ApiNotificationJSON } from 'mastodon/api_types/notifications';
export const notificationsUpdate = createAction(
'notifications/update',
@ -13,7 +8,7 @@ export const notificationsUpdate = createAction(
playSound,
...args
}: {
notification: FakeApiNotificationJSON;
notification: ApiNotificationJSON;
usePendingItems: boolean;
playSound: boolean;
}) => ({

View file

@ -1,3 +1,5 @@
import { browserHistory } from 'mastodon/components/router';
import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
@ -93,7 +95,7 @@ export function redraft(status, raw_text) {
};
}
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
export const editStatus = (id) => (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
if (status.get('poll')) {
@ -104,7 +106,7 @@ export const editStatus = (id, routerHistory) => (dispatch, getState) => {
api().get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch(fetchStatusSourceSuccess());
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
}).catch(error => {
dispatch(fetchStatusSourceFail(error));
@ -124,7 +126,7 @@ export const fetchStatusSourceFail = error => ({
error,
});
export function deleteStatus(id, routerHistory, withRedraft = false) {
export function deleteStatus(id, withRedraft = false) {
return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
@ -141,7 +143,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
if (withRedraft) {
dispatch(redraft(status, response.data.text));
ensureComposeIsVisible(getState, routerHistory);
ensureComposeIsVisible(getState);
}
}).catch(error => {
dispatch(deleteStatusFail(id, error));
@ -308,6 +310,21 @@ export function revealStatus(ids) {
};
}
export function toggleStatusSpoilers(statusId) {
return (dispatch, getState) => {
const status = getState().statuses.get(statusId);
if (!status)
return;
if (status.get('hidden')) {
dispatch(revealStatus(statusId));
} else {
dispatch(hideStatus(statusId));
}
};
}
export function toggleStatusCollapse(id, isCollapsed) {
return {
type: STATUS_COLLAPSE,
@ -348,3 +365,15 @@ export const undoStatusTranslation = (id, pollId) => ({
id,
pollId,
});
export const navigateToStatus = (statusId) => {
return (_dispatch, getState) => {
const state = getState();
const accountId = state.statuses.getIn([statusId, 'account']);
const acct = state.accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}/${statusId}`);
}
};
};

View file

@ -10,6 +10,7 @@ import {
deleteAnnouncement,
} from './announcements';
import { updateConversations } from './conversations';
import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
import { updateNotifications, expandNotifications } from './notifications';
import { updateStatus } from './statuses';
import {
@ -36,7 +37,7 @@ const randomUpTo = max =>
* @param {string} channelName
* @param {Object.<string, string>} params
* @param {Object} options
* @param {function(Function, Function): void} [options.fallback]
* @param {function(Function, Function): Promise<void>} [options.fallback]
* @param {function(): void} [options.fillGaps]
* @param {function(object): boolean} [options.accept]
* @returns {function(): void}
@ -51,14 +52,13 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
let pollingId;
/**
* @param {function(Function, Function): void} fallback
* @param {function(Function, Function): Promise<void>} fallback
*/
const useFallback = fallback => {
fallback(dispatch, () => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
});
const useFallback = async fallback => {
await fallback(dispatch, getState);
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
};
return {
@ -98,9 +98,23 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
case 'notification': {
// @ts-expect-error
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
const notificationJSON = JSON.parse(data.payload);
dispatch(updateNotifications(notificationJSON, messages, locale));
// TODO: remove this once the groups feature replaces the previous one
if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
dispatch(processNewNotificationForGroups(notificationJSON));
}
break;
}
case 'notifications_merged':
const state = getState();
if (state.notifications.top || !state.notifications.mounted)
dispatch(expandNotifications({ forceLoad: true, maxId: undefined }));
if(state.settings.getIn(['notifications', 'groupingBeta'], false)) {
dispatch(refreshStaleNotificationGroups());
}
break;
case 'conversation':
// @ts-expect-error
@ -125,21 +139,30 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
/**
* @param {Function} dispatch
* @param {function(): void} done
* @param {Function} getState
*/
const refreshHomeTimelineAndNotification = (dispatch, done) => {
// @ts-expect-error
dispatch(expandHomeTimeline({}, () =>
// @ts-expect-error
dispatch(expandNotifications({}, () =>
dispatch(fetchAnnouncements(done))))));
};
async function refreshHomeTimelineAndNotification(dispatch, getState) {
await dispatch(expandHomeTimeline({ maxId: undefined }));
// TODO: remove this once the groups feature replaces the previous one
if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
// TODO: polling for merged notifications
try {
await dispatch(pollRecentGroupNotifications());
} catch (error) {
// TODO
}
} else {
await dispatch(expandNotifications({}));
}
await dispatch(fetchAnnouncements());
}
/**
* @returns {function(): void}
*/
export const connectUserStream = () =>
// @ts-expect-error
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
/**

View file

@ -76,21 +76,18 @@ export function clearTimeline(timeline) {
};
}
const noOp = () => {};
const parseTags = (tags = {}, mode) => {
return (tags[mode] || []).map((tag) => {
return tag.value;
});
};
export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => {
export function expandTimeline(timelineId, path, params = {}) {
return async (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
const isLoadingMore = !!params.max_id;
if (timeline.get('isLoading')) {
done();
return;
}
@ -109,7 +106,8 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
api().get(path, { params }).then(response => {
try {
const response = await api().get(path, { params });
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
@ -127,52 +125,48 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
if (timelineId === 'home') {
dispatch(submitMarkers());
}
}).catch(error => {
} catch(error) {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
}).finally(() => {
done();
});
}
};
}
export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => {
export function fillTimelineGaps(timelineId, path, params = {}) {
return async (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
const items = timeline.get('items');
const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
// Only expand at most two gaps to avoid doing too many requests
done = gaps.take(2).reduce((done, maxId) => {
return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
}, done);
done();
for (const maxId of gaps.take(2)) {
await dispatch(expandTimeline(timelineId, path, { ...params, maxId }));
}
};
}
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia });
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia });
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export const expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
export const expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId });
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => {
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
any: parseTags(tags, 'any'),
all: parseTags(tags, 'all'),
none: parseTags(tags, 'none'),
local: local,
}, done);
});
};
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done);
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
export const fillHomeTimelineGaps = () => fillTimelineGaps('home', '/api/v1/timelines/home', {});
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia });
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia });
export const fillListTimelineGaps = (id) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {});
export function expandTimelineRequest(timeline, isLoadingMore) {
return {

View file

@ -2,8 +2,8 @@ import { apiRequestGet, apiRequestPut } from 'mastodon/api';
import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies';
export const apiGetNotificationPolicy = () =>
apiRequestGet<NotificationPolicyJSON>('/v1/notifications/policy');
apiRequestGet<NotificationPolicyJSON>('v2/notifications/policy');
export const apiUpdateNotificationsPolicy = (
policy: Partial<NotificationPolicyJSON>,
) => apiRequestPut<NotificationPolicyJSON>('/v1/notifications/policy', policy);
) => apiRequestPut<NotificationPolicyJSON>('v2/notifications/policy', policy);

View file

@ -0,0 +1,26 @@
import api, { apiRequest, getLinks } from 'mastodon/api';
import type { ApiNotificationGroupsResultJSON } from 'mastodon/api_types/notifications';
export const apiFetchNotifications = async (params?: {
exclude_types?: string[];
max_id?: string;
since_id?: string;
}) => {
const response = await api().request<ApiNotificationGroupsResultJSON>({
method: 'GET',
url: '/api/v2_alpha/notifications',
params,
});
const { statuses, accounts, notification_groups } = response.data;
return {
statuses,
accounts,
notifications: notification_groups,
links: getLinks(response),
};
};
export const apiClearNotifications = () =>
apiRequest<undefined>('POST', 'v1/notifications/clear');

View file

@ -1,10 +1,13 @@
// See app/serializers/rest/notification_policy_serializer.rb
export type NotificationPolicyValue = 'accept' | 'filter' | 'drop';
export interface NotificationPolicyJSON {
filter_not_following: boolean;
filter_not_followers: boolean;
filter_new_accounts: boolean;
filter_private_mentions: boolean;
for_not_following: NotificationPolicyValue;
for_not_followers: NotificationPolicyValue;
for_new_accounts: NotificationPolicyValue;
for_private_mentions: NotificationPolicyValue;
for_limited_accounts: NotificationPolicyValue;
summary: {
pending_requests_count: number;
pending_notifications_count: number;

View file

@ -0,0 +1,151 @@
// See app/serializers/rest/notification_group_serializer.rb
import type { AccountWarningAction } from 'mastodon/models/notification_group';
import type { ApiAccountJSON } from './accounts';
import type { ApiReportJSON } from './reports';
import type { ApiStatusJSON } from './statuses';
// See app/model/notification.rb
export const allNotificationTypes = [
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update',
'admin.sign_up',
'admin.report',
'moderation_warning',
'severed_relationships',
];
export type NotificationWithStatusType =
| 'favourite'
| 'reblog'
| 'status'
| 'mention'
| 'poll'
| 'update';
export type NotificationType =
| NotificationWithStatusType
| 'follow'
| 'follow_request'
| 'moderation_warning'
| 'severed_relationships'
| 'admin.sign_up'
| 'admin.report';
export interface BaseNotificationJSON {
id: string;
type: NotificationType;
created_at: string;
group_key: string;
account: ApiAccountJSON;
}
export interface BaseNotificationGroupJSON {
group_key: string;
notifications_count: number;
type: NotificationType;
sample_account_ids: string[];
latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly
most_recent_notification_id: string;
page_min_id?: string;
page_max_id?: string;
}
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
type: NotificationWithStatusType;
status_id: string | null;
}
interface NotificationWithStatusJSON extends BaseNotificationJSON {
type: NotificationWithStatusType;
status: ApiStatusJSON | null;
}
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'admin.report';
report: ApiReportJSON;
}
interface ReportNotificationJSON extends BaseNotificationJSON {
type: 'admin.report';
report: ApiReportJSON;
}
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
type: SimpleNotificationTypes;
}
interface SimpleNotificationJSON extends BaseNotificationJSON {
type: SimpleNotificationTypes;
}
export interface ApiAccountWarningJSON {
id: string;
action: AccountWarningAction;
text: string;
status_ids: string[];
created_at: string;
target_account: ApiAccountJSON;
appeal: unknown;
}
interface ModerationWarningNotificationGroupJSON
extends BaseNotificationGroupJSON {
type: 'moderation_warning';
moderation_warning: ApiAccountWarningJSON;
}
interface ModerationWarningNotificationJSON extends BaseNotificationJSON {
type: 'moderation_warning';
moderation_warning: ApiAccountWarningJSON;
}
export interface ApiAccountRelationshipSeveranceEventJSON {
id: string;
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
purged: boolean;
target_name: string;
followers_count: number;
following_count: number;
created_at: string;
}
interface AccountRelationshipSeveranceNotificationGroupJSON
extends BaseNotificationGroupJSON {
type: 'severed_relationships';
event: ApiAccountRelationshipSeveranceEventJSON;
}
interface AccountRelationshipSeveranceNotificationJSON
extends BaseNotificationJSON {
type: 'severed_relationships';
event: ApiAccountRelationshipSeveranceEventJSON;
}
export type ApiNotificationJSON =
| SimpleNotificationJSON
| ReportNotificationJSON
| AccountRelationshipSeveranceNotificationJSON
| NotificationWithStatusJSON
| ModerationWarningNotificationJSON;
export type ApiNotificationGroupJSON =
| SimpleNotificationGroupJSON
| ReportNotificationGroupJSON
| AccountRelationshipSeveranceNotificationGroupJSON
| NotificationGroupWithStatusJSON
| ModerationWarningNotificationGroupJSON;
export interface ApiNotificationGroupsResultJSON {
accounts: ApiAccountJSON[];
statuses: ApiStatusJSON[];
notification_groups: ApiNotificationGroupJSON[];
}

View file

@ -0,0 +1,16 @@
import type { ApiAccountJSON } from './accounts';
export type ReportCategory = 'other' | 'spam' | 'legal' | 'violation';
export interface ApiReportJSON {
id: string;
action_taken: unknown;
action_taken_at: unknown;
category: ReportCategory;
comment: string;
forwarded: boolean;
created_at: string;
status_ids: string[];
rule_ids: string[];
target_account: ApiAccountJSON;
}

View file

@ -58,6 +58,29 @@ export interface ApiPreviewCardJSON {
authors: ApiPreviewCardAuthorJSON[];
}
export type FilterContext =
| 'home'
| 'notifications'
| 'public'
| 'thread'
| 'account';
export interface ApiFilterJSON {
id: string;
title: string;
context: FilterContext;
expires_at: string;
filter_action: 'warn' | 'hide';
keywords?: unknown[]; // TODO: FilterKeywordSerializer
statuses?: unknown[]; // TODO: FilterStatusSerializer
}
export interface ApiFilterResultJSON {
filter: ApiFilterJSON;
keyword_matches: string[];
status_matches: string[];
}
export interface ApiStatusJSON {
id: string;
created_at: string;
@ -80,8 +103,7 @@ export interface ApiStatusJSON {
bookmarked?: boolean;
pinned?: boolean;
// filtered: FilterResult[]
filtered: unknown; // TODO
filtered?: ApiFilterResultJSON[];
content?: string;
text?: string;

View file

@ -1,5 +1,4 @@
import Rails from '@rails/ujs';
import 'font-awesome/css/font-awesome.css';
export function start() {
require.context('../images/', true, /\.(jpg|png|svg)$/);

View file

@ -165,7 +165,7 @@ describe('computeHashtagBarForStatus', () => {
);
});
it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
it('does not put the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
const status = createStatus(
'<p>This is my content! <a href="test">#hashtag</a></p>',
['hashtag'],

View file

@ -106,7 +106,7 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica
</>
);
} else if (defaultAction === 'mute') {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={handleMute} />;
buttons = <Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />;
} else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
} else if (!account.get('suspended') && !account.get('moved') || following) {

View file

@ -153,7 +153,7 @@ class ReportReasonSelector extends PureComponent {
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === 'legal'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled || rules.length === 0}>
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
</Category>
</div>

View file

@ -11,6 +11,8 @@ interface Props {
style?: React.CSSProperties;
inline?: boolean;
animate?: boolean;
counter?: number | string;
counterBorderColor?: string;
}
export const Avatar: React.FC<Props> = ({
@ -19,6 +21,8 @@ export const Avatar: React.FC<Props> = ({
size = 20,
inline = false,
style: styleFromParent,
counter,
counterBorderColor,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
@ -43,6 +47,14 @@ export const Avatar: React.FC<Props> = ({
style={style}
>
{src && <img src={src} alt='' />}
{counter && (
<div
className='account__avatar__counter'
style={{ borderColor: counterBorderColor }}
>
{counter}
</div>
)}
</div>
);
};

View file

@ -1,5 +1,6 @@
import classNames from 'classnames';
import CheckIndeterminateSmallIcon from '@/material-icons/400-24px/check_indeterminate_small.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from './icon';
@ -7,6 +8,7 @@ import { Icon } from './icon';
interface Props {
value: string;
checked: boolean;
indeterminate: boolean;
name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode;
@ -16,6 +18,7 @@ export const CheckBox: React.FC<Props> = ({
name,
value,
checked,
indeterminate,
onChange,
label,
}) => {
@ -29,8 +32,14 @@ export const CheckBox: React.FC<Props> = ({
onChange={onChange}
/>
<span className={classNames('check-box__input', { checked })}>
{checked && <Icon id='check' icon={DoneIcon} />}
<span
className={classNames('check-box__input', { checked, indeterminate })}
>
{indeterminate ? (
<Icon id='indeterminate' icon={CheckIndeterminateSmallIcon} />
) : (
checked && <Icon id='check' icon={DoneIcon} />
)}
</span>
<span>{label}</span>

View file

@ -0,0 +1,15 @@
import { StatusBanner, BannerVariant } from './status_banner';
export const ContentWarning: React.FC<{
text: string;
expanded?: boolean;
onClick?: () => void;
}> = ({ text, expanded, onClick }) => (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Yellow}
>
<p dangerouslySetInnerHTML={{ __html: text }} />
</StatusBanner>
);

View file

@ -3,6 +3,8 @@ import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
import { unblockDomain } from 'mastodon/actions/domain_blocks';
import { useAppDispatch } from 'mastodon/store';
import { IconButton } from './icon_button';
@ -13,17 +15,15 @@ const messages = defineMessages({
},
});
interface Props {
export const Domain: React.FC<{
domain: string;
onUnblockDomain: (domain: string) => void;
}
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
}> = ({ domain }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const handleDomainUnblock = useCallback(() => {
onUnblockDomain(domain);
}, [domain, onUnblockDomain]);
dispatch(unblockDomain(domain));
}, [dispatch, domain]);
return (
<div className='domain'>

View file

@ -20,7 +20,7 @@ let id = 0;
class DropdownMenu extends PureComponent {
static propTypes = {
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
items: PropTypes.array.isRequired,
loading: PropTypes.bool,
scrollable: PropTypes.bool,
onClose: PropTypes.func.isRequired,
@ -39,6 +39,7 @@ class DropdownMenu extends PureComponent {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
e.stopPropagation();
e.preventDefault();
}
};
@ -164,7 +165,7 @@ class Dropdown extends PureComponent {
children: PropTypes.node,
icon: PropTypes.string,
iconComponent: PropTypes.func,
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]),
items: PropTypes.array.isRequired,
loading: PropTypes.bool,
size: PropTypes.number,
title: PropTypes.string,

View file

@ -0,0 +1,185 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { supportsPassiveEvents } from 'detect-passive-events';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import type { IconProp } from './icon';
import { Icon } from './icon';
const listenerOptions = supportsPassiveEvents
? { passive: true, capture: true }
: true;
export interface SelectItem {
value: string;
icon?: string;
iconComponent?: IconProp;
text: string;
meta: string;
extra?: string;
}
interface Props {
value: string;
classNamePrefix: string;
style?: React.CSSProperties;
items: SelectItem[];
onChange: (value: string) => void;
onClose: () => void;
}
export const DropdownSelector: React.FC<Props> = ({
style,
items,
value,
classNamePrefix = 'privacy-dropdown',
onClose,
onChange,
}) => {
const nodeRef = useRef<HTMLUListElement>(null);
const focusedItemRef = useRef<HTMLLIElement>(null);
const [currentValue, setCurrentValue] = useState(value);
const handleDocumentClick = useCallback(
(e: MouseEvent | TouchEvent) => {
if (
nodeRef.current &&
e.target instanceof Node &&
!nodeRef.current.contains(e.target)
) {
onClose();
e.stopPropagation();
}
},
[nodeRef, onClose],
);
const handleClick = useCallback(
(
e: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>,
) => {
const value = e.currentTarget.getAttribute('data-index');
e.preventDefault();
onClose();
if (value) onChange(value);
},
[onClose, onChange],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLLIElement>) => {
const value = e.currentTarget.getAttribute('data-index');
const index = items.findIndex((item) => item.value === value);
let element: Element | null | undefined = null;
switch (e.key) {
case 'Escape':
onClose();
break;
case ' ':
case 'Enter':
handleClick(e);
break;
case 'ArrowDown':
element =
nodeRef.current?.children[index + 1] ??
nodeRef.current?.firstElementChild;
break;
case 'ArrowUp':
element =
nodeRef.current?.children[index - 1] ??
nodeRef.current?.lastElementChild;
break;
case 'Tab':
if (e.shiftKey) {
element =
nodeRef.current?.children[index + 1] ??
nodeRef.current?.firstElementChild;
} else {
element =
nodeRef.current?.children[index - 1] ??
nodeRef.current?.lastElementChild;
}
break;
case 'Home':
element = nodeRef.current?.firstElementChild;
break;
case 'End':
element = nodeRef.current?.lastElementChild;
break;
}
if (element && element instanceof HTMLElement) {
const selectedValue = element.getAttribute('data-index');
element.focus();
if (selectedValue) setCurrentValue(selectedValue);
e.preventDefault();
e.stopPropagation();
}
},
[nodeRef, items, onClose, handleClick, setCurrentValue],
);
useEffect(() => {
document.addEventListener('click', handleDocumentClick, { capture: true });
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
focusedItemRef.current?.focus({ preventScroll: true });
return () => {
document.removeEventListener('click', handleDocumentClick, {
capture: true,
});
document.removeEventListener(
'touchend',
handleDocumentClick,
listenerOptions,
);
};
}, [handleDocumentClick]);
return (
<ul style={style} role='listbox' ref={nodeRef}>
{items.map((item) => (
<li
role='option'
tabIndex={0}
key={item.value}
data-index={item.value}
onKeyDown={handleKeyDown}
onClick={handleClick}
className={classNames(`${classNamePrefix}__option`, {
active: item.value === currentValue,
})}
aria-selected={item.value === currentValue}
ref={item.value === currentValue ? focusedItemRef : null}
>
{item.icon && item.iconComponent && (
<div className={`${classNamePrefix}__option__icon`}>
<Icon id={item.icon} icon={item.iconComponent} />
</div>
)}
<div className={`${classNamePrefix}__option__content`}>
<strong>{item.text}</strong>
{item.meta}
</div>
{item.extra && (
<div
className={`${classNamePrefix}__option__additional`}
title={item.extra}
>
<Icon id='info-circle' icon={InfoIcon} />
</div>
)}
</li>
))}
</ul>
);
};

View file

@ -0,0 +1,23 @@
import { FormattedMessage } from 'react-intl';
import { StatusBanner, BannerVariant } from './status_banner';
export const FilterWarning: React.FC<{
title: string;
expanded?: boolean;
onClick?: () => void;
}> = ({ title, expanded, onClick }) => (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Blue}
>
<p>
<FormattedMessage
id='filter_warning.matches_filter'
defaultMessage='Matches filter “{title}”'
values={{ title }}
/>
</p>
</StatusBanner>
);

View file

@ -1,13 +1,9 @@
import { useCallback, useEffect } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { useIntl, defineMessages } from 'react-intl';
import { useIdentity } from '@/mastodon/identity_context';
import {
fetchRelationships,
followAccount,
unfollowAccount,
} from 'mastodon/actions/accounts';
import { fetchRelationships, followAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
@ -60,29 +56,14 @@ export const FollowButton: React.FC<{
if (accountId === me) {
return;
} else if (relationship.following || relationship.requested) {
} else if (account && (relationship.following || relationship.requested)) {
dispatch(
openModal({
modalType: 'CONFIRM',
modalProps: {
message: (
<FormattedMessage
id='confirmations.unfollow.message'
defaultMessage='Are you sure you want to unfollow {name}?'
values={{ name: <strong>@{account?.acct}</strong> }}
/>
),
confirm: intl.formatMessage(messages.unfollow),
onConfirm: () => {
dispatch(unfollowAccount(accountId));
},
},
}),
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
);
} else {
dispatch(followAccount(accountId));
}
}, [dispatch, intl, accountId, relationship, account, signedIn]);
}, [dispatch, accountId, relationship, account, signedIn]);
let label;

View file

@ -1,5 +1,7 @@
import { useEffect, forwardRef } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
@ -25,6 +27,11 @@ export const HoverCardAccount = forwardRef<
accountId ? state.accounts.get(accountId) : undefined,
);
const note = useAppSelector(
(state) =>
state.relationships.getIn([accountId, 'note']) as string | undefined,
);
useEffect(() => {
if (accountId && !account) {
dispatch(fetchAccount(accountId));
@ -53,6 +60,17 @@ export const HoverCardAccount = forwardRef<
className='hover-card__bio'
/>
<AccountFields fields={account.fields} limit={2} />
{note && note.length > 0 && (
<dl className='hover-card__note'>
<dt className='hover-card__note-label'>
<FormattedMessage
id='account.account_note_header'
defaultMessage='Personal note'
/>
</dt>
<dd>{note}</dd>
</dl>
)}
</div>
<div className='hover-card__number'>

View file

@ -9,18 +9,18 @@ const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
interface Props {
interface Props<T> {
disabled: boolean;
maxId: string;
onClick: (maxId: string) => void;
param: T;
onClick: (params: T) => void;
}
export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
export const LoadGap = <T,>({ disabled, param, onClick }: Props<T>) => {
const intl = useIntl();
const handleClick = useCallback(() => {
onClick(maxId);
}, [maxId, onClick]);
onClick(param);
}, [param, onClick]);
return (
<button

View file

@ -22,7 +22,7 @@ type LocationState = MastodonLocationState | null | undefined;
type HistoryPath = Path | LocationDescriptor<LocationState>;
const browserHistory = createBrowserHistory<LocationState>();
export const browserHistory = createBrowserHistory<LocationState>();
const originalPush = browserHistory.push.bind(browserHistory);
const originalReplace = browserHistory.replace.bind(browserHistory);

View file

@ -13,6 +13,8 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import { ContentWarning } from 'mastodon/components/content_warning';
import { FilterWarning } from 'mastodon/components/filter_warning';
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router';
@ -116,7 +118,10 @@ class Status extends ImmutablePureComponent {
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
scrollKey: PropTypes.string,
skipPrepend: PropTypes.bool,
avatarSize: PropTypes.number,
deployPictureInPicture: PropTypes.func,
unfocusable: PropTypes.bool,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
@ -137,7 +142,7 @@ class Status extends ImmutablePureComponent {
state = {
showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault),
forceFilter: undefined,
showDespiteFilter: undefined,
};
componentDidUpdate (prevProps) {
@ -149,7 +154,7 @@ class Status extends ImmutablePureComponent {
if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
this.setState({
showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault),
forceFilter: undefined,
showDespiteFilter: undefined,
});
}
}
@ -266,7 +271,7 @@ class Status extends ImmutablePureComponent {
handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this._properStatus(), this.props.history);
this.props.onReply(this._properStatus());
};
handleHotkeyFavourite = () => {
@ -279,7 +284,7 @@ class Status extends ImmutablePureComponent {
handleHotkeyMention = e => {
e.preventDefault();
this.props.onMention(this._properStatus().get('account'), this.props.history);
this.props.onMention(this._properStatus().get('account'));
};
handleHotkeyOpen = () => {
@ -322,20 +327,32 @@ class Status extends ImmutablePureComponent {
};
handleHotkeyToggleHidden = () => {
this.props.onToggleHidden(this._properStatus());
const { onToggleHidden } = this.props;
const status = this._properStatus();
if (status.get('matched_filters')) {
const expandedBecauseOfCW = !status.get('hidden') || status.get('spoiler_text').length === 0;
const expandedBecauseOfFilter = this.state.showDespiteFilter;
if (expandedBecauseOfFilter && !expandedBecauseOfCW) {
onToggleHidden(status);
} else if (expandedBecauseOfFilter && expandedBecauseOfCW) {
onToggleHidden(status);
this.handleFilterToggle();
} else {
this.handleFilterToggle();
}
} else {
onToggleHidden(status);
}
};
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
};
handleUnfilterClick = e => {
this.setState({ forceFilter: false });
e.preventDefault();
};
handleFilterClick = () => {
this.setState({ forceFilter: true });
handleFilterToggle = () => {
this.setState(state => ({ ...state, showDespiteFilter: !state.showDespiteFilter }));
};
_properStatus () {
@ -353,7 +370,7 @@ class Status extends ImmutablePureComponent {
};
render () {
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId } = this.props;
const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
let { status, account, ...other } = this.props;
@ -379,8 +396,8 @@ class Status extends ImmutablePureComponent {
if (hidden) {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={0}>
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={unfocusable ? null : 0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span>
</div>
@ -393,25 +410,6 @@ class Status extends ImmutablePureComponent {
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
const matchedFilters = status.get('matched_filters');
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
</button>
</div>
</HotKeys>
);
}
if (featured) {
prepend = (
<div className='status__prepend'>
@ -539,18 +537,18 @@ class Status extends ImmutablePureComponent {
}
if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={46} />;
statusAvatar = <Avatar account={status.get('account')} size={avatarSize} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
const expanded = (!matchedFilters || this.state.showDespiteFilter) && (!status.get('hidden') || status.get('spoiler_text').length === 0);
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
{prepend}
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
{!skipPrepend && prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
@ -571,22 +569,27 @@ class Status extends ImmutablePureComponent {
</a>
</div>
<StatusContent
status={status}
onClick={this.handleClick}
expanded={expanded}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
collapsible
onCollapsedToggle={this.handleCollapsedToggle}
{...statusContentProps}
/>
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
{media}
{(status.get('spoiler_text').length > 0 && (!matchedFilters || this.state.showDespiteFilter)) && <ContentWarning text={status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml')} expanded={expanded} onClick={this.handleExpandedToggle} />}
{expanded && hashtagBar}
{expanded && (
<>
<StatusContent
status={status}
onClick={this.handleClick}
onTranslate={this.handleTranslate}
collapsible
onCollapsedToggle={this.handleCollapsedToggle}
{...statusContentProps}
/>
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
{media}
{hashtagBar}
</>
)}
<StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} />
</div>
</div>
</HotKeys>

View file

@ -17,7 +17,6 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
@ -61,7 +60,6 @@ const messages = defineMessages({
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
hide: { id: 'status.hide', defaultMessage: 'Hide post' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
@ -118,7 +116,7 @@ class StatusActionBar extends ImmutablePureComponent {
const { signedIn } = this.props.identity;
if (signedIn) {
this.props.onReply(this.props.status, this.props.history);
this.props.onReply(this.props.status);
} else {
this.props.onInteractionModal('reply', this.props.status);
}
@ -157,15 +155,15 @@ class StatusActionBar extends ImmutablePureComponent {
};
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.props.history);
this.props.onDelete(this.props.status);
};
handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.props.history, true);
this.props.onDelete(this.props.status, true);
};
handleEditClick = () => {
this.props.onEdit(this.props.status, this.props.history);
this.props.onEdit(this.props.status);
};
handlePinClick = () => {
@ -173,11 +171,11 @@ class StatusActionBar extends ImmutablePureComponent {
};
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.props.history);
this.props.onMention(this.props.status.get('account'));
};
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.props.history);
this.props.onDirect(this.props.status.get('account'));
};
handleMuteClick = () => {
@ -241,10 +239,6 @@ class StatusActionBar extends ImmutablePureComponent {
navigator.clipboard.writeText(url);
};
handleHideClick = () => {
this.props.onFilter();
};
render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.props.identity;
@ -377,10 +371,6 @@ class StatusActionBar extends ImmutablePureComponent {
reblogIconComponent = RepeatDisabledIcon;
}
const filterButton = this.props.onFilter && (
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
);
const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
return (
@ -390,8 +380,6 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
{filterButton}
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}

View file

@ -0,0 +1,37 @@
import { FormattedMessage } from 'react-intl';
export enum BannerVariant {
Yellow = 'yellow',
Blue = 'blue',
}
export const StatusBanner: React.FC<{
children: React.ReactNode;
variant: BannerVariant;
expanded?: boolean;
onClick?: () => void;
}> = ({ children, variant, expanded, onClick }) => (
<div
className={
variant === BannerVariant.Yellow
? 'content-warning'
: 'content-warning content-warning--filter'
}
>
{children}
<button className='link-button' onClick={onClick}>
{expanded ? (
<FormattedMessage
id='content_warning.hide'
defaultMessage='Hide post'
/>
) : (
<FormattedMessage
id='content_warning.show'
defaultMessage='Show anyway'
/>
)}
</button>
</div>
);

View file

@ -4,7 +4,7 @@ import { PureComponent } from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import classnames from 'classnames';
import { Link, withRouter } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
@ -15,7 +15,6 @@ import PollContainer from 'mastodon/containers/poll_container';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
/**
@ -73,8 +72,6 @@ class StatusContent extends PureComponent {
identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired,
statusContent: PropTypes.string,
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onTranslate: PropTypes.func,
onClick: PropTypes.func,
collapsible: PropTypes.bool,
@ -87,10 +84,6 @@ class StatusContent extends PureComponent {
history: PropTypes.object.isRequired
};
state = {
hidden: true,
};
_updateStatusLinks () {
const node = this.node;
@ -218,17 +211,6 @@ class StatusContent extends PureComponent {
this.startXY = null;
};
handleSpoilerClick = (e) => {
e.preventDefault();
if (this.props.onExpandedToggle) {
// The parent manages the state
this.props.onExpandedToggle();
} else {
this.setState({ hidden: !this.state.hidden });
}
};
handleTranslate = () => {
this.props.onTranslate();
};
@ -240,18 +222,15 @@ class StatusContent extends PureComponent {
render () {
const { status, intl, statusContent } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
const content = { __html: statusContent ?? getStatusContent(status) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.props.history,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': renderReadMore,
});
@ -269,38 +248,7 @@ class StatusContent extends PureComponent {
<PollContainer pollId={status.get('poll')} lang={language} />
);
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
const mentionLinks = status.get('mentions').map(item => (
<Link to={`/@${item.get('acct')}`} key={item.get('id')} className='status-link mention'>
@<span>{item.get('username')}</span>
</Link>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
if (hidden) {
mentionsPlaceholder = <div>{mentionLinks}</div>;
}
return (
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
{' '}
<button type='button' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick} aria-expanded={!hidden}>{toggleText}</button>
</p>
{mentionsPlaceholder}
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''} translate`} lang={language} dangerouslySetInnerHTML={content} />
{!hidden && poll}
{translateButton}
</div>
);
} else if (this.props.onClick) {
if (this.props.onClick) {
return (
<>
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>

View file

@ -107,7 +107,7 @@ export default class StatusList extends ImmutablePureComponent {
<LoadGap
key={'gap:' + statusIds.get(index + 1)}
disabled={isLoading}
maxId={index > 0 ? statusIds.get(index - 1) : null}
param={index > 0 ? statusIds.get(index - 1) : null}
onClick={onLoadMore}
/>
);

View file

@ -1,25 +1,23 @@
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
interface Props {
resource: JSX.Element;
message: React.ReactNode;
label: React.ReactNode;
url: string;
className?: string;
}
export const TimelineHint: React.FC<Props> = ({ resource, url }) => (
<div className='timeline-hint'>
<strong>
<FormattedMessage
id='timeline_hint.remote_resource_not_displayed'
defaultMessage='{resource} from other servers are not displayed.'
values={{ resource }}
/>
</strong>
<br />
export const TimelineHint: React.FC<Props> = ({
className,
message,
label,
url,
}) => (
<div className={classNames('timeline-hint', className)}>
<p>{message}</p>
<a href={url} target='_blank' rel='noopener noreferrer'>
<FormattedMessage
id='account.browse_more_on_origin_server'
defaultMessage='Browse more on the original profile'
/>
{label}
</a>
</div>
);

View file

@ -1,24 +1,20 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
} from '../actions/accounts';
import { openModal } from '../actions/modal';
import { initMuteModal } from '../actions/mutes';
import Account from '../components/account';
import { makeGetAccount } from '../selectors';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
@ -29,18 +25,11 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
const mapDispatchToProps = (dispatch) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
} else {
dispatch(followAccount(account.get('id')));
}

View file

@ -1,36 +0,0 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { blockDomain, unblockDomain } from '../actions/domain_blocks';
import { openModal } from '../actions/modal';
import { Domain } from '../components/domain';
const messages = defineMessages({
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => {
const mapStateToProps = () => ({});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onBlockDomain (domain) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
}));
},
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain));

View file

@ -1,4 +1,4 @@
import { defineMessages, injectIntl } from 'react-intl';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
@ -21,11 +21,9 @@ import {
initAddFilter,
} from '../actions/filters';
import {
reblog,
favourite,
toggleReblog,
toggleFavourite,
bookmark,
unreblog,
unfavourite,
unbookmark,
pin,
unpin,
@ -38,29 +36,16 @@ import {
muteStatus,
unmuteStatus,
deleteStatus,
hideStatus,
revealStatus,
toggleStatusSpoilers,
toggleStatusCollapse,
editStatus,
translateStatus,
undoStatusTranslation,
} from '../actions/statuses';
import Status from '../components/status';
import { boostModal, deleteModal } from '../initial_state';
import { deleteModal } from '../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
@ -74,48 +59,26 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
const mapDispatchToProps = (dispatch, { contextType }) => ({
onReply (status, router) {
onReply (status) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)) },
}));
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
} else {
dispatch(replyCompose(status, router));
dispatch(replyCompose(status));
}
});
},
onModalReblog (status, privacy) {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
}
},
onReblog (status, e) {
if ((e && e.shiftKey) || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
dispatch(toggleReblog(status.get('id'), e.shiftKey));
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
dispatch(toggleFavourite(status.get('id')));
},
onBookmark (status) {
@ -144,35 +107,21 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}));
},
onDelete (status, history, withRedraft = false) {
onDelete (status, withRedraft = false) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
dispatch(deleteStatus(status.get('id'), withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
}
},
onEdit (status, history) {
onEdit (status) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.editMessage),
confirm: intl.formatMessage(messages.editConfirm),
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_EDIT_STATUS', modalProps: { statusId: status.get('id') } }));
} else {
dispatch(editStatus(status.get('id'), history));
dispatch(editStatus(status.get('id')));
}
});
},
@ -185,12 +134,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}
},
onDirect (account, router) {
dispatch(directCompose(account, router));
onDirect (account) {
dispatch(directCompose(account));
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
onMention (account) {
dispatch(mentionCompose(account));
},
onOpenMedia (statusId, media, index, lang) {
@ -241,11 +190,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
dispatch(toggleStatusSpoilers(status.get('id')));
},
onToggleCollapsed (status, isCollapsed) {

View file

@ -151,7 +151,7 @@ class AccountNote extends ImmutablePureComponent {
return (
<div className='account__header__account-note'>
<label htmlFor={`account-note-${account.get('id')}`}>
<FormattedMessage id='account.account_note_header' defaultMessage='Note' /> <InlineAlert show={saved} />
<FormattedMessage id='account.account_note_header' defaultMessage='Personal note' /> <InlineAlert show={saved} />
</label>
<Textarea

View file

@ -2,13 +2,11 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { NavLink, withRouter } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import InnerHeader from '../../account/components/header';
import MemorialNote from './memorial_note';
@ -36,7 +34,6 @@ class Header extends ImmutablePureComponent {
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
...WithRouterPropTypes,
};
handleFollow = () => {
@ -48,11 +45,11 @@ class Header extends ImmutablePureComponent {
};
handleMention = () => {
this.props.onMention(this.props.account, this.props.history);
this.props.onMention(this.props.account);
};
handleDirect = () => {
this.props.onDirect(this.props.account, this.props.history);
this.props.onDirect(this.props.account);
};
handleReport = () => {
@ -155,4 +152,4 @@ class Header extends ImmutablePureComponent {
}
export default withRouter(Header);
export default Header;

View file

@ -1,4 +1,4 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
@ -6,7 +6,6 @@ import { openURL } from 'mastodon/actions/search';
import {
followAccount,
unfollowAccount,
unblockAccount,
unmuteAccount,
pinAccount,
@ -24,11 +23,6 @@ import { initReport } from '../../../actions/reports';
import { makeGetAccount, getAccountHidden } from '../../../selectors';
import Header from '../components/header';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
@ -41,18 +35,11 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
const mapDispatchToProps = (dispatch) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
} else {
dispatch(followAccount(account.get('id')));
}
@ -77,12 +64,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
onMention (account) {
dispatch(mentionCompose(account));
},
onDirect (account, router) {
dispatch(directCompose(account, router));
onDirect (account) {
dispatch(directCompose(account));
},
onReblogToggle (account) {

View file

@ -12,6 +12,7 @@ import BundleColumnError from 'mastodon/features/ui/components/bundle_column_err
import { me } from 'mastodon/initial_state';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors';
import { useAppSelector } from 'mastodon/store';
import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags';
@ -59,12 +60,22 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
};
};
const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older posts' />} />
);
const RemoteHint = ({ accountId, url }) => {
const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.posts_may_be_missing' defaultMessage='Some posts from this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_posts' defaultMessage='See more posts on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
};
class AccountTimeline extends ImmutablePureComponent {
@ -175,12 +186,12 @@ class AccountTimeline extends ImmutablePureComponent {
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
}
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return (
<Column>

View file

@ -7,7 +7,6 @@ import { useDispatch } from 'react-redux';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'mastodon/actions/modal';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@ -23,8 +22,6 @@ const messages = defineMessages({
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
export const ActionBar = () => {
@ -32,16 +29,8 @@ export const ActionBar = () => {
const intl = useIntl();
const handleLogoutClick = useCallback(() => {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
}, [dispatch, intl]);
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
}, [dispatch]);
let menu = [];

View file

@ -10,8 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import { Button } from '../../../components/button';
@ -71,7 +69,6 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool,
lang: PropTypes.string,
maxChars: PropTypes.number,
...WithOptionalRouterPropTypes
};
static defaultProps = {
@ -120,7 +117,7 @@ class ComposeForm extends ImmutablePureComponent {
return;
}
this.props.onSubmit(this.props.history || null);
this.props.onSubmit();
if (e) {
e.preventDefault();
@ -317,4 +314,4 @@ class ComposeForm extends ImmutablePureComponent {
}
export default withOptionalRouter(injectIntl(ComposeForm));
export default injectIntl(ComposeForm);

View file

@ -13,6 +13,7 @@ import { cancelReplyCompose } from 'mastodon/actions/compose';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { EmbeddedStatusContent } from 'mastodon/features/notifications_v2/components/embedded_status_content';
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@ -33,8 +34,6 @@ export const EditIndicator = () => {
return null;
}
const content = { __html: status.get('contentHtml') };
return (
<div className='edit-indicator'>
<div className='edit-indicator__header'>
@ -49,7 +48,12 @@ export const EditIndicator = () => {
</div>
</div>
<div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} />
<EmbeddedStatusContent
className='edit-indicator__content translate'
content={status.get('contentHtml')}
language={status.get('language')}
mentions={status.get('mentions')}
/>
{(status.get('poll') || status.get('media_attachments').size > 0) && (
<div className='edit-indicator__attachments'>

View file

@ -11,10 +11,9 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import { DropdownSelector } from 'mastodon/components/dropdown_selector';
import { Icon } from 'mastodon/components/icon';
import { PrivacyDropdownMenu } from './privacy_dropdown_menu';
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
@ -143,7 +142,7 @@ class PrivacyDropdown extends PureComponent {
{({ props, placement }) => (
<div {...props}>
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
<PrivacyDropdownMenu
<DropdownSelector
items={this.options}
value={value}
onClose={this.handleClose}

View file

@ -1,128 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { supportsPassiveEvents } from 'detect-passive-events';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import { Icon } from 'mastodon/components/icon';
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
export const PrivacyDropdownMenu = ({ style, items, value, onClose, onChange }) => {
const nodeRef = useRef(null);
const focusedItemRef = useRef(null);
const [currentValue, setCurrentValue] = useState(value);
const handleDocumentClick = useCallback((e) => {
if (nodeRef.current && !nodeRef.current.contains(e.target)) {
onClose();
e.stopPropagation();
}
}, [nodeRef, onClose]);
const handleClick = useCallback((e) => {
const value = e.currentTarget.getAttribute('data-index');
e.preventDefault();
onClose();
onChange(value);
}, [onClose, onChange]);
const handleKeyDown = useCallback((e) => {
const value = e.currentTarget.getAttribute('data-index');
const index = items.findIndex(item => (item.value === value));
let element = null;
switch (e.key) {
case 'Escape':
onClose();
break;
case ' ':
case 'Enter':
handleClick(e);
break;
case 'ArrowDown':
element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild;
break;
case 'ArrowUp':
element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild;
break;
case 'Tab':
if (e.shiftKey) {
element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild;
} else {
element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild;
}
break;
case 'Home':
element = nodeRef.current.firstChild;
break;
case 'End':
element = nodeRef.current.lastChild;
break;
}
if (element) {
element.focus();
setCurrentValue(element.getAttribute('data-index'));
e.preventDefault();
e.stopPropagation();
}
}, [nodeRef, items, onClose, handleClick, setCurrentValue]);
useEffect(() => {
document.addEventListener('click', handleDocumentClick, { capture: true });
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
focusedItemRef.current?.focus({ preventScroll: true });
return () => {
document.removeEventListener('click', handleDocumentClick, { capture: true });
document.removeEventListener('touchend', handleDocumentClick, listenerOptions);
};
}, [handleDocumentClick]);
return (
<ul style={{ ...style }} role='listbox' ref={nodeRef}>
{items.map(item => (
<li
role='option'
tabIndex={0}
key={item.value}
data-index={item.value}
onKeyDown={handleKeyDown}
onClick={handleClick}
className={classNames('privacy-dropdown__option', { active: item.value === currentValue })}
aria-selected={item.value === currentValue}
ref={item.value === currentValue ? focusedItemRef : null}
>
<div className='privacy-dropdown__option__icon'>
<Icon id={item.icon} icon={item.iconComponent} />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{item.text}</strong>
{item.meta}
</div>
{item.extra && (
<div className='privacy-dropdown__option__additional' title={item.extra}>
<Icon id='info-circle' icon={InfoIcon} />
</div>
)}
</li>
))}
</ul>
);
};
PrivacyDropdownMenu.propTypes = {
style: PropTypes.object,
items: PropTypes.array.isRequired,
value: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};

View file

@ -9,6 +9,7 @@ import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react'
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import { EmbeddedStatusContent } from 'mastodon/features/notifications_v2/components/embedded_status_content';
export const ReplyIndicator = () => {
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
@ -19,8 +20,6 @@ export const ReplyIndicator = () => {
return null;
}
const content = { __html: status.get('contentHtml') };
return (
<div className='reply-indicator'>
<div className='reply-indicator__line' />
@ -34,7 +33,12 @@ export const ReplyIndicator = () => {
<DisplayName account={account} />
</Link>
<div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
<EmbeddedStatusContent
className='reply-indicator__content translate'
content={status.get('contentHtml')}
language={status.get('language')}
mentions={status.get('mentions')}
/>
{(status.get('poll') || status.get('media_attachments').size > 0) && (
<div className='reply-indicator__attachments'>

View file

@ -37,8 +37,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(changeCompose(text));
},
onSubmit (router) {
dispatch(submitCompose(router));
onSubmit () {
dispatch(submitCompose());
},
onClearSuggestions () {

View file

@ -21,7 +21,6 @@ import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
import { openModal } from 'mastodon/actions/modal';
import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon';
import { logOut } from 'mastodon/utils/log_out';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
@ -42,8 +41,6 @@ const messages = defineMessages({
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
const mapStateToProps = (state, ownProps) => ({
@ -72,20 +69,12 @@ class Compose extends PureComponent {
}
handleLogoutClick = e => {
const { dispatch, intl } = this.props;
const { dispatch } = this.props;
e.preventDefault();
e.stopPropagation();
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
return false;
};

View file

@ -18,7 +18,7 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import { replyCompose } from 'mastodon/actions/compose';
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
import { openModal } from 'mastodon/actions/modal';
import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'mastodon/actions/statuses';
import { muteStatus, unmuteStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
import AttachmentList from 'mastodon/components/attachment_list';
import AvatarComposite from 'mastodon/components/avatar_composite';
import { IconButton } from 'mastodon/components/icon_button';
@ -36,8 +36,6 @@ const messages = defineMessages({
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
});
const getAccounts = createSelector(
@ -103,19 +101,12 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(lastStatus, history)),
},
}));
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status: lastStatus } }));
} else {
dispatch(replyCompose(lastStatus, history));
dispatch(replyCompose(lastStatus));
}
});
}, [dispatch, lastStatus, history, intl]);
}, [dispatch, lastStatus]);
const handleDelete = useCallback(() => {
dispatch(deleteConversation(id));
@ -138,11 +129,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
}, [dispatch, lastStatus]);
const handleShowMore = useCallback(() => {
if (lastStatus.get('hidden')) {
dispatch(revealStatus(lastStatus.get('id')));
} else {
dispatch(hideStatus(lastStatus.get('id')));
}
dispatch(toggleStatusSpoilers(lastStatus.get('id')));
}, [dispatch, lastStatus]);
if (!lastStatus) {

View file

@ -8,7 +8,6 @@ import { Link } from 'react-router-dom';
import {
followAccount,
unfollowAccount,
unblockAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
@ -29,20 +28,12 @@ const messages = defineMessages({
id: 'account.cancel_follow_request',
defaultMessage: 'Withdraw follow request',
},
cancelFollowRequestConfirm: {
id: 'confirmations.cancel_follow_request.confirm',
defaultMessage: 'Withdraw request',
},
requested: {
id: 'account.requested',
defaultMessage: 'Awaiting approval. Click to cancel follow request',
},
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
unfollowConfirm: {
id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow',
},
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});
@ -89,48 +80,17 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
const handleFollow = useCallback(() => {
if (!account) return;
if (account.getIn(['relationship', 'following'])) {
if (
account.getIn(['relationship', 'following']) ||
account.getIn(['relationship', 'requested'])
) {
dispatch(
openModal({
modalType: 'CONFIRM',
modalProps: {
message: (
<FormattedMessage
id='confirmations.unfollow.message'
defaultMessage='Are you sure you want to unfollow {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
),
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => {
dispatch(unfollowAccount(account.get('id')));
},
},
}),
);
} else if (account.getIn(['relationship', 'requested'])) {
dispatch(
openModal({
modalType: 'CONFIRM',
modalProps: {
message: (
<FormattedMessage
id='confirmations.cancel_follow_request.message'
defaultMessage='Are you sure you want to withdraw your request to follow {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
),
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
onConfirm: () => {
dispatch(unfollowAccount(account.get('id')));
},
},
}),
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
);
} else {
dispatch(followAccount(account.get('id')));
}
}, [account, dispatch, intl]);
}, [account, dispatch]);
const handleBlock = useCallback(() => {
if (account?.relationship?.blocking) {

View file

@ -11,16 +11,15 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
import { Domain } from 'mastodon/components/domain';
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import DomainContainer from '../../containers/domain_container';
import Column from '../ui/components/column';
const messages = defineMessages({
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
});
const mapStateToProps = state => ({
@ -70,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{domains.map(domain =>
<DomainContainer key={domain} domain={domain} />,
<Domain key={domain} domain={domain} />,
)}
</ScrollableList>

View file

@ -12,6 +12,7 @@ import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors';
import { useAppSelector } from 'mastodon/store';
import {
lookupAccount,
@ -51,12 +52,22 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
};
};
const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} />
);
const RemoteHint = ({ accountId, url }) => {
const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.followers_may_be_missing' defaultMessage='Followers for this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_followers' defaultMessage='See more followers on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
};
class Followers extends ImmutablePureComponent {
@ -141,12 +152,12 @@ class Followers extends ImmutablePureComponent {
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
}
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return (
<Column>

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