Compare commits

...

241 Commits

Author SHA1 Message Date
Claire
e272cf5983 Fix download of stable translation files in glitch-soc 2024-10-08 13:42:57 +02:00
Claire
4382de310c Merge pull request #2873 from ClearlyClaire/glitch-soc/backports-4.3
Merge upstream changes (stable-4.3)
2024-10-08 13:36:30 +02:00
Claire
94c69bba25 Merge branch 'stable-4.3' into glitch-soc/backports-4.3 2024-10-08 13:19:29 +02:00
github-actions[bot]
ab36c152f9 New Crowdin Translations for stable-4.3 (automated) (#32297)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-10-08 13:18:49 +02:00
Matt Jankowski
fc5b558b32 Reduce factory usage across spec/services area (#32098) 2024-10-08 10:44:32 +02:00
Claire
77ff94d3d2 Fix source strings being uploaded to crowdin in merge groups (#32298) 2024-10-08 10:10:50 +02:00
Claire
959841ae95 Merge pull request #2871 from ClearlyClaire/glitch-soc/backports-4.3
Merge upstream changes (stable-4.3)
2024-10-07 21:25:03 +02:00
Eugen Rochko
f669493d96 [Glitch] Fix missing avatar fallback interfering with transparency in web UI
Port cae93e79a4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-07 20:13:58 +02:00
Eugen Rochko
83b3c50778 [Glitch] Fix wrong width on logo in detailed link card in web UI
Port 889edc560a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-07 20:06:49 +02:00
Claire
dc7a42551f [Glitch] Fix media gallery items having incorrect borders when hidden
Port 3b4312476f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-07 20:06:14 +02:00
Claire
4a859140ec Merge commit 'edcf3d9234b03d6b1c4b29d1d15339f7f64040fb' into glitch-soc/backports-4.3 2024-10-07 20:03:53 +02:00
Claire
edcf3d9234 Bump version to v4.3.0 (#32283) 2024-10-07 17:37:05 +02:00
Eugen Rochko
cae93e79a4 Fix missing avatar fallback interfering with transparency in web UI (#32270) 2024-10-07 16:22:11 +02:00
Claire
83a98cb81a Add missing on_delete: :cascade on notification_permissions (#32281) 2024-10-07 16:22:11 +02:00
Eugen Rochko
889edc560a Fix wrong width on logo in detailed link card in web UI (#32271) 2024-10-07 16:22:11 +02:00
github-actions[bot]
2e0d918d7d New Crowdin Translations for stable-4.3 (automated) (#32253)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-10-07 11:21:49 +02:00
Claire
3b4312476f Fix media gallery items having incorrect borders when hidden (#32257) 2024-10-07 10:54:23 +02:00
Claire
4fba4f8c82 Fix notification push notifications not including the author's username (#32254) 2024-10-07 10:54:23 +02:00
Matt Jankowski
25de2f57ee Add coverage for missing status scenario in NotificationMailer (#32256) 2024-10-07 10:54:23 +02:00
Claire
026643ab24 Fix video player's height in detailed status view 2024-10-06 19:19:14 +02:00
Claire
61e3e81e28 Merge pull request #2865 from ClearlyClaire/glitch-soc/backports-4.3
Merge upstream changes (stable-4.3)
2024-10-06 15:56:56 +02:00
Claire
354f54907d [Glitch] Fix unsupported grouped notifications from streaming causing duplicate IDs
Port 6d5aa58f88 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-05 21:05:55 +02:00
Claire
4d611e94ee [Glitch] Hide badges in media gallery when media are hidden
Port 55b5364534 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-05 21:05:15 +02:00
Claire
a09a26da49 [Glitch] Fix editing description of media uploads with custom thumbnails
Port 404f467fcf to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-05 21:04:52 +02:00
Claire
59a8066045 [Glitch] Fix media uploads in composer appearing over search results in advanced interface
Port 4a2d3929c5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-05 21:04:31 +02:00
Claire
3cad5095c9 [Glitch] Fix incorrect 'navigator' check
Port 931553844d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-05 21:04:09 +02:00
Matt Jankowski
e58d99a771 [Glitch] Adjust spacing on setting sub-nav items when below mobile size
Port 09cf617d7f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-05 21:03:45 +02:00
Matt Jankowski
69c76fd94a [Glitch] Improve alignment of icons on admin roles list
Port c828e7731c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-05 21:03:22 +02:00
Renaud Chaput
1b6bd585ab [Glitch] Fix follow notifications from streaming being grouped
Port 8ac00533ff to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-10-05 21:00:39 +02:00
Claire
dfe851b476 Merge branch 'stable-4.3' into glitch-soc/backports-4.3
Conflicts:
- `app/helpers/application_helper.rb`:
  Upstream added a helper where glitch-soc had its own, not really
  a conflict.
  Added upstream's helper.
2024-10-05 20:45:05 +02:00
Claire
6d5aa58f88 Fix unsupported grouped notifications from streaming causing duplicate IDs (#32243) 2024-10-04 17:48:03 +02:00
Claire
81cd489208 Fix Content-Security-Policy when using sso-redirect (#32241) 2024-10-04 17:48:03 +02:00
Claire
55b5364534 Hide badges in media gallery when media are hidden (#32224) 2024-10-04 17:48:03 +02:00
Matt Jankowski
2e8b752c55 Move admin action log type list generation to helper (#32178) 2024-10-04 17:48:03 +02:00
Matt Jankowski
d82ffdccbb Add copyable_input helper method to wrap shared options (#32119) 2024-10-04 17:48:03 +02:00
Matt Jankowski
5c72b46a4e Clean up labels on development application form (#32116) 2024-10-04 17:48:03 +02:00
Matt Jankowski
aa46348c03 Enable hostname config for all system specs (#32109) 2024-10-04 17:48:03 +02:00
Claire
404f467fcf Fix editing description of media uploads with custom thumbnails (#32221) 2024-10-04 17:48:03 +02:00
Claire
4a2d3929c5 Fix media uploads in composer appearing over search results in advanced interface (#32217) 2024-10-04 17:48:03 +02:00
Matt Jankowski
ceba0f082e Provide use_path to qr generator for svg data size reduction (#32127) 2024-10-04 17:48:03 +02:00
Matt Jankowski
7de8d5ffca Add relevant_params to ReportFilter (matches account filter) (#32136) 2024-10-04 17:48:03 +02:00
Matt Jankowski
74291dfb77 Remove unneeded reorder(nil) conditions (#32200) 2024-10-04 17:48:03 +02:00
Matt Jankowski
f07707a9bb Extract WebPushRequest from push notification worker and subscription (#32208) 2024-10-04 17:48:03 +02:00
Claire
931553844d Fix incorrect 'navigator' check (#32219) 2024-10-04 17:48:03 +02:00
Matt Jankowski
243a85ec8d Expand coverage for Export utility class (#32212) 2024-10-04 17:48:03 +02:00
Christian Schmidt
cbf1349370 Support /.well-known/host-meta.json (#32206) 2024-10-04 17:48:03 +02:00
Jeong Arm
b8fdffe824 Ignore error if mentioned account was not processable (#29215)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-10-04 17:48:03 +02:00
Matt Jankowski
c91e06bcad Fix Rails/CreateTableWithTimestamps cop (#30836) 2024-10-04 17:48:03 +02:00
Jeong Arm
b2ce9bb4c7 Show timestamp when the user deletes their account on admin dashboard (#25640)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-10-04 17:48:03 +02:00
Matt Jankowski
19d1392b33 Avoid repeated icon stack in settings sidebar (#32201) 2024-10-04 17:48:03 +02:00
Matt Jankowski
09cf617d7f Adjust spacing on setting sub-nav items when below mobile size (#32137) 2024-10-04 17:48:03 +02:00
Matt Jankowski
784d1bfb29 Fix broken border on applications list (#32147) 2024-10-04 17:48:03 +02:00
Claire
754b03d8cb Fix unneeded requests to blocked domains when receiving relayed signed activities from them (#31161) 2024-10-04 17:48:03 +02:00
Emelia Smith
f397550311 Add detection and download of material_symbol icons in config/navigation.rb (#31366) 2024-10-04 17:48:03 +02:00
Matt Jankowski
97db4bd4dd Wrap datetime in time element with attrs (#32177) 2024-10-04 17:48:03 +02:00
Matt Jankowski
1e19242134 Extract constants for header and avatar geometry (#32151) 2024-10-04 17:48:03 +02:00
Matt Jankowski
4e6f13a0fb Only show email domain blocks MX table when some found (#32155) 2024-10-04 17:48:03 +02:00
Matt Jankowski
f517f0dbef Fix nav item active highlight for some paths (#32159) 2024-10-04 17:48:03 +02:00
Matt Jankowski
53624b1b54 Remove explicit put action in settings forms (#32176) 2024-10-04 17:48:03 +02:00
renovate[bot]
a473988969 Update dependency postcss-preset-env to v10.0.5 (#32019)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 17:48:03 +02:00
Matt Jankowski
4ad1e955eb Use module: :users in routes/admin section (#30767) 2024-10-04 17:48:03 +02:00
Matt Jankowski
66ef4b9984 Remove WebfingerHelper module & move usage inline (#31203) 2024-10-04 17:48:03 +02:00
David Roetzel
ce2481a81b Move OTP secret length to configuration (#32125) 2024-10-04 17:48:03 +02:00
renovate[bot]
efa74a6c44 Update RuboCop (non-major) to v1.22.1 (#31573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 17:48:03 +02:00
Matt Jankowski
bdceb1dacf Add date_range view helper (#32187) 2024-10-04 17:48:03 +02:00
renovate[bot]
e13453aec4 Update dependency webmock to v3.24.0 (#32190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 17:48:03 +02:00
renovate[bot]
25e8a6eaeb Update dependency propshaft to v1.1.0 (#32192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 17:48:03 +02:00
Matt Jankowski
c828e7731c Improve alignment of icons on admin roles list (#32153) 2024-10-04 17:48:03 +02:00
Matt Jankowski
6734b6550f Extract dashboard partial for admin instance page (#32189) 2024-10-04 17:48:03 +02:00
renovate[bot]
6398d7b784 Update peter-evans/create-pull-request action to v7.0.5 (#32164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 17:48:03 +02:00
Matt Jankowski
1283c3544c Avoid id duplication conflict with main navigation from settings profile link (#32181) 2024-10-04 17:48:03 +02:00
Renaud Chaput
8ac00533ff Fix follow notifications from streaming being grouped (#32179) 2024-10-04 17:48:03 +02:00
Matt Jankowski
1b3472bec8 Use account display name for pretend blog example in attribution area (#32188) 2024-10-04 17:48:03 +02:00
Claire
c8df7f4995 Change github action repo to glitch-soc 2024-09-30 20:50:32 +02:00
Claire
94743fea2c Merge remote-tracking branch 'upstream/stable-4.3' into glitch-soc/stable-4.3 2024-09-30 20:49:42 +02:00
Claire
deee164acf Support translation branches in Crowdin (#32174) 2024-09-30 19:45:40 +02:00
Claire
88756ab75f Merge pull request #2861 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 03210085b7
2024-09-30 13:30:14 +02:00
Claire
9af9ef6fb3 Merge commit '03210085b7481568cc507f088144aaf1dae73c88' into glitch-soc/merge-upstream 2024-09-30 13:04:19 +02:00
Claire
03210085b7 Bump version to 4.3.0-rc.1 (#32124) 2024-09-30 10:42:59 +00:00
Claire
0c872beed4 Merge commit from fork
This should not change the set of words matched by `USERNAME_RE` but does
change the one matched by `MENTION_RE`. Indeed, the previous regexp allowed
a domain part to start with `.` or `-`, which the new regexp does not allow.
2024-09-30 12:25:54 +02:00
Claire
e22eff8900 Remove regexp timeout feature (#32169) 2024-09-30 09:41:06 +00:00
renovate[bot]
431b382563 Update dependency sass to v1.79.4 (#32139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 08:55:18 +00:00
renovate[bot]
bf7cfba48e Update DefinitelyTyped types (non-major) (#32163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 08:53:52 +00:00
github-actions[bot]
f477dc399e New Crowdin Translations (automated) (#32140)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-30 08:53:29 +00:00
renovate[bot]
6037714f76 Update dependency propshaft to v1.0.1 (#32158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 08:47:57 +00:00
Eugen Rochko
c352ce6f45 Fix missing permission on new embeds making them unclickable (#32135) 2024-09-30 08:20:20 +00:00
Claire
9bf624b44d Merge pull request #2860 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 9d664f87a0
2024-09-29 20:36:32 +02:00
Eugen Rochko
e80971e660 [Glitch] Change media reordering design in the compose form in web UI
Port 11a12e56b3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 14:03:19 +02:00
Eugen Rochko
9e10fd59b7 [Glitch] Add ability to view alt text by clicking the ALT badge in web UI
Port a04433f995 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 13:35:35 +02:00
Eugen Rochko
9b5f073cb3 [Glitch] Change design of media tab on profiles in web UI
Port 89df27a06c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 12:57:58 +02:00
Eugen Rochko
157ecf255b [Glitch] Change responsive break points on navigation panel in web UI
Port 28c4eca0af to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 11:53:49 +02:00
Matt Jankowski
b8c23f94b0 [Glitch] Add no-toolbar state for "nothing here" batch table views
Port 24d3ce7bab to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 11:29:18 +02:00
Matt Jankowski
3fa34bd73a [Glitch] Use 1 column layout for form ul on narrow widths
Port 106b22bd2d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 11:25:24 +02:00
Claire
77d2f7eef6 [Glitch] Fix scrollbar width
Port 89c39e7826 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 11:24:54 +02:00
Claire
8439084587 [Glitch] Add fallback to domain block confirmation modal
Port 7a62d57427 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 11:21:56 +02:00
Eugen Rochko
7b290cee47 [Glitch] Add preview of followers removed in domain block modal in web UI
Port 3426ea2912 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-29 11:21:05 +02:00
Claire
7ef25ae53b Merge commit '9d664f87a04b6a5157ddbe60ee33b5b7a960198e' into glitch-soc/merge-upstream 2024-09-29 11:06:09 +02:00
Christian Schmidt
9d664f87a0 Mailer layout fixes (#32132) 2024-09-27 19:41:41 +00:00
Matt Jankowski
24d3ce7bab Add no-toolbar state for "nothing here" batch table views (#32128) 2024-09-27 19:38:44 +00:00
Eugen Rochko
11a12e56b3 Change media reordering design in the compose form in web UI (#32093) 2024-09-27 15:09:39 +00:00
Matt Jankowski
cdd7526531 Remove completed TODO note in tags request spec (#32108) 2024-09-27 08:22:40 +00:00
Matt Jankowski
e02e88bff4 Use previously extracted model constants in form maxlength attributes (#32113) 2024-09-27 08:21:27 +00:00
Matt Jankowski
04dd3a9eb6 Wrap webhook event label with samp tag (#32115) 2024-09-27 08:20:21 +00:00
Matt Jankowski
675ec1a0ad Only show recently used tags hint when they are present (#32120) 2024-09-27 08:18:42 +00:00
github-actions[bot]
c9b0699964 New Crowdin Translations (automated) (#32121)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-27 07:34:46 +00:00
Claire
513f187daf Add “A Mastodon update is available.” message on admin dashboard for non-bugfix updates (#32106) 2024-09-26 19:27:57 +00:00
renovate[bot]
ee2d966080 Update dependency blurhash to v0.1.8 (#32114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 19:27:13 +00:00
Matt Jankowski
106b22bd2d Use 1 column layout for form ul on narrow widths (#32112) 2024-09-26 19:26:40 +00:00
Claire
89c39e7826 Fix scrollbar width (#32091) 2024-09-26 14:26:04 +00:00
Eugen Rochko
a04433f995 Add ability to view alt text by clicking the ALT badge in web UI (#32058) 2024-09-26 13:26:49 +00:00
Claire
7a62d57427 Add fallback to domain block confirmation modal (#32105) 2024-09-26 12:47:56 +00:00
Eugen Rochko
89df27a06c Change design of media tab on profiles in web UI (#31967) 2024-09-26 12:31:32 +00:00
Christian Schmidt
00aaf77e04 Use same styling for statuses in email as on web (#32073) 2024-09-26 11:48:01 +00:00
Claire
437cecc965 Fix awkward status action bar layout changes (#2859) 2024-09-26 12:02:40 +02:00
Matt Jankowski
db57fe80c8 Remove page_json var from ap/replies spec (#32000) 2024-09-26 08:54:01 +00:00
github-actions[bot]
278a075b22 New Crowdin Translations (automated) (#32103)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-26 08:47:38 +00:00
Claire
886baa5e35 Fix typo causing incorrect error being raised in blurhash processing failure (#32104) 2024-09-26 07:40:59 +00:00
Christian Schmidt
db332553c9 Rename "Data export" menu item (#32099) 2024-09-25 19:54:28 +00:00
Claire
f610fdd6e7 Merge pull request #2858 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 28966fa0a6
2024-09-25 21:15:51 +02:00
Eugen Rochko
3426ea2912 Add preview of followers removed in domain block modal in web UI (#32032) 2024-09-25 18:13:36 +00:00
Eugen Rochko
28c4eca0af Change responsive break points on navigation panel in web UI (#32034) 2024-09-25 16:36:19 +00:00
Renaud Chaput
0b1310feb3 [Glitch] Keep the status action buttons at their position regardless of the counter size
Port 739ad0eed2 to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-25 18:12:24 +02:00
Renaud Chaput
5716ebf390 [Glitch] Add notification grouping for follow notifications
Port d6f5ee75ab to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-25 18:09:19 +02:00
Renaud Chaput
36ce5813cb [Glitch] Fix search params being dropped when redirected to non-deck path
Port 3dc4ddc663 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-25 18:08:55 +02:00
Matt Jankowski
d9d84822bb [Glitch] Use not-allowed for cursor on disabled buttons
Port 69aa5699ce to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-25 18:08:12 +02:00
Claire
633165ba9c Merge commit '28966fa0a6d7b98ee94696acdc79e45449ce8349' into glitch-soc/merge-upstream 2024-09-25 17:41:37 +02:00
Renaud Chaput
28966fa0a6 Remove deprecated v2_alpha endpoint for grouped notifications (#32089) 2024-09-25 15:21:11 +00:00
Renaud Chaput
739ad0eed2 Keep the status action buttons at their position regardless of the counter size (#32084)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-09-25 14:33:58 +00:00
Matt Jankowski
51777fe3e2 Prefer structure checks over multi-line size/parts checks in parsed_body (#32063) 2024-09-25 13:54:22 +00:00
Renaud Chaput
d6f5ee75ab Add notification grouping for follow notifications (#32085) 2024-09-25 13:36:19 +00:00
Renaud Chaput
3dc4ddc663 Fix search params being dropped when redirected to non-deck path (#31984) 2024-09-25 13:35:37 +00:00
Matt Jankowski
83574f641a Add coverage and use mailer callback to check functional user in notification mailer (#32055) 2024-09-25 08:07:48 +00:00
Matt Jankowski
c2ef83ea4c Consolidate shared a scope example parts into one attributes check (#32046) 2024-09-25 07:56:42 +00:00
Matt Jankowski
c3b6a7a297 Reduce factory creation (36 -> 12) in spec/controllers/oauth/* area (#32045) 2024-09-25 07:56:08 +00:00
Matt Jankowski
06ecf9008b Remove single-use shared examples in controller specs (#32044) 2024-09-25 07:50:15 +00:00
Matt Jankowski
69aa5699ce Use not-allowed for cursor on disabled buttons (#32076) 2024-09-25 07:43:12 +00:00
github-actions[bot]
4e6fc3a62f New Crowdin Translations (automated) (#32083)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-25 07:40:14 +00:00
renovate[bot]
a773c239c3 Update dependency aws-sdk-s3 to v1.166.0 (#32079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 07:33:37 +00:00
Claire
440b695b79 Fix polls not being displayed in detailed status (#2857) 2024-09-24 20:54:24 +02:00
Claire
5df7e36244 Merge pull request #2856 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 70988519df
2024-09-24 20:51:46 +02:00
Eugen Rochko
ba7b1f06c1 [Glitch] Fix too many requests caused by relationship look-ups in web UI
Port 70988519df to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-24 19:42:30 +02:00
Renaud Chaput
6142adc7d6 [Glitch] Fix wrapping in dashboard quick access buttons
Port f1b6a611aa to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-24 19:41:37 +02:00
Claire
131696277c [Glitch] Fix multiple bugs in notification requests and notification policies
Port 0a6b75b71e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-24 19:41:04 +02:00
Eugen Rochko
aac6296183 [Glitch] Change hide media button to be in top right corner in web UI
Port d54ce67dc9 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-24 19:40:34 +02:00
Claire
c6039f99ce Merge commit '70988519df66f0b8edeb6ca95140f1d3e436fea8' into glitch-soc/merge-upstream 2024-09-24 19:34:30 +02:00
Eugen Rochko
70988519df Fix too many requests caused by relationship look-ups in web UI (#32042)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-09-24 17:02:36 +00:00
Renaud Chaput
f1b6a611aa Fix wrapping in dashboard quick access buttons (#32043) 2024-09-24 16:47:45 +00:00
André Menrath
556837f156 Fix the summary of converted object types to be treated as HTML (#28629) 2024-09-24 15:57:53 +00:00
Claire
c36a76b9eb Fix error when accepting appeal for sensitive posts deleted in the meantime (#32037)
Co-authored-by: David Roetzel <david@roetzel.de>
2024-09-24 15:19:55 +00:00
Claire
0a6b75b71e Fix multiple bugs in notification requests and notification policies (#32062) 2024-09-24 15:03:38 +00:00
David Roetzel
cfb8fc6222 Increase regexp timeout and allow override (#32056) 2024-09-24 13:16:58 +00:00
Matt Jankowski
19dedd7cfd Set important mailer headers with after_action callback (#32057) 2024-09-24 13:16:31 +00:00
Matt Jankowski
780e2e9d66 Convert notification mailer spec shared examples to matchers (#32047) 2024-09-24 12:07:16 +00:00
renovate[bot]
7c61533111 Update dependency aws-sdk-s3 to v1.165.0 (#32050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-24 09:54:25 +00:00
github-actions[bot]
11ac5c8929 New Crowdin Translations (automated) (#32052)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-24 09:42:14 +00:00
Claire
c808055fc3 Update dependency webrick (#32054) 2024-09-24 08:16:22 +00:00
Eugen Rochko
d54ce67dc9 Change hide media button to be in top right corner in web UI (#32048) 2024-09-24 08:00:20 +00:00
Claire
6551129aff Merge pull request #2853 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 5dfdec6453
2024-09-24 09:44:10 +02:00
Claire
38744a4e51 [Glitch] Change mobile breakpoint back to old version and allow main column to shrink
Port b5bdc69f7b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-23 20:26:05 +02:00
Claire
c2c3a66478 Merge commit '5dfdec645313e556413147597138a8008bc35996' into glitch-soc/merge-upstream 2024-09-23 20:24:18 +02:00
Matt Jankowski
5dfdec6453 Convert settings/applications controller spec to system/request specs (#32006) 2024-09-23 13:37:32 +00:00
Eugen Rochko
aaab6b7adc Add reblogs and favourites counts to statuses in ActivityPub (#32007) 2024-09-23 13:14:15 +00:00
Claire
b5bdc69f7b Change mobile breakpoint back to old version and allow main column to shrink (#32033) 2024-09-23 12:53:35 +00:00
Matt Jankowski
bbf7752256 Combine assertions in Notification model spec (#32015) 2024-09-23 10:45:34 +00:00
Matt Jankowski
2b4bda8004 Add response_avatar_link helper to webfinger request spec (#31999) 2024-09-23 10:44:52 +00:00
Matt Jankowski
447d0a3e88 Remove double no-records cases in api/v1/admin req specs (#32014) 2024-09-23 09:27:53 +00:00
Matt Jankowski
66ed7ea4b5 Move status creation to "with rss" context in accounts request spec (#32020) 2024-09-23 09:20:43 +00:00
Matt Jankowski
cd7b670cd8 Reduce factory creation in User#reset_password! spec (#32021) 2024-09-23 09:18:04 +00:00
Claire
5d6a3f2cb0 Update dependency google-protobuf (#32029) 2024-09-23 09:13:51 +00:00
renovate[bot]
770ec9240a Update Yarn to v4.5.0 (#31914)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 09:02:16 +00:00
Tim Campbell
11eae691ba Feature more otel customization (#31998) 2024-09-23 08:55:35 +00:00
github-actions[bot]
ed90d9342e New Crowdin Translations (automated) (#32011)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-23 08:50:19 +00:00
Matt Jankowski
0ba3ad4a35 Remove body_json_ids from api/v2/admin/accounts spec (#32003) 2024-09-23 08:45:05 +00:00
Matt Jankowski
e0b45b35c9 Combine repeated parsed_body assertions into single (#32002) 2024-09-23 08:42:52 +00:00
Eugen Rochko
5fae1d55e5 Fix OAuth authorization prompt referring to third-party apps (#32005) 2024-09-23 08:42:03 +00:00
renovate[bot]
10d2f83025 Update dependency selenium-webdriver to v4.25.0 (#32008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 08:41:26 +00:00
renovate[bot]
958f01e722 Update dependency sass to v1.79.3 (#32009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 08:41:05 +00:00
renovate[bot]
f4632d941a Update dependency aws-sdk-s3 to v1.164.0 (#32010)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 08:40:46 +00:00
renovate[bot]
c37f9c0d44 Update dependency jsdom to v25.0.1 (#32017)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 08:36:57 +00:00
renovate[bot]
84d04386dd Update DefinitelyTyped types (non-major) (#32026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 08:35:37 +00:00
renovate[bot]
f294c4a594 Update libretranslate/libretranslate Docker tag to v1.6.1 (#32027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 08:35:23 +00:00
renovate[bot]
efc0d4d526 Update dependency react-intl to v6.7.0 (#32028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 08:35:11 +00:00
Claire
6ac6d86525 Merge pull request #2852 from ClearlyClaire/glitch-soc/features/grouped-notifications-cw
Add content warning support to grouped notifications
2024-09-22 21:57:55 +02:00
Claire
7c148ed1cb Use new CW class in more places 2024-09-22 21:17:15 +02:00
Claire
4d754935a9 Replace new-style upstream CWs with old-style CWs for now 2024-09-22 20:55:06 +02:00
Eugen Rochko
0d26c9fb0b [Glitch] Fix wrong width on content warnings and filters in web UI
Port b265a654d7 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-22 19:31:22 +02:00
Eugen Rochko
7d97e3d82f [Glitch] Change how content warnings and filters are displayed in web UI
Partially apply 500f4925a5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-22 19:31:22 +02:00
Claire
5aebdc9bcb Merge pull request #2850 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 7ed9c590b9
2024-09-21 20:27:48 +02:00
Eugen Rochko
a969c6a6a6 [Glitch] Change zoom icon in web UI
Port e7fd0985c9 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-21 15:36:17 +02:00
Renaud Chaput
03829d8e1d [Glitch] Update directory page options to use URL params
Port ae03e4ffc6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-21 15:17:56 +02:00
Claire
86b9d3b4e5 [Glitch] Fix custom history.push and history.replace building bogus location if path is omitted
Port 57a38f071b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-21 15:16:54 +02:00
Eugen Rochko
9bd5838646 [Glitch] Fix browser glitch caused by two overlapping scroll animations in web UI
Port ef4d6ab988 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-21 15:16:28 +02:00
Eugen Rochko
80cb285819 [Glitch] Fix sass deprecation warning
Port 29656cb9e0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-21 15:14:54 +02:00
Renaud Chaput
d77348f830 [Glitch] Fix the appearance of avatars when they do not load
Port 8b70834035 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-21 15:13:57 +02:00
Christian Schmidt
0820cbcb35 [Glitch] Mute XHR abort errors
Port 7740f1a6bb to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2024-09-21 15:13:23 +02:00
Claire
221bba1897 Merge commit '7ed9c590b98610f8d68deab9ef8df260eec6d8f0' into glitch-soc/merge-upstream 2024-09-21 15:06:06 +02:00
github-actions[bot]
b0f6d3e112 New Crowdin Translations (automated) (#2773)
* New Crowdin translations

* Fix bogus `no.yml`

* Fix bogus `simple_form.no.yml`

---------

Co-authored-by: GitHub Actions <noreply@github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-09-21 14:57:30 +02:00
Claire
7ed9c590b9 Fix issue when encountering reblog of deleted post in feed rebuild (#32001) 2024-09-20 14:58:06 +00:00
Claire
ed8b0e4b1e Fix links for reblogs in moderation interface (#31979) 2024-09-20 13:33:26 +00:00
Matt Jankowski
d55f4fbda1 Add content type checks to api/v2 request specs (#31983) 2024-09-20 13:19:53 +00:00
Matt Jankowski
171394e914 Add coverage for CSV responses for severed relationships (#31962) 2024-09-20 13:13:47 +00:00
Matt Jankowski
66326065b0 Add response.content_type checks for JSON to api/v1 request specs (#31981) 2024-09-20 13:13:04 +00:00
Matt Jankowski
a7dbf6f5a5 Use heredoc/squish for inline css styles in oembed serializer (#31991) 2024-09-20 12:50:51 +00:00
Matt Jankowski
bdf83c353f Move default embed size knowledge into OEmbedSerializer (#31990)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-09-20 12:39:48 +00:00
Claire
8afa3bb2fa Change Mastodon to issue correctly-signed queries by default (#31994) 2024-09-20 10:10:09 +00:00
Eugen Rochko
e7fd0985c9 Change zoom icon in web UI (#29683) 2024-09-20 09:42:02 +00:00
Matt Jankowski
04a939d640 Add reviewed and unreviewed scopes to Reviewable model concern (#31988) 2024-09-20 08:51:37 +00:00
Matt Jankowski
c922af2737 Add LIMIT constant for api/v1/peers/search endpoint (#31989) 2024-09-20 08:31:58 +00:00
github-actions[bot]
162f9a3c90 New Crowdin Translations (automated) (#31993)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-20 08:31:28 +00:00
renovate[bot]
840fd69730 Update dependency sass to v1.79.2 (#31992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 08:20:27 +00:00
Matt Jankowski
9a03902ab6 Capture actual behavior in v2/notifications "someone else" dismiss scenario (#31985) 2024-09-20 08:16:19 +00:00
renovate[bot]
09459ed000 Update dependency react-select to v5.8.1 (#31982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 08:15:14 +00:00
Renaud Chaput
ae03e4ffc6 Update directory page options to use URL params (#31977) 2024-09-19 15:34:08 +00:00
Claire
57a38f071b Fix custom history.push and history.replace building bogus location if path is omitted (#31980) 2024-09-19 14:58:33 +00:00
Matt Jankowski
5a8f2fe31d Convert settings/exports controller spec to system/request specs (#31965) 2024-09-19 13:43:40 +00:00
Matt Jankowski
2946a9286b Use headers shorthand in mailers (#31956) 2024-09-19 13:38:32 +00:00
renovate[bot]
6801afa12f Update dependency devise-two-factor to v6 [SECURITY] (#31957)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: David Roetzel <david@roetzel.de>
2024-09-19 10:56:09 +00:00
Eugen Rochko
ef4d6ab988 Fix browser glitch caused by two overlapping scroll animations in web UI (#31960) 2024-09-19 10:52:46 +00:00
github-actions[bot]
efdc17513d New Crowdin Translations (automated) (#31974)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-19 10:34:19 +00:00
Matt Jankowski
5d573c976e Remove unused E2EE-related methods (#31964) 2024-09-19 10:23:58 +00:00
Matt Jankowski
b071e618e7 Combine API request spec assertions (#31970) 2024-09-19 10:15:21 +00:00
renovate[bot]
1fce55cf5d Update dependency aws-sdk-s3 to v1.163.0 (#31972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 09:51:14 +00:00
renovate[bot]
90db524a90 Update dependency puma to v6.4.3 (#31975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 09:50:58 +00:00
Claire
62a39d60ce Fix rolling updates by moving DropEndToEndMessageTables to post-deployment migrations (#31963) 2024-09-19 09:50:06 +00:00
Eugen Rochko
29656cb9e0 Fix sass deprecation warning (#31961) 2024-09-18 17:39:32 +00:00
Renaud Chaput
8b70834035 Fix the appearance of avatars when they do not load (#31966) 2024-09-18 17:39:15 +00:00
Matt Jankowski
e3baa1cdda Add coverage for AccountDeletionRequest class (#31937) 2024-09-18 13:29:57 +00:00
renovate[bot]
42f9f507b6 Update dependency pg to v8.13.0 (#31949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 13:29:21 +00:00
Matt Jankowski
bf8eaaa9a5 Convert controller spec for security_key_options endpoint to request spec (#31938) 2024-09-18 09:42:36 +00:00
Matt Jankowski
6f836c45aa Remove crypto values from doorkeeper application/token scopes (#31945) 2024-09-18 09:27:50 +00:00
Matt Jankowski
5405bdd344 Remove unused E2EE messaging code (#31193) 2024-09-18 09:27:43 +00:00
renovate[bot]
2d399f5d4a Update dependency pg-connection-string to v2.7.0 (#31950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 09:17:57 +00:00
Christian Schmidt
7740f1a6bb Mute XHR abort errors (#31952) 2024-09-18 08:43:24 +00:00
renovate[bot]
a791274824 Update dependency sass to v1.79.1 (#31958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 08:28:42 +00:00
Matt Jankowski
eb16763bff Use have_http_link_header matcher in api/v1/trends/* specs (#31940) 2024-09-18 08:22:07 +00:00
Matt Jankowski
943738671c Remove unneeded to_s on Link header comparison in statuses controller spec (#31941) 2024-09-18 08:21:31 +00:00
renovate[bot]
6f3d7516dc Update dependency dotenv to v3.1.4 (#31953)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 08:10:22 +00:00
github-actions[bot]
bd86c692cf New Crowdin Translations (automated) (#31959)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-09-18 08:06:44 +00:00
renovate[bot]
b7548dbf29 Update dependency memory_profiler to v1.1.0 (#31947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 08:05:59 +00:00
Matt Jankowski
a397141d78 Move non-action public method controller callback to private methods (#31933) 2024-09-18 08:05:25 +00:00
renovate[bot]
f3f06dafe3 Update dependency babel-loader to v8.4.1 (#31931)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-18 08:05:06 +00:00
813 changed files with 9860 additions and 6693 deletions

View File

@@ -69,7 +69,7 @@ services:
hard: -1
libretranslate:
image: libretranslate/libretranslate:v1.6.0
image: libretranslate/libretranslate:v1.6.1
restart: unless-stopped
volumes:
- lt-data:/home/libretranslate/.local

View File

@@ -0,0 +1,70 @@
name: Crowdin / Download translations (stable branches)
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
download-translations-stable:
runs-on: ubuntu-latest
if: github.repository == 'glitch-soc/mastodon'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Increase Git http.postBuffer
# This is needed due to a bug in Ubuntu's cURL version?
# See https://github.com/orgs/community/discussions/55820
run: |
git config --global http.version HTTP/1.1
git config --global http.postBuffer 157286400
# Download the translation files from Crowdin
- name: crowdin action
uses: crowdin/github-action@v2
with:
config: crowdin-glitch.yml
upload_sources: false
upload_translations: false
download_translations: true
crowdin_branch_name: ${{ github.base_ref || github.ref_name }}
push_translations: false
create_pull_request: false
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# As the files are extracted from a Docker container, they belong to root:root
# We need to fix this before the next steps
- name: Fix file permissions
run: sudo chown -R runner:docker .
# This is needed to run the normalize step
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
- name: Run i18n normalize task
run: bundle exec i18n-tasks normalize
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.5
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'
author: 'GitHub Actions <noreply@github.com>'
body: |
New Crowdin translations, automated with GitHub Actions
See `.github/workflows/crowdin-download.yml`
This PR will be updated every day with new translations.
Due to a limitation in GitHub Actions, checks are not running on this PR without manual action.
If you want to run the checks, then close and re-open it.
branch: i18n/crowdin/translations-${{ github.base_ref || github.ref_name }}
base: ${{ github.base_ref || github.ref_name }}
labels: i18n

View File

@@ -53,7 +53,7 @@ jobs:
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.1
uses: peter-evans/create-pull-request@v7.0.5
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)'

View File

@@ -1,7 +1,6 @@
name: Crowdin / Upload translations
on:
merge_group:
push:
branches:
- 'main'
@@ -32,7 +31,7 @@ jobs:
upload_sources: true
upload_translations: false
download_translations: false
crowdin_branch_name: main
crowdin_branch_name: ${{ github.base_ref || github.ref_name }}
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}

View File

@@ -2,7 +2,7 @@
All notable changes to this project will be documented in this file.
## [4.3.0] - UNRELEASED
## [4.3.0] - 2024-10-08
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.
@@ -10,12 +10,13 @@ The following changelog entries focus on changes visible to users, administrator
- **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.
- Change `form-action` Content-Security-Policy directive to be more restrictive (#26897 by @ClearlyClaire)
- Fix ReDoS vulnerability on some Ruby versions ([GHSA-jpxp-r43f-rhvx](https://github.com/mastodon/mastodon/security/advisories/GHSA-jpxp-r43f-rhvx))
- Change `form-action` Content-Security-Policy directive to be more restrictive (#26897 and #32241 by @ClearlyClaire)
- Update dependencies
### Added
- **Add 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, #31513, #31592, #31594, #31638, #31746, #31652, #31709, #31725, #31745, #31613, #31657, #31840, #31610 and #31929 by @ClearlyClaire, @Gargron, @mgmn, and @renchap)\
- **Add 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, #31513, #31592, #31594, #31638, #31746, #31652, #31709, #31725, #31745, #31613, #31657, #31840, #31610, #31929, #32089, #32085, #32243, #32179 and #32254 by @ClearlyClaire, @Gargron, @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.\
@@ -27,7 +28,7 @@ The following changelog entries focus on changes visible to users, administrator
- `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts
- `POST /api/v2/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group
- `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#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, #31541, and #31723 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\
- **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, #31541, #31723, #32062 and #32281 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.\
@@ -60,7 +61,7 @@ The following changelog entries focus on changes visible to users, administrator
- **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, #30846, #31819, and #31900 by @Gargron and @oneiros)\
- **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, #30846, #31819, #31900 and #32188 by @Gargron, @mjankowski and @oneiros)\
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
@@ -76,7 +77,11 @@ The following changelog entries focus on changes visible to users, administrator
Clicking the domain of a user in their profile will now open a tooltip with a short explanation about servers and federation.
- **Add support for Redis sentinel** (#31694, #31623, #31744, #31767, and #31768 by @ThisIsMissEm and @oneiros)\
See https://docs.joinmastodon.org/admin/scaling/#redis-sentinel
- Add ability to reorder uploaded media before posting in web UI (#28456 by @Gargron)
- **Add ability to reorder uploaded media before posting in web UI** (#28456 and #32093 by @Gargron)
- Add “A Mastodon update is available.” message on admin dashboard for non-bugfix updates (#32106 by @ClearlyClaire)
- Add ability to view alt text by clicking the ALT badge in web UI (#32058 by @Gargron)
- Add preview of followers removed in domain block modal in web UI (#32032 and #32105 by @ClearlyClaire and @Gargron)
- Add reblogs and favourites counts to statuses in ActivityPub (#32007 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
@@ -122,14 +127,14 @@ The following changelog entries focus on changes visible to users, administrator
- 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 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)\
- Add support for libvips in addition to ImageMagick (#30090, #30590, #30597, #30632, #30857, #30869, #30858 and #32104 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 validations to `Web::PushSubscription` (#30540 and #30542 by @ThisIsMissEm)
- Add anchors to each authorized application in `/oauth/authorized_applications` (#31677 by @fowl2)
- 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)\
- Add OpenTelemetry instrumentation (#30130, #30322, #30353, #30350 and #31998 by @julianocosta89, @renchap, @robbkidd and @timetinytim)\
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
@@ -138,7 +143,6 @@ The following changelog entries focus on changes visible to users, administrator
- 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 global Regexp timeout (#31928 by @ClearlyClaire)
- 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)
@@ -146,10 +150,12 @@ The following changelog entries focus on changes visible to users, administrator
- 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 date of account deletion in list of accounts in the admin interface (#25640 by @tribela)
- 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 support for serving JRD `/.well-known/host-meta.json` in addition to XRD host-meta (#32206 by @c960657)
- 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)
@@ -164,18 +170,18 @@ The following changelog entries focus on changes visible to users, administrator
### 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)\
- **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, #31525, #32153, and #32201 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, #29659, and #31889 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\
- **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, #29659, #31889 and #32033 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 modals in the web UI** (#29576, #29614, #29640, #29644, #30131, #30884, #31399, #31555, #31752, #31801, #31883, #31844, #31864, and #31943 by @ClearlyClaire, @Gargron, @tribela and @vmstan)\
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 colors throughout the web UI** (#29522, #29584, #29653, #29779, #29803, #29809, #29808, #29828, #31034, #31168, #31266, #31348, #31349, #31361, #31510 and #32128 by @ClearlyClaire, @Gargron, @mjankowski, @renchap, and @vmstan)
- **Change onboarding prompt to follow suggestions carousel in web UI** (#28878, #29272, and #31912 by @Gargron)
- **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, and #29879 by @ClearlyClaire, @Gargron, @hteumeuleu, and @mjankowski)\
- **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, #29879, #32073 and #32132 by @c960657, @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 read 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.\
@@ -188,10 +194,17 @@ The following changelog entries focus on changes visible to users, administrator
Administrators may need to update their setup accordingly.
- Change how content warnings and filters are displayed in web UI (#31365, and #31761 by @Gargron)
- Change preview card processing to ignore `undefined` as canonical url (#31882 by @oneiros)
- Change embedded posts to use web UI (#31766 by @Gargron)
- Change embedded posts to use web UI (#31766, #32135 and #32271 by @Gargron)
- Change inner borders in media galleries in web UI (#31852 by @Gargron)
- Change design of hide media button in web UI (#31807 by @Gargron)
- Change design of media attachments and profile media tab in web UI (#31807, #32048, #31967, #32217, #32224 and #32257 by @ClearlyClaire and @Gargron)
- Change labels on thread indicators in web UI (#31806 by @Gargron)
- Change label of "Data export" menu item in settings interface (#32099 by @c960657)
- Change responsive break points on navigation panel in web UI (#32034 by @Gargron)
- Change cursor to `not-allowed` on disabled buttons (#32076 by @mjankowski)
- Change OAuth authorization prompt to not refer to apps as “third-party” (#32005 by @Gargron)
- Change Mastodon to issue correct HTTP signatures by default (#31994 by @ClearlyClaire)
- Change zoom icon in web UI (#29683 by @Gargron)
- Change directory page to use URL query strings for options (#31980, #31977 and #31984 by @ClearlyClaire and @renchap)
- Change report action buttons to be disabled when action has already been taken (#31773, #31822, and #31899 by @ClearlyClaire and @ThisIsMissEm)
- Change width of columns in advanced web UI (#31762 by @Gargron)
- Change design of unread conversations in web UI (#31763 by @Gargron)
@@ -254,6 +267,7 @@ The following changelog entries focus on changes visible to users, administrator
### Removed
- Remove unused E2EE messaging code and related `crypto` OAuth scope (#31193, #31945, #31963, and #31964 by @ClearlyClaire and @mjankowski)
- 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)\
@@ -269,9 +283,22 @@ The following changelog entries focus on changes visible to users, administrator
- 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 error when accepting an appeal for sensitive posts deleted in the meantime (#32037 by @ClearlyClaire)
- Fix error when encountering reblog of deleted post in feed rebuild (#32001 by @ClearlyClaire)
- Fix Safari browser glitch related to horizontal scrolling (#31960 by @Gargron)
- Fix unresolvable mentions sometimes preventing processing incoming posts (#29215 by @tribela and @ClearlyClaire)
- Fix too many requests caused by relationship look-ups in web UI (#32042 by @Gargron)
- Fix links for reblogs in moderation interface (#31979 by @ClearlyClaire)
- Fix the appearance of avatars when they do not load (#31966 and #32270 by @Gargron and @renchap)
- Fix spurious error notifications for aborted requests in web UI (#31952 by @c960657)
- Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil)
- Fix security context sometimes not being added in LD-Signed activities (#31871 by @ClearlyClaire)
- Fix cross-origin loading of `inert.css` polyfill (#30687 by @louis77)
- Fix wrapping in dashboard quick access buttons (#32043 by @renchap)
- Fix recently used tags hint being displayed in profile edition page when there is none (#32120 by @mjankowski)
- Fix checkbox lists on narrow screens in the settings interface (#32112 by @mjankowski)
- Fix the position of status action buttons being affected by interaction counters (#32084 by @renchap)
- Fix the summary of converted ActivityPub object types to be treated as HTML (#28629 by @Menrath)
- Fix cutoff of instance name in sign-up form (#30598 by @oneiros)
- Fix invalid date searches returning 503 errors (#31526 by @notchairmk)
- Fix invalid `visibility` values in `POST /api/v1/statuses` returning 500 errors (#31571 by @c960657)
@@ -285,10 +312,12 @@ The following changelog entries focus on changes visible to users, administrator
- 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 various cases of duplicate, missing, or inconsistent borders or scrollbar styles (#31068, #31286, #31268, #31275, #31284, #31305, #31346, #31372, #31373, #31389, #31432, #31391, #31445, #32091, #32147 and #32137 by @ClearlyClaire, @mjankowski, @valtlai and @vmstan)
- Fix editing description of media uploads with custom thumbnails (#32221 by @ClearlyClaire)
- 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 unneeded requests to blocked domains when receiving relayed signed activities from them (#31161 by @ClearlyClaire)
- 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)
@@ -298,6 +327,7 @@ The following changelog entries focus on changes visible to users, administrator
- 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 navigation item active highlight for some paths (#32159 by @mjankowski)
- 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)

View File

@@ -47,7 +47,6 @@ gem 'color_diff', '~> 0.1'
gem 'csv', '~> 3.2'
gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.6'
gem 'ed25519', '~> 1.3'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'hiredis', '~> 0.6'

View File

@@ -100,20 +100,20 @@ GEM
attr_required (1.0.2)
awrence (1.2.1)
aws-eventstream (1.3.0)
aws-partitions (1.974.0)
aws-sdk-core (3.205.0)
aws-partitions (1.978.0)
aws-sdk-core (3.209.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.91.0)
aws-sdk-core (~> 3, >= 3.205.0)
aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.162.0)
aws-sdk-core (~> 3, >= 3.205.0)
aws-sdk-s3 (1.166.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1)
aws-sigv4 (1.10.0)
aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
@@ -134,7 +134,7 @@ GEM
bindata (2.5.0)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.7)
blurhash (0.1.8)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (6.2.1)
@@ -197,7 +197,7 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (5.1.0)
devise-two-factor (6.0.0)
activesupport (~> 7.0)
devise (~> 4.0)
railties (~> 7.0)
@@ -212,9 +212,8 @@ GEM
domain_name (0.6.20240107)
doorkeeper (5.7.1)
railties (>= 5)
dotenv (3.1.2)
dotenv (3.1.4)
drb (2.2.1)
ed25519 (1.3.0)
elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11)
elasticsearch-transport (= 7.17.11)
@@ -290,7 +289,7 @@ GEM
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (3.25.4)
google-protobuf (3.25.5)
googleapis-common-protos-types (1.15.0)
google-protobuf (>= 3.18, < 5.a)
haml (6.3.0)
@@ -348,7 +347,7 @@ GEM
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.7.2)
irb (1.14.0)
irb (1.14.1)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
@@ -407,7 +406,7 @@ GEM
llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
logger (1.6.0)
logger (1.6.1)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
@@ -429,7 +428,7 @@ GEM
addressable (~> 2.5)
azure-storage-blob (~> 2.0.1)
hashie (~> 5.0)
memory_profiler (1.0.2)
memory_profiler (1.1.0)
mime-types (3.5.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2024.0820)
@@ -602,7 +601,7 @@ GEM
actionmailer (>= 3)
net-smtp
premailer (~> 1.7, >= 1.7.9)
propshaft (1.0.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
@@ -610,7 +609,7 @@ GEM
psych (5.1.2)
stringio
public_suffix (6.0.1)
puma (6.4.2)
puma (6.4.3)
nio4r (~> 2.0)
pundit (2.4.0)
activesupport (>= 3.0.0)
@@ -699,7 +698,7 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.3.7)
rexml (3.3.8)
rotp (6.3.0)
rouge (4.3.0)
rpam2 (4.0.2)
@@ -749,15 +748,15 @@ GEM
parser (>= 3.3.1.0)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
rubocop-performance (1.21.1)
rubocop-performance (1.22.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.25.1)
rubocop-rails (2.26.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.4)
rubocop-rspec (3.0.5)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
@@ -782,7 +781,7 @@ GEM
scenic (1.8.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
selenium-webdriver (4.24.0)
selenium-webdriver (4.25.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -885,7 +884,7 @@ GEM
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
webmock (3.23.1)
webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -894,7 +893,7 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.8.1)
webrick (1.8.2)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
@@ -937,7 +936,6 @@ DEPENDENCIES
discard (~> 1.2)
doorkeeper (~> 5.6)
dotenv
ed25519 (~> 1.3)
email_spec
fabrication (~> 2.30)
faker (~> 3.2)

View File

@@ -13,8 +13,9 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
## Supported Versions
| Version | Supported |
| ------- | --------- |
| 4.2.x | Yes |
| 4.1.x | Yes |
| < 4.1 | No |
| Version | Supported |
| ------- | ---------------- |
| 4.3.x | Yes |
| 4.2.x | Yes |
| 4.1.x | Until 2025-04-08 |
| < 4.1 | No |

View File

@@ -1,18 +0,0 @@
# frozen_string_literal: true
class ActivityPub::ClaimsController < ActivityPub::BaseController
skip_before_action :authenticate_user!
before_action :require_account_signature!
before_action :set_claim_result
def create
render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
end
private
def set_claim_result
@claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
end
end

View File

@@ -22,8 +22,6 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
@items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) }
when 'tags'
@items = for_signed_account { @account.featured_tags }
when 'devices'
@items = @account.devices
else
not_found
end
@@ -31,7 +29,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_size
case params[:id]
when 'featured', 'devices', 'tags'
when 'featured', 'tags'
@size = @items.size
else
not_found
@@ -42,7 +40,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
case params[:id]
when 'featured'
@type = :ordered
when 'devices', 'tags'
when 'tags'
@type = :unordered
else
not_found

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
class ActivityPub::LikesController < ActivityPub::BaseController
include Authorization
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status
def index
expires_in 0, public: @status.distributable? && public_fetch_mode?
render json: likes_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def pundit_user
signed_request_account
end
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def likes_collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_status_likes_url(@account, @status),
type: :unordered,
size: @status.favourites_count
)
end
end

View File

@@ -12,7 +12,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
before_action :set_replies
def index
expires_in 0, public: public_fetch_mode?
expires_in 0, public: @status.distributable? && public_fetch_mode?
render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
class ActivityPub::SharesController < ActivityPub::BaseController
include Authorization
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status
def index
expires_in 0, public: @status.distributable? && public_fetch_mode?
render json: shares_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def pundit_user
signed_request_account
end
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def shares_collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_status_shares_url(@account, @status),
type: :unordered,
size: @status.reblogs_count
)
end
end

View File

@@ -7,7 +7,7 @@ class Api::OEmbedController < Api::BaseController
before_action :require_public_status!
def show
render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
render json: @status, serializer: OEmbedSerializer, width: params[:maxwidth], height: params[:maxheight]
end
private
@@ -23,12 +23,4 @@ class Api::OEmbedController < Api::BaseController
def status_finder
StatusFinder.new(params[:url])
end
def maxwidth_or_default
(params[:maxwidth].presence || 400).to_i
end
def maxheight_or_default
params[:maxheight].present? ? params[:maxheight].to_i : nil
end
end

View File

@@ -1,30 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::DeliveriesController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
def create
devices.each do |device_params|
DeliverToDeviceService.new.call(current_account, @current_device, device_params)
end
render_empty
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
def resource_params
params.require(:device)
params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
end
def devices
Array(resource_params[:device])
end
end

View File

@@ -1,47 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
LIMIT = 80
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
before_action :set_encrypted_messages, only: :index
after_action :insert_pagination_headers, only: :index
def index
render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
end
def clear
@current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
render_empty
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
def set_encrypted_messages
@encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def next_path
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
end
def pagination_collection
@encrypted_messages
end
def records_continue?
@encrypted_messages.size == limit_param(LIMIT)
end
end

View File

@@ -1,25 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_claim_results
def create
render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
end
private
def set_claim_results
@claim_results = devices.filter_map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }
end
def resource_params
params.permit(device: [:account_id, :device_id])
end
def devices
Array(resource_params[:device])
end
end

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::CountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
def show
render json: { one_time_keys: @current_device.one_time_keys.count }
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
end

View File

@@ -1,26 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_accounts
before_action :set_query_results
def create
render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
end
private
def set_accounts
@accounts = Account.where(id: account_ids).includes(:devices)
end
def set_query_results
@query_results = @accounts.filter_map { |account| ::Keys::QueryService.new.call(account) }
end
def account_ids
Array(params[:id]).map(&:to_i)
end
end

View File

@@ -1,29 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
def create
device = Device.find_or_initialize_by(access_token: doorkeeper_token)
device.transaction do
device.account = current_account
device.update!(resource_params[:device])
if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
resource_params[:one_time_keys].each do |one_time_key_params|
device.one_time_keys.create!(one_time_key_params)
end
end
end
render json: device, serializer: REST::Keys::DeviceSerializer
end
private
def resource_params
params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
class Api::V1::DomainBlocks::PreviewsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }
before_action :require_user!
before_action :set_domain
before_action :set_domain_block_preview
def show
render json: @domain_block_preview, serializer: REST::DomainBlockPreviewSerializer
end
private
def set_domain
@domain = TagManager.instance.normalize_domain(params[:domain])
end
def set_domain_block_preview
@domain_block_preview = with_read_replica do
DomainBlockPreviewPresenter.new(
following_count: current_account.following.where(domain: @domain).count,
followers_count: current_account.followers.where(domain: @domain).count
)
end
end
end

View File

@@ -7,6 +7,8 @@ class Api::V1::Peers::SearchController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale
LIMIT = 10
vary_by ''
def index
@@ -35,10 +37,10 @@ class Api::V1::Peers::SearchController < Api::BaseController
field: 'accounts_count',
modifier: 'log2p',
},
}).limit(10).pluck(:domain)
}).limit(LIMIT).pluck(:domain)
else
domain = normalized_domain
@domains = Instance.searchable.domain_starts_with(domain).limit(10).pluck(:domain)
@domains = Instance.searchable.domain_starts_with(domain).limit(LIMIT).pluck(:domain)
end
rescue Addressable::URI::InvalidURIError
@domains = []

View File

@@ -9,7 +9,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
return not_found if @status.hidden?
if @status.local?
render json: @status, serializer: OEmbedSerializer, width: 400
render json: @status, serializer: OEmbedSerializer
else
return not_found unless user_signed_in?

View File

@@ -20,11 +20,6 @@ class Auth::SessionsController < Devise::SessionsController
p.form_action(false)
end
def check_suspicious!
user = find_user
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
end
def create
super do |resource|
# We only need to call this if this hasn't already been
@@ -101,6 +96,11 @@ class Auth::SessionsController < Devise::SessionsController
private
def check_suspicious!
user = find_user
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
end
def home_paths(resource)
paths = [about_path, '/explore']

View File

@@ -13,7 +13,7 @@ module WebAppControllerConcern
policy = ContentSecurityPolicy.new
if policy.sso_host.present?
p.form_action policy.sso_host
p.form_action policy.sso_host, -> { "https://#{request.host}/auth/auth/" }
else
p.form_action :none
end
@@ -31,7 +31,7 @@ module WebAppControllerConcern
def redirect_unauthenticated_to_permalinks!
return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in
permalink_redirector = PermalinkRedirector.new(request.path)
permalink_redirector = PermalinkRedirector.new(request.original_fullpath)
return if permalink_redirector.redirect_path.blank?
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?

View File

@@ -15,7 +15,7 @@ module Settings
end
def create
session[:new_otp_secret] = User.generate_otp_secret(32)
session[:new_otp_secret] = User.generate_otp_secret
redirect_to new_settings_two_factor_authentication_confirmation_path
end

View File

@@ -7,7 +7,23 @@ module WellKnown
def show
@webfinger_template = "#{webfinger_url}?resource={uri}"
expires_in 3.days, public: true
render content_type: 'application/xrd+xml', formats: [:xml]
respond_to do |format|
format.any do
render content_type: 'application/xrd+xml', formats: [:xml]
end
format.json do
render json: {
links: [
{
rel: 'lrdd',
template: @webfinger_template,
},
],
}
end
end
end
end
end

View File

@@ -35,4 +35,11 @@ module Admin::ActionLogsHelper
end
end
end
def sorted_action_log_types
Admin::ActionLogFilter::ACTION_TYPE_MAP
.keys
.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] }
.sort_by(&:first)
end
end

View File

@@ -18,6 +18,11 @@ module Admin::DashboardHelper
end
end
def date_range(range)
[l(range.first), l(range.last)]
.join(' - ')
end
def relevant_account_timestamp(account)
timestamp, exact = if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago
[account.user_current_sign_in_at, true]
@@ -25,6 +30,8 @@ module Admin::DashboardHelper
[account.user_current_sign_in_at, false]
elsif account.user_pending?
[account.user_created_at, true]
elsif account.suspended_at.present? && account.local? && account.user.nil?
[account.suspended_at, true]
elsif account.last_status_at.present?
[account.last_status_at, true]
else

View File

@@ -1,12 +1,6 @@
# frozen_string_literal: true
module ApplicationHelper
DANGEROUS_SCOPES = %w(
read
write
follow
).freeze
RTL_LOCALES = %i(
ar
ckb
@@ -95,8 +89,11 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)"
end
def class_for_scope(scope)
'scope-danger' if DANGEROUS_SCOPES.include?(scope.to_s)
def label_for_scope(scope)
safe_join [
tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }),
tag.span(t("doorkeeper.scopes.#{scope}"), class: :hint),
]
end
def can?(action, record)
@@ -244,6 +241,10 @@ module ApplicationHelper
full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg'))
end
def copyable_input(options = {})
tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
end
# glitch-soc addition to handle the multiple flavors
def preload_locale_pack
supported_locales = Themes.instance.flavour(current_flavour)['locales']

View File

@@ -24,23 +24,6 @@ module ContextHelper
indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' },
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: {
'toot' => 'http://joinmastodon.org/ns#',
'Device' => 'toot:Device',
'Ed25519Signature' => 'toot:Ed25519Signature',
'Ed25519Key' => 'toot:Ed25519Key',
'Curve25519Key' => 'toot:Curve25519Key',
'EncryptedMessage' => 'toot:EncryptedMessage',
'publicKeyBase64' => 'toot:publicKeyBase64',
'deviceId' => 'toot:deviceId',
'claim' => { '@type' => '@id', '@id' => 'toot:claim' },
'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' },
'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' },
'devices' => { '@type' => '@id', '@id' => 'toot:devices' },
'messageFranking' => 'toot:messageFranking',
'messageType' => 'toot:messageType',
'cipherText' => 'toot:cipherText',
},
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
}.freeze

View File

@@ -10,16 +10,17 @@ module SettingsHelper
end
def featured_tags_hint(recently_used_tags)
safe_join(
[
t('simple_form.hints.featured_tag.name'),
safe_join(
links_for_featured_tags(recently_used_tags),
', '
),
],
' '
)
recently_used_tags.present? &&
safe_join(
[
t('simple_form.hints.featured_tag.name'),
safe_join(
links_for_featured_tags(recently_used_tags),
', '
),
],
' '
)
end
def session_device_icon(session)

View File

@@ -1,7 +0,0 @@
# frozen_string_literal: true
module WebfingerHelper
def webfinger!(uri)
Webfinger.new(uri).perform
end
end

View File

@@ -1,4 +1,5 @@
import { browserHistory } from 'flavours/glitch/components/router';
import { debounceWithDispatchAndArguments } from 'flavours/glitch/utils/debounce';
import api, { getLinks } from '../api';
@@ -462,6 +463,20 @@ export function expandFollowingFail(id, error) {
};
}
const debouncedFetchRelationships = debounceWithDispatchAndArguments((dispatch, ...newAccountIds) => {
if (newAccountIds.length === 0) {
return;
}
dispatch(fetchRelationshipsRequest(newAccountIds));
api().get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
});
}, { delay: 500 });
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
const state = getState();
@@ -473,13 +488,7 @@ export function fetchRelationships(accountIds) {
return;
}
dispatch(fetchRelationshipsRequest(newAccountIds));
api().get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
});
debouncedFetchRelationships(dispatch, ...newAccountIds);
};
}

View File

@@ -1,5 +1,7 @@
import { defineMessages } from 'react-intl';
import { AxiosError } from 'axios';
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
@@ -50,6 +52,11 @@ export const showAlertForError = (error, skipNotFound = false) => {
});
}
// An aborted request, e.g. due to reloading the browser window, it not really error
if (error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
}
console.error(error);
return showAlert({

View File

@@ -37,8 +37,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
});
return;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if ('navigator' && 'sendBeacon' in navigator) {
} else if ('sendBeacon' in navigator) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();

View File

@@ -68,10 +68,19 @@ function dispatchAssociatedRecords(
dispatch(importFetchedStatuses(fetchedStatuses));
}
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
export function shouldGroupNotificationType(type: string) {
return supportedGroupedNotificationTypes.includes(type);
}
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) =>
apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }),
apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes,
exclude_types: getExcludedTypes(getState()),
}),
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
@@ -93,6 +102,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes,
max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()),
}),
@@ -109,6 +119,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes,
max_id: undefined,
exclude_types: getExcludedTypes(getState()),
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones

View File

@@ -17,6 +17,6 @@ export const updateNotificationsPolicy = createDataLoadingThunk(
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
);
export const decreasePendingNotificationsCount = createAction<number>(
'notificationPolicy/decreasePendingNotificationCount',
export const decreasePendingRequestsCount = createAction<number>(
'notificationPolicy/decreasePendingRequestsCount',
);

View File

@@ -13,11 +13,11 @@ import type {
ApiNotificationJSON,
} from 'flavours/glitch/api_types/notifications';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
import type { AppDispatch, RootState } from 'flavours/glitch/store';
import type { AppDispatch } from 'flavours/glitch/store';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { decreasePendingNotificationsCount } from './notification_policies';
import { decreasePendingRequestsCount } from './notification_policies';
// TODO: refactor with notification_groups
function dispatchAssociatedRecords(
@@ -169,19 +169,11 @@ export const expandNotificationsForRequest = createDataLoadingThunk(
},
);
const selectNotificationCountForRequest = (state: RootState, id: string) => {
const requests = state.notificationRequests.items;
const thisRequest = requests.find((request) => request.id === id);
return thisRequest ? thisRequest.notifications_count : 0;
};
export const acceptNotificationRequest = createDataLoadingThunk(
'notificationRequest/accept',
({ id }: { id: string }) => apiAcceptNotificationRequest(id),
(_data, { dispatch, getState, discardLoadData, actionArg: { id } }) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(decreasePendingNotificationsCount(count));
(_data, { dispatch, discardLoadData }) => {
dispatch(decreasePendingRequestsCount(1));
// The payload is not used in any functions
return discardLoadData;
@@ -191,10 +183,8 @@ export const acceptNotificationRequest = createDataLoadingThunk(
export const dismissNotificationRequest = createDataLoadingThunk(
'notificationRequest/dismiss',
({ id }: { id: string }) => apiDismissNotificationRequest(id),
(_data, { dispatch, getState, discardLoadData, actionArg: { id } }) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(decreasePendingNotificationsCount(count));
(_data, { dispatch, discardLoadData }) => {
dispatch(decreasePendingRequestsCount(1));
// The payload is not used in any functions
return discardLoadData;
@@ -204,13 +194,8 @@ export const dismissNotificationRequest = createDataLoadingThunk(
export const acceptNotificationRequests = createDataLoadingThunk(
'notificationRequests/acceptBulk',
({ ids }: { ids: string[] }) => apiAcceptNotificationRequests(ids),
(_data, { dispatch, getState, discardLoadData, actionArg: { ids } }) => {
const count = ids.reduce(
(count, id) => count + selectNotificationCountForRequest(getState(), id),
0,
);
dispatch(decreasePendingNotificationsCount(count));
(_data, { dispatch, discardLoadData, actionArg: { ids } }) => {
dispatch(decreasePendingRequestsCount(ids.length));
// The payload is not used in any functions
return discardLoadData;
@@ -220,13 +205,8 @@ export const acceptNotificationRequests = createDataLoadingThunk(
export const dismissNotificationRequests = createDataLoadingThunk(
'notificationRequests/dismissBulk',
({ ids }: { ids: string[] }) => apiDismissNotificationRequests(ids),
(_data, { dispatch, getState, discardLoadData, actionArg: { ids } }) => {
const count = ids.reduce(
(count, id) => count + selectNotificationCountForRequest(getState(), id),
0,
);
dispatch(decreasePendingNotificationsCount(count));
(_data, { dispatch, discardLoadData, actionArg: { ids } }) => {
dispatch(decreasePendingRequestsCount(ids.length));
// The payload is not used in any functions
return discardLoadData;

View File

@@ -10,7 +10,7 @@ import api, { getLinks } from '../api';
import { unescapeHTML } from '../utils/html';
import { requestNotificationPermission } from '../utils/notifications';
import { fetchFollowRequests, fetchRelationships } from './accounts';
import { fetchFollowRequests } from './accounts';
import {
importFetchedAccount,
importFetchedAccounts,
@@ -68,14 +68,6 @@ defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
});
const fetchRelatedRelationships = (dispatch, notifications) => {
const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
if (accountIds.length > 0) {
dispatch(fetchRelationships(accountIds));
}
};
export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING,
});
@@ -118,8 +110,6 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered}));
fetchRelatedRelationships(dispatch, [notification]);
} else if (playSound && !filtered) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
@@ -211,7 +201,6 @@ export function expandNotifications({ maxId = undefined, forceLoad = false }) {
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
fetchRelatedRelationships(dispatch, response.data);
dispatch(submitMarkers());
} catch(error) {
dispatch(expandNotificationsFail(error, isLoadingMore));

View File

@@ -42,6 +42,9 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
// eslint-disable-next-line import/no-default-export
export default function api(withAuthorization = true) {
return axios.create({
transitional: {
clarifyTimeoutError: true,
},
headers: {
...csrfHeader,
...(withAuthorization ? authorizationTokenFromInitialState() : {}),
@@ -67,6 +70,7 @@ export async function apiRequest<ApiResponse = unknown>(
args: {
params?: RequestParamsOrData;
data?: RequestParamsOrData;
timeout?: number;
} = {},
) {
const { data } = await api().request<ApiResponse>({

View File

@@ -31,6 +31,7 @@ export const apiFetchNotifications = async (
export const apiFetchNotificationGroups = async (params?: {
url?: string;
grouped_types?: string[];
exclude_types?: string[];
max_id?: string;
since_id?: string;
@@ -91,5 +92,5 @@ export const apiAcceptNotificationRequests = async (id: string[]) => {
};
export const apiDismissNotificationRequests = async (id: string[]) => {
return apiRequestPost('v1/notifications/dismiss/dismiss', { id });
return apiRequestPost('v1/notifications/requests/dismiss', { id });
};

View File

@@ -0,0 +1,67 @@
import { useState, useCallback, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/Overlay';
import type {
OffsetValue,
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const AltTextBadge: React.FC<{
description: string;
}> = ({ description }) => {
const anchorRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const handleClick = useCallback(() => {
setOpen((v) => !v);
}, [setOpen]);
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
return (
<>
<button
ref={anchorRef}
className='media-gallery__alt__label'
onClick={handleClick}
>
ALT
</button>
<Overlay
rootClose
onHide={handleClose}
show={open}
target={anchorRef.current}
placement='top-end'
flip
offset={offset}
popperConfig={popperConfig}
>
{({ props }) => (
<div {...props} className='hover-card-controller'>
<div
className='media-gallery__alt__popover dropdown-animation'
role='tooltip'
>
<h4>
<FormattedMessage
id='alt_text_badge.title'
defaultMessage='Alt text'
/>
</h4>
<p>{description}</p>
</div>
</div>
)}
</Overlay>
</>
);
};

View File

@@ -1,10 +1,11 @@
import { useState, useCallback } from 'react';
import classNames from 'classnames';
import { useHovering } from 'flavours/glitch/hooks/useHovering';
import { autoPlayGif } from 'flavours/glitch/initial_state';
import type { Account } from 'flavours/glitch/models/account';
import { useHovering } from '../hooks/useHovering';
import { autoPlayGif } from '../initial_state';
interface Props {
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size: number;
@@ -25,6 +26,8 @@ export const Avatar: React.FC<Props> = ({
counterBorderColor,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const style = {
...styleFromParent,
@@ -37,17 +40,29 @@ export const Avatar: React.FC<Props> = ({
? account?.get('avatar')
: account?.get('avatar_static');
const handleLoad = useCallback(() => {
setLoading(false);
}, [setLoading]);
const handleError = useCallback(() => {
setError(true);
}, [setError]);
return (
<div
className={classNames('account__avatar', {
'account__avatar-inline': inline,
'account__avatar--inline': inline,
'account__avatar--loading': loading,
})}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={style}
data-avatar-of={account && `@${account.get('acct')}`}
>
{src && <img src={src} alt='' />}
{src && !error && (
<img src={src} alt='' onLoad={handleLoad} onError={handleError} />
)}
{counter && (
<div
className='account__avatar__counter'

View File

@@ -7,6 +7,7 @@ interface BaseProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
block?: boolean;
secondary?: boolean;
dangerous?: boolean;
}
interface PropsChildren extends PropsWithChildren<BaseProps> {
@@ -26,6 +27,7 @@ export const Button: React.FC<Props> = ({
disabled,
block,
secondary,
dangerous,
className,
title,
text,
@@ -46,6 +48,7 @@ export const Button: React.FC<Props> = ({
className={classNames('button', className, {
'button-secondary': secondary,
'button--block': block,
'button--dangerous': dangerous,
})}
disabled={disabled}
onClick={handleClick}

View File

@@ -0,0 +1,27 @@
/* Significantly rewritten from upstream to keep the old design for now */
import { FormattedMessage } from 'react-intl';
export const ContentWarning: React.FC<{
text: string;
expanded?: boolean;
onClick?: () => void;
icons?: React.ReactNode[];
}> = ({ text, expanded, onClick, icons }) => (
<p>
<span dangerouslySetInnerHTML={{ __html: text }} className='translate' />{' '}
<button
type='button'
className='status__content__spoiler-link'
onClick={onClick}
aria-expanded={expanded}
>
{expanded ? (
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
) : (
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
)}
{icons}
</button>
</p>
);

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

@@ -10,7 +10,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import { AltTextBadge } from 'flavours/glitch/components/alt_text_badge';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { formatTime } from 'flavours/glitch/features/video';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
@@ -58,7 +60,7 @@ class Item extends PureComponent {
hoverToPlay () {
const { attachment } = this.props;
return !this.getAutoPlay() && attachment.get('type') === 'gifv';
return !this.getAutoPlay() && ['gifv', 'video'].includes(attachment.get('type'));
}
handleClick = (e) => {
@@ -97,7 +99,7 @@ class Item extends PureComponent {
}
if (attachment.get('description')?.length > 0) {
badges.push(<span key='alt' className='media-gallery__alt__label'>ALT</span>);
badges.push(<AltTextBadge key='alt' description={attachment.get('description')} />);
}
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
@@ -152,10 +154,15 @@ class Item extends PureComponent {
/>
</a>
);
} else if (attachment.get('type') === 'gifv') {
} else if (['gifv', 'video'].includes(attachment.get('type'))) {
const autoPlay = this.getAutoPlay();
const duration = attachment.getIn(['meta', 'original', 'duration']);
badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);
if (attachment.get('type') === 'gifv') {
badges.push(<span key='gif' className='media-gallery__alt__label media-gallery__alt__label--non-interactive'>GIF</span>);
} else {
badges.push(<span key='video' className='media-gallery__alt__label media-gallery__alt__label--non-interactive'>{formatTime(Math.floor(duration))}</span>);
}
thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
@@ -169,6 +176,7 @@ class Item extends PureComponent {
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onLoadedData={this.handleImageLoad}
autoPlay={autoPlay}
playsInline
loop
@@ -190,7 +198,7 @@ class Item extends PureComponent {
{visible && thumbnail}
{badges && (
{visible && badges && (
<div className='media-gallery__item__badges'>
{badges}
</div>
@@ -348,14 +356,14 @@ class MediaGallery extends PureComponent {
return (
<div className={computedClass} style={style} ref={this.handleRef}>
{children}
{(!visible || uncached) && (
<div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
{spoilerButton}
</div>
)}
{children}
{(visible && !uncached) && (
<div className='media-gallery__actions'>
<button className='media-gallery__actions__pill' onClick={this.handleOpen}><FormattedMessage id='media_gallery.hide' defaultMessage='Hide' /></button>

View File

@@ -153,7 +153,7 @@ class ModalRoot extends PureComponent {
return (
<div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.7)` : null }} />
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.9)` : null }} />
<div role='dialog' className='modal-root__container'>{children}</div>
</div>
</div>

View File

@@ -4,22 +4,22 @@ import AccountNavigation from 'flavours/glitch/features/account/navigation';
import Trends from 'flavours/glitch/features/getting_started/containers/trends_container';
import { showTrends } from 'flavours/glitch/initial_state';
const DefaultNavigation: React.FC = () =>
showTrends ? (
<>
<div className='flex-spacer' />
<Trends />
</>
) : null;
const DefaultNavigation: React.FC = () => (showTrends ? <Trends /> : null);
export const NavigationPortal: React.FC = () => (
<Switch>
<Route path='/@:acct' exact component={AccountNavigation} />
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} />
</Switch>
<div className='navigation-panel__portal'>
<Switch>
<Route path='/@:acct' exact component={AccountNavigation} />
<Route
path='/@:acct/tagged/:tagged?'
exact
component={AccountNavigation}
/>
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} />
</Switch>
</div>
);

View File

@@ -51,7 +51,8 @@ function normalizePath(
if (
layoutFromWindow() === 'multi-column' &&
!location.pathname?.startsWith('/deck')
location.pathname &&
!location.pathname.startsWith('/deck')
) {
location.pathname = `/deck${location.pathname}`;
}

View File

@@ -648,6 +648,27 @@ class Status extends ImmutablePureComponent {
media={status.get('media_attachments')}
/>,
);
} else if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
media.push(
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={attachments}
lang={language}
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
hidden={isCollapsed || !isExpanded}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>,
);
mediaIcons.push('picture-o');
} else if (attachments.getIn([0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
@@ -703,27 +724,6 @@ class Status extends ImmutablePureComponent {
</Bundle>,
);
mediaIcons.push('video-camera');
} else { // Media type is 'image' or 'gifv'
media.push(
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={attachments}
lang={language}
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
hidden={isCollapsed || !isExpanded}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>,
);
mediaIcons.push('picture-o');
}
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {

View File

@@ -315,36 +315,48 @@ class StatusActionBar extends ImmutablePureComponent {
}
const filterButton = this.props.onFilter && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
</div>
);
return (
<div className='status__action-bar'>
<IconButton
className='status__action-bar-button'
title={replyTitle}
icon={replyIcon}
iconComponent={replyIconComponent}
onClick={this.handleReplyClick}
counter={showReplyCount ? status.get('replies_count') : undefined}
obfuscateCount
/>
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<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} />
<div className='status__action-bar__button-wrapper'>
<IconButton
className='status__action-bar-button'
title={replyTitle}
icon={replyIcon}
iconComponent={replyIconComponent}
onClick={this.handleReplyClick}
counter={showReplyCount ? status.get('replies_count') : undefined}
obfuscateCount
/>
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<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} />
</div>
<div className='status__action-bar__button-wrapper'>
<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} />
</div>
{filterButton}
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
size={18}
iconComponent={MoreHorizIcon}
direction='right'
ariaLabel={intl.formatMessage(messages.more)}
/>
<div className='status__action-bar__button-wrapper'>
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
size={18}
iconComponent={MoreHorizIcon}
direction='right'
ariaLabel={intl.formatMessage(messages.more)}
/>
</div>
<div className='status__action-bar-spacer' />
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>

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

@@ -14,6 +14,7 @@ import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
import MovieIcon from '@/material-icons/400-24px/movie.svg?react';
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
import { ContentWarning } from 'flavours/glitch/components/content_warning';
import { Icon } from 'flavours/glitch/components/icon';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
@@ -350,7 +351,7 @@ class StatusContent extends PureComponent {
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 spoilerHtml = 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': parseClick && !disabled,
@@ -375,45 +376,26 @@ class StatusContent extends PureComponent {
</Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
let toggleText = null;
if (hidden) {
toggleText = [
<FormattedMessage
id='status.show_more'
defaultMessage='Show more'
key='0'
/>,
];
if (mediaIcons) {
const mediaComponents = {
'link': LinkIcon,
'picture-o': ImageIcon,
'tasks': InsertChartIcon,
'video-camera': MovieIcon,
'music': MusicNoteIcon,
};
let spoilerIcons = [];
if (hidden && mediaIcons) {
const mediaComponents = {
'link': LinkIcon,
'picture-o': ImageIcon,
'tasks': InsertChartIcon,
'video-camera': MovieIcon,
'music': MusicNoteIcon,
};
mediaIcons.forEach((mediaIcon, idx) => {
toggleText.push(
<Icon
fixedWidth
className='status__content__spoiler-icon'
id={mediaIcon}
icon={mediaComponents[mediaIcon]}
aria-hidden='true'
key={`icon-${idx}`}
/>,
);
});
}
} else {
toggleText = (
<FormattedMessage
id='status.show_less'
defaultMessage='Show less'
key='0'
spoilerIcons = mediaIcons.map((mediaIcon) => (
<Icon
fixedWidth
className='status__content__spoiler-icon'
id={mediaIcon}
icon={mediaComponents[mediaIcon]}
aria-hidden='true'
key={`icon-${mediaIcon}`}
/>
);
));
}
if (hidden) {
@@ -422,15 +404,7 @@ class StatusContent extends PureComponent {
return (
<div className={classNames} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<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' onClick={this.handleSpoilerClick} aria-expanded={!hidden}>
{toggleText}
</button>
</p>
<ContentWarning text={spoilerHtml} expanded={!hidden} onClick={this.handleSpoilerClick} icons={spoilerIcons} />
{mentionsPlaceholder}

View File

@@ -43,10 +43,7 @@ class AccountNavigation extends PureComponent {
}
return (
<>
<div className='flex-spacer' />
<FeaturedTags accountId={accountId} tagged={tagged} />
</>
<FeaturedTags accountId={accountId} tagged={tagged} />
);
}

View File

@@ -1,158 +0,0 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AudiotrackIcon from '@/material-icons/400-24px/music_note.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
displayWidth: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
};
state = {
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
loaded: false,
};
handleImageLoad = () => {
this.setState({ loaded: true });
};
handleMouseEnter = e => {
if (this.hoverToPlay()) {
e.target.play();
}
};
handleMouseLeave = e => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
};
hoverToPlay () {
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
}
handleClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (this.state.visible) {
this.props.onOpenMedia(this.props.attachment);
} else {
this.setState({ visible: true });
}
}
};
render () {
const { attachment, displayWidth } = this.props;
const { visible, loaded } = this.state;
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
const height = width;
const status = attachment.get('status');
const title = status.get('spoiler_text') || attachment.get('description');
let thumbnail, label, icon, content;
if (!visible) {
icon = (
<span className='account-gallery__item__icons'>
<Icon id='eye-slash' icon={VisibilityOffIcon} />
</span>
);
} else {
if (['audio', 'video'].includes(attachment.get('type'))) {
content = (
<img
src={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')}
lang={status.get('language')}
onLoad={this.handleImageLoad}
/>
);
if (attachment.get('type') === 'audio') {
label = <Icon id='music' icon={AudiotrackIcon} />;
} else {
label = <Icon id='play' icon={PlayArrowIcon} />;
}
} else if (attachment.get('type') === 'image') {
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
content = (
<img
src={attachment.get('preview_url')}
alt={attachment.get('description')}
lang={status.get('language')}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (attachment.get('type') === 'gifv') {
content = (
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
title={attachment.get('description')}
lang={status.get('language')}
role='application'
src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlayGif}
playsInline
loop
muted
/>
);
label = 'GIF';
}
thumbnail = (
<div className='media-gallery__gifv'>
{content}
{label && (
<div className='media-gallery__item__badges'>
<span className='media-gallery__gifv__label'>{label}</span>
</div>
)}
</div>
);
}
return (
<div className='account-gallery__item' style={{ width, height }}>
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<Blurhash
hash={attachment.get('blurhash')}
className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
dummy={!useBlurhash}
/>
{visible ? thumbnail : icon}
</a>
</div>
);
}
}

View File

@@ -0,0 +1,203 @@
import { useState, useCallback } from 'react';
import classNames from 'classnames';
import HeadphonesIcon from '@/material-icons/400-24px/headphones-fill.svg?react';
import MovieIcon from '@/material-icons/400-24px/movie-fill.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { AltTextBadge } from 'flavours/glitch/components/alt_text_badge';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { formatTime } from 'flavours/glitch/features/video';
import {
autoPlayGif,
displayMedia,
useBlurhash,
} from 'flavours/glitch/initial_state';
import type { Status, MediaAttachment } from 'flavours/glitch/models/status';
export const MediaItem: React.FC<{
attachment: MediaAttachment;
onOpenMedia: (arg0: MediaAttachment) => void;
}> = ({ attachment, onOpenMedia }) => {
const [visible, setVisible] = useState(
(displayMedia !== 'hide_all' &&
!attachment.getIn(['status', 'sensitive'])) ||
displayMedia === 'show_all',
);
const [loaded, setLoaded] = useState(false);
const handleImageLoad = useCallback(() => {
setLoaded(true);
}, [setLoaded]);
const handleMouseEnter = useCallback(
(e: React.MouseEvent<HTMLVideoElement>) => {
if (e.target instanceof HTMLVideoElement) {
void e.target.play();
}
},
[],
);
const handleMouseLeave = useCallback(
(e: React.MouseEvent<HTMLVideoElement>) => {
if (e.target instanceof HTMLVideoElement) {
e.target.pause();
e.target.currentTime = 0;
}
},
[],
);
const handleClick = useCallback(
(e: React.MouseEvent<HTMLAnchorElement>) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (visible) {
onOpenMedia(attachment);
} else {
setVisible(true);
}
}
},
[attachment, visible, onOpenMedia, setVisible],
);
const status = attachment.get('status') as Status;
const description = (attachment.getIn(['translation', 'description']) ||
attachment.get('description')) as string | undefined;
const previewUrl = attachment.get('preview_url') as string;
const fullUrl = attachment.get('url') as string;
const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
const lang = status.get('language') as string;
const blurhash = attachment.get('blurhash') as string;
const statusUrl = status.get('url') as string;
const type = attachment.get('type') as string;
let thumbnail;
const badges = [];
if (description && description.length > 0) {
badges.push(<AltTextBadge key='alt' description={description} />);
}
if (!visible) {
thumbnail = (
<div className='media-gallery__item__overlay'>
<Icon id='eye-slash' icon={VisibilityOffIcon} />
</div>
);
} else if (type === 'audio') {
thumbnail = (
<>
<img
src={previewUrl || avatarUrl}
alt={description}
title={description}
lang={lang}
onLoad={handleImageLoad}
/>
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
<Icon id='music' icon={HeadphonesIcon} />
</div>
</>
);
} else if (type === 'image') {
const focusX = (attachment.getIn(['meta', 'focus', 'x']) || 0) as number;
const focusY = (attachment.getIn(['meta', 'focus', 'y']) || 0) as number;
const x = (focusX / 2 + 0.5) * 100;
const y = (focusY / -2 + 0.5) * 100;
thumbnail = (
<img
src={previewUrl}
alt={description}
title={description}
lang={lang}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={handleImageLoad}
/>
);
} else if (['video', 'gifv'].includes(type)) {
const duration = attachment.getIn([
'meta',
'original',
'duration',
]) as number;
thumbnail = (
<div className='media-gallery__gifv'>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={description}
title={description}
lang={lang}
src={fullUrl}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onLoadedData={handleImageLoad}
autoPlay={autoPlayGif}
playsInline
loop
muted
/>
{type === 'video' && (
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
<Icon id='play' icon={MovieIcon} />
</div>
)}
</div>
);
if (type === 'gifv') {
badges.push(
<span
key='gif'
className='media-gallery__alt__label media-gallery__alt__label--non-interactive'
>
GIF
</span>,
);
} else {
badges.push(
<span
key='video'
className='media-gallery__alt__label media-gallery__alt__label--non-interactive'
>
{formatTime(Math.floor(duration))}
</span>,
);
}
}
return (
<div className='media-gallery__item media-gallery__item--square'>
<Blurhash
hash={blurhash}
className={classNames('media-gallery__preview', {
'media-gallery__preview--hidden': visible && loaded,
})}
dummy={!useBlurhash}
/>
<a
className='media-gallery__item-thumbnail'
href={statusUrl}
onClick={handleClick}
target='_blank'
rel='noopener noreferrer'
>
{thumbnail}
</a>
{badges.length > 0 && (
<div className='media-gallery__item__badges'>{badges}</div>
)}
</div>
);
};

View File

@@ -20,7 +20,7 @@ import { expandAccountMediaTimeline } from '../../actions/timelines';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
import MediaItem from './components/media_item';
import { MediaItem } from './components/media_item';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);

View File

@@ -21,11 +21,11 @@ const messages = defineMessages({
export const SensitiveButton = () => {
const intl = useIntl();
const spoilersAlwaysOn = useAppSelector((state) => state.getIn(['local_settings', 'always_show_spoilers_field']));
const spoilerText = useAppSelector((state) => state.getIn(['compose', 'spoiler_text']));
const sensitive = useAppSelector((state) => state.getIn(['compose', 'sensitive']));
const spoiler = useAppSelector((state) => state.getIn(['compose', 'spoiler']));
const mediaCount = useAppSelector((state) => state.getIn(['compose', 'media_attachments']).size);
const spoilersAlwaysOn = useAppSelector((state) => state.local_settings.getIn(['always_show_spoilers_field']));
const spoilerText = useAppSelector((state) => state.compose.get('spoiler_text'));
const sensitive = useAppSelector((state) => state.compose.get('sensitive'));
const spoiler = useAppSelector((state) => state.compose.get('spoiler'));
const mediaCount = useAppSelector((state) => state.compose.get('media_attachments').size);
const disabled = spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler;
const active = sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0);

View File

@@ -1,81 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import spring from 'react-motion/lib/spring';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { undoUploadCompose, initMediaEditModal } from 'flavours/glitch/actions/compose';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import Motion from 'flavours/glitch/features/ui/util/optional_motion';
export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
const dispatch = useDispatch();
const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
const sensitive = useSelector(state => state.getIn(['compose', 'sensitive']));
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
}, [dispatch, id]);
const handleFocalPointClick = useCallback(() => {
dispatch(initMediaEditModal(id));
}, [dispatch, id]);
const handleDragStart = useCallback(() => {
onDragStart(id);
}, [onDragStart, id]);
const handleDragEnter = useCallback(() => {
onDragEnter(id);
}, [onDragEnter, id]);
if (!media) {
return null;
}
const focusX = media.getIn(['meta', 'focus', 'x']);
const focusY = media.getIn(['meta', 'focus', 'y']);
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
const missingDescription = (media.get('description') || '').length === 0;
return (
<div className='compose-form__upload' draggable onDragStart={handleDragStart} onDragEnter={handleDragEnter} onDragEnd={onDragEnd}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
{sensitive && <Blurhash
hash={media.get('blurhash')}
className='compose-form__upload__preview'
/>}
<div className='compose-form__upload__actions'>
<button type='button' className='icon-button compose-form__upload__delete' onClick={handleUndoClick}><Icon icon={CloseIcon} /></button>
<button type='button' className='icon-button' onClick={handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
</div>
<div className='compose-form__upload__warning'>
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
</div>
</div>
)}
</Motion>
</div>
);
};
Upload.propTypes = {
id: PropTypes.string,
onDragEnter: PropTypes.func,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
};

View File

@@ -0,0 +1,130 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import {
undoUploadCompose,
initMediaEditModal,
} from 'flavours/glitch/actions/compose';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
export const Upload: React.FC<{
id: string;
dragging?: boolean;
overlay?: boolean;
tall?: boolean;
wide?: boolean;
}> = ({ id, dragging, overlay, tall, wide }) => {
const dispatch = useAppDispatch();
const media = useAppSelector(
(state) =>
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
.find((item: MediaAttachment) => item.get('id') === id) as // eslint-disable-line @typescript-eslint/no-unsafe-member-access
| MediaAttachment
| undefined,
);
const sensitive = useAppSelector(
(state) => state.compose.get('sensitive') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
}, [dispatch, id]);
const handleFocalPointClick = useCallback(() => {
dispatch(initMediaEditModal(id));
}, [dispatch, id]);
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
if (!media) {
return null;
}
const focusX = media.getIn(['meta', 'focus', 'x']) as number;
const focusY = media.getIn(['meta', 'focus', 'y']) as number;
const x = (focusX / 2 + 0.5) * 100;
const y = (focusY / -2 + 0.5) * 100;
const missingDescription =
((media.get('description') as string | undefined) ?? '').length === 0;
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
className={classNames('compose-form__upload media-gallery__item', {
dragging,
overlay,
'media-gallery__item--tall': tall,
'media-gallery__item--wide': wide,
})}
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
>
<div
className='compose-form__upload__thumbnail'
style={{
backgroundImage: !sensitive
? `url(${media.get('preview_url') as string})`
: undefined,
backgroundPosition: `${x}% ${y}%`,
}}
>
{sensitive && (
<Blurhash
hash={media.get('blurhash') as string}
className='compose-form__upload__preview'
/>
)}
<div className='compose-form__upload__actions'>
<button
type='button'
className='icon-button compose-form__upload__delete'
onClick={handleUndoClick}
>
<Icon id='close' icon={CloseIcon} />
</button>
<button
type='button'
className='icon-button'
onClick={handleFocalPointClick}
>
<Icon id='edit' icon={EditIcon} />{' '}
<FormattedMessage id='upload_form.edit' defaultMessage='Edit' />
</button>
</div>
<div className='compose-form__upload__warning'>
<button
type='button'
className={classNames('icon-button', {
active: missingDescription,
})}
onClick={handleFocalPointClick}
>
{missingDescription && <Icon id='warning' icon={WarningIcon} />} ALT
</button>
</div>
</div>
</div>
);
};

View File

@@ -1,56 +0,0 @@
import { useRef, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { changeMediaOrder } from 'flavours/glitch/actions/compose';
import { SensitiveButton } from './sensitive_button';
import { Upload } from './upload';
import { UploadProgress } from './upload_progress';
export const UploadForm = () => {
const dispatch = useDispatch();
const mediaIds = useSelector(state => state.getIn(['compose', 'media_attachments']).map(item => item.get('id')));
const active = useSelector(state => state.getIn(['compose', 'is_uploading']));
const progress = useSelector(state => state.getIn(['compose', 'progress']));
const isProcessing = useSelector(state => state.getIn(['compose', 'is_processing']));
const dragItem = useRef();
const dragOverItem = useRef();
const handleDragStart = useCallback(id => {
dragItem.current = id;
}, [dragItem]);
const handleDragEnter = useCallback(id => {
dragOverItem.current = id;
}, [dragOverItem]);
const handleDragEnd = useCallback(() => {
dispatch(changeMediaOrder(dragItem.current, dragOverItem.current));
dragItem.current = null;
dragOverItem.current = null;
}, [dispatch, dragItem, dragOverItem]);
return (
<>
<UploadProgress active={active} progress={progress} isProcessing={isProcessing} />
{mediaIds.size > 0 && (
<div className='compose-form__uploads'>
{mediaIds.map(id => (
<Upload
key={id}
id={id}
onDragStart={handleDragStart}
onDragEnter={handleDragEnter}
onDragEnd={handleDragEnd}
/>
))}
</div>
)}
{!mediaIds.isEmpty() && <SensitiveButton />}
</>
);
};

View File

@@ -0,0 +1,188 @@
import { useState, useCallback, useMemo } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import type { List } from 'immutable';
import type {
DragStartEvent,
DragEndEvent,
UniqueIdentifier,
Announcements,
ScreenReaderInstructions,
} from '@dnd-kit/core';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
rectSortingStrategy,
} from '@dnd-kit/sortable';
import { changeMediaOrder } from 'flavours/glitch/actions/compose';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { SensitiveButton } from './sensitive_button';
import { Upload } from './upload';
import { UploadProgress } from './upload_progress';
const messages = defineMessages({
screenReaderInstructions: {
id: 'upload_form.drag_and_drop.instructions',
defaultMessage:
'To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.',
},
onDragStart: {
id: 'upload_form.drag_and_drop.on_drag_start',
defaultMessage: 'Picked up media attachment {item}.',
},
onDragOver: {
id: 'upload_form.drag_and_drop.on_drag_over',
defaultMessage: 'Media attachment {item} was moved.',
},
onDragEnd: {
id: 'upload_form.drag_and_drop.on_drag_end',
defaultMessage: 'Media attachment {item} was dropped.',
},
onDragCancel: {
id: 'upload_form.drag_and_drop.on_drag_cancel',
defaultMessage:
'Dragging was cancelled. Media attachment {item} was dropped.',
},
});
export const UploadForm: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const mediaIds = useAppSelector(
(state) =>
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
.map((item: MediaAttachment) => item.get('id')) as List<string>, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
);
const active = useAppSelector(
(state) => state.compose.get('is_uploading') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const progress = useAppSelector(
(state) => state.compose.get('progress') as number, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const isProcessing = useAppSelector(
(state) => state.compose.get('is_processing') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragStart = useCallback(
(e: DragStartEvent) => {
const { active } = e;
setActiveId(active.id);
},
[setActiveId],
);
const handleDragEnd = useCallback(
(e: DragEndEvent) => {
const { active, over } = e;
if (over && active.id !== over.id) {
dispatch(changeMediaOrder(active.id, over.id));
}
setActiveId(null);
},
[dispatch, setActiveId],
);
const accessibility: {
screenReaderInstructions: ScreenReaderInstructions;
announcements: Announcements;
} = useMemo(
() => ({
screenReaderInstructions: {
draggable: intl.formatMessage(messages.screenReaderInstructions),
},
announcements: {
onDragStart({ active }) {
return intl.formatMessage(messages.onDragStart, { item: active.id });
},
onDragOver({ active }) {
return intl.formatMessage(messages.onDragOver, { item: active.id });
},
onDragEnd({ active }) {
return intl.formatMessage(messages.onDragEnd, { item: active.id });
},
onDragCancel({ active }) {
return intl.formatMessage(messages.onDragCancel, { item: active.id });
},
},
}),
[intl],
);
return (
<>
<UploadProgress
active={active}
progress={progress}
isProcessing={isProcessing}
/>
{mediaIds.size > 0 && (
<div
className={`compose-form__uploads media-gallery media-gallery--layout-${mediaIds.size}`}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
accessibility={accessibility}
>
<SortableContext
items={mediaIds.toArray()}
strategy={rectSortingStrategy}
>
{mediaIds.map((id, idx) => (
<Upload
key={id}
id={id}
dragging={id === activeId}
tall={mediaIds.size < 3 || (mediaIds.size === 3 && idx === 0)}
wide={mediaIds.size === 1}
/>
))}
</SortableContext>
<DragOverlay>
{activeId ? <Upload id={activeId as string} overlay /> : null}
</DragOverlay>
</DndContext>
</div>
)}
{!mediaIds.isEmpty() && <SensitiveButton />}
</>
);
};

View File

@@ -1,5 +1,5 @@
import type { ChangeEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@@ -26,6 +26,8 @@ import { RadioButton } from 'flavours/glitch/components/radio_button';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { useSearchParam } from '../../hooks/useSearchParam';
import { AccountCard } from './components/account_card';
const messages = defineMessages({
@@ -50,18 +52,19 @@ export const Directory: React.FC<{
const intl = useIntl();
const dispatch = useAppDispatch();
const [state, setState] = useState<{
order: string | null;
local: boolean | null;
}>({
order: null,
local: null,
});
const column = useRef<Column>(null);
const order = state.order ?? params?.order ?? 'active';
const local = state.local ?? params?.local ?? false;
const [orderParam, setOrderParam] = useSearchParam('order');
const [localParam, setLocalParam] = useSearchParam('local');
let localParamBool: boolean | undefined;
if (localParam === 'false') {
localParamBool = false;
}
const order = orderParam ?? params?.order ?? 'active';
const local = localParamBool ?? params?.local ?? true;
const handlePin = useCallback(() => {
if (columnId) {
@@ -104,10 +107,10 @@ export const Directory: React.FC<{
if (columnId) {
dispatch(changeColumnParams(columnId, ['order'], e.target.value));
} else {
setState((s) => ({ order: e.target.value, local: s.local }));
setOrderParam(e.target.value);
}
},
[dispatch, columnId],
[dispatch, columnId, setOrderParam],
);
const handleChangeLocal = useCallback<ChangeEventHandler<HTMLInputElement>>(
@@ -116,11 +119,13 @@ export const Directory: React.FC<{
dispatch(
changeColumnParams(columnId, ['local'], e.target.value === '1'),
);
} else if (e.target.value === '1') {
setLocalParam('true');
} else {
setState((s) => ({ local: e.target.value === '1', order: s.order }));
setLocalParam('false');
}
},
[dispatch, columnId],
[dispatch, columnId, setLocalParam],
);
const handleLoadMore = useCallback(() => {

View File

@@ -31,7 +31,7 @@ export const FilteredNotificationsIconButton: React.FC<{
history.push('/notifications/requests');
}, [history]);
if (policy === null || policy.summary.pending_notifications_count === 0) {
if (policy === null || policy.summary.pending_requests_count <= 0) {
return null;
}
@@ -70,7 +70,7 @@ export const FilteredNotificationsBanner: React.FC = () => {
};
}, [dispatch]);
if (policy === null || policy.summary.pending_notifications_count === 0) {
if (policy === null || policy.summary.pending_requests_count <= 0) {
return null;
}

View File

@@ -8,11 +8,13 @@ import type { List as ImmutableList, RecordOf } from 'immutable';
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
import { toggleStatusSpoilers } from 'flavours/glitch/actions/statuses';
import { Avatar } from 'flavours/glitch/components/avatar';
import { ContentWarning } from 'flavours/glitch/components/content_warning';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { Icon } from 'flavours/glitch/components/icon';
import type { Status } from 'flavours/glitch/models/status';
import { useAppSelector } from 'flavours/glitch/store';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { EmbeddedStatusContent } from './embedded_status_content';
@@ -23,6 +25,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
}) => {
const history = useHistory();
const clickCoordinatesRef = useRef<[number, number] | null>();
const dispatch = useAppDispatch();
const status = useAppSelector(
(state) => state.statuses.get(statusId) as Status | undefined,
@@ -96,15 +99,21 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
[],
);
const handleContentWarningClick = useCallback(() => {
dispatch(toggleStatusSpoilers(statusId));
}, [dispatch, statusId]);
if (!status) {
return null;
}
// Assign status attributes to variables with a forced type, as status is not yet properly typed
const contentHtml = status.get('contentHtml') as string;
const contentWarning = status.get('spoilerHtml') as string;
const poll = status.get('poll');
const language = status.get('language') as string;
const mentions = status.get('mentions') as ImmutableList<Mention>;
const expanded = !status.get('hidden') || !contentWarning;
const mediaAttachmentsSize = (
status.get('media_attachments') as ImmutableList<unknown>
).size;
@@ -124,14 +133,24 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
<DisplayName account={account} />
</div>
<EmbeddedStatusContent
className='notification-group__embedded-status__content reply-indicator__content translate'
content={contentHtml}
language={language}
mentions={mentions}
/>
{contentWarning && (
<ContentWarning
text={contentWarning}
onClick={handleContentWarningClick}
expanded={expanded}
/>
)}
{(poll || mediaAttachmentsSize > 0) && (
{(!contentWarning || expanded) && (
<EmbeddedStatusContent
className='notification-group__embedded-status__content reply-indicator__content translate'
content={contentHtml}
language={language}
mentions={mentions}
/>
)}
{expanded && (poll || mediaAttachmentsSize > 0) && (
<div className='notification-group__embedded-status__attachments reply-indicator__attachments'>
{!!poll && (
<>

View File

@@ -21,6 +21,7 @@ import { Permalink } from 'flavours/glitch/components/permalink';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import { useAppHistory } from 'flavours/glitch/components/router';
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
import PollContainer from 'flavours/glitch/containers/poll_container';
import { useAppSelector } from 'flavours/glitch/store';
import { Avatar } from '../../../components/avatar';
@@ -194,6 +195,28 @@ export const DetailedStatus: React.FC<{
)
) {
media.push(<AttachmentList media={status.get('media_attachments')} />);
} else if (
['image', 'gifv'].includes(
status.getIn(['media_attachments', 0, 'type']) as string,
) ||
status.get('media_attachments').size > 1
) {
media.push(
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
lang={language}
height={300}
letterbox={letterboxMedia}
fullwidth={fullwidthMedia}
hidden={!expanded}
onOpenMedia={onOpenMedia}
visible={showMedia}
onToggleVisibility={onToggleMediaVisibility}
/>,
);
mediaIcons.push('picture-o');
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description =
@@ -236,6 +259,7 @@ export const DetailedStatus: React.FC<{
src={attachment.get('url')}
alt={description}
lang={language}
inline
width={300}
height={150}
onOpenVideo={handleOpenVideo}
@@ -248,23 +272,6 @@ export const DetailedStatus: React.FC<{
/>,
);
mediaIcons.push('video-camera');
} else {
media.push(
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
lang={language}
height={300}
letterbox={letterboxMedia}
fullwidth={fullwidthMedia}
hidden={!expanded}
onOpenMedia={onOpenMedia}
visible={showMedia}
onToggleVisibility={onToggleMediaVisibility}
/>,
);
mediaIcons.push('picture-o');
}
} else if (status.get('spoiler_text').length === 0) {
media.push(
@@ -277,6 +284,17 @@ export const DetailedStatus: React.FC<{
mediaIcons.push('link');
}
if (status.get('poll')) {
contentMedia.push(
<PollContainer
pollId={status.get('poll')}
// @ts-expect-error -- Poll/PollContainer is not typed yet
lang={status.get('language')}
/>,
);
contentMediaIcons.push('tasks');
}
if (status.get('application')) {
applicationLink = (
<>

View File

@@ -99,7 +99,7 @@ export const BlockModal = ({ accountId, acct }) => {
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</button>
<Button onClick={handleClick} autoFocus>
<Button onClick={handleClick} dangerous autoFocus>
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
</Button>
</div>

View File

@@ -3,11 +3,11 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useRouteMatch, NavLink } from 'react-router-dom';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, onClick, href, method, badge, transparent, ...other }) => {
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, onClick, href, method, badge, transparent, optional, ...other }) => {
const match = useRouteMatch(to);
const className = classNames('column-link', { 'column-link--transparent': transparent });
const className = classNames('column-link', { 'column-link--transparent': transparent, 'column-link--optional': optional });
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon;
const activeIconElement = activeIcon ?? (activeIconComponent ? <Icon id={icon} icon={activeIconComponent} className='column-link__icon' /> : iconElement);
@@ -58,6 +58,7 @@ ColumnLink.propTypes = {
method: PropTypes.string,
badge: PropTypes.node,
transparent: PropTypes.bool,
optional: PropTypes.bool,
};
export default ColumnLink;

View File

@@ -4,8 +4,6 @@ import { Children, cloneElement, useCallback } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
import BundleContainer from '../containers/bundle_container';
import {
@@ -65,17 +63,13 @@ export default class ColumnsArea extends ImmutablePureComponent {
};
// Corresponds to (max-width: $no-gap-breakpoint - 1px) in SCSS
mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1206px)');
mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)');
state = {
renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
};
componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
if (this.mediaQuery) {
if (this.mediaQuery.addEventListener) {
this.mediaQuery.addEventListener('change', this.handleLayoutChange);
@@ -88,23 +82,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
}
UNSAFE_componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
}
componentWillUnmount () {
if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
if (this.mediaQuery) {
if (this.mediaQuery.removeEventListener) {
this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
@@ -117,7 +95,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleChildrenContentChange() {
if (!this.props.singleColumn) {
const modifier = this.isRtlLayout ? -1 : 1;
this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}
@@ -125,14 +103,6 @@ export default class ColumnsArea extends ImmutablePureComponent {
this.setState({ renderComposePanel: !e.matches });
};
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
};
setRef = (node) => {
this.node = node;
};

View File

@@ -1,106 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'flavours/glitch/actions/accounts';
import { blockDomain } from 'flavours/glitch/actions/domain_blocks';
import { closeModal } from 'flavours/glitch/actions/modal';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
export const DomainBlockModal = ({ domain, accountId, acct }) => {
const dispatch = useDispatch();
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockDomain(domain));
}, [dispatch, domain]);
const handleSecondaryClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={DomainDisabledIcon} />
</div>
<div>
<h1><FormattedMessage id='domain_block_modal.title' defaultMessage='Block domain?' /></h1>
<div>{domain}</div>
</div>
</div>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_wont_know' defaultMessage="They won't know they've been blocked." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_wont_see_posts' defaultMessage="You won't see posts or notifications from users on this server." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={PersonRemoveIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_will_lose_followers' defaultMessage='All your followers from this server will be removed.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_cant_follow' defaultMessage='Nobody from this server can follow you.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={HistoryIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_can_interact_with_old_posts' defaultMessage='People from this server can interact with your old posts.' /></div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<Button onClick={handleSecondaryClick} secondary>
<FormattedMessage id='domain_block_modal.block_account_instead' defaultMessage='Block @{name} instead' values={{ name: acct.split('@')[0] }} />
</Button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</button>
<Button onClick={handleClick} autoFocus>
<FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
</Button>
</div>
</div>
</div>
);
};
DomainBlockModal.propTypes = {
domain: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default DomainBlockModal;

View File

@@ -0,0 +1,223 @@
import { useCallback, useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'flavours/glitch/actions/accounts';
import { blockDomain } from 'flavours/glitch/actions/domain_blocks';
import { closeModal } from 'flavours/glitch/actions/modal';
import { apiRequest } from 'flavours/glitch/api';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { useAppDispatch } from 'flavours/glitch/store';
interface DomainBlockPreviewResponse {
following_count: number;
followers_count: number;
}
export const DomainBlockModal: React.FC<{
domain: string;
accountId: string;
acct: string;
}> = ({ domain, accountId, acct }) => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
const [preview, setPreview] = useState<
DomainBlockPreviewResponse | 'error' | null
>(null);
const handleClick = useCallback(() => {
if (loading) {
return; // Prevent destructive action before the preview finishes loading or times out
}
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockDomain(domain));
}, [dispatch, loading, domain]);
const handleSecondaryClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
useEffect(() => {
setLoading(true);
apiRequest<DomainBlockPreviewResponse>('GET', 'v1/domain_blocks/preview', {
params: { domain },
timeout: 5000,
})
.then((data) => {
setPreview(data);
setLoading(false);
return '';
})
.catch(() => {
setPreview('error');
setLoading(false);
});
}, [setPreview, setLoading, domain]);
return (
<div className='modal-root__modal safety-action-modal' aria-live='polite'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon id='' icon={DomainDisabledIcon} />
</div>
<div>
<h1>
<FormattedMessage
id='domain_block_modal.title'
defaultMessage='Block domain?'
/>
</h1>
<div>{domain}</div>
</div>
</div>
<div className='safety-action-modal__bullet-points'>
{preview &&
preview !== 'error' &&
preview.followers_count + preview.following_count > 0 && (
<div>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={PersonRemoveIcon} />
</div>
<div>
<strong>
<FormattedMessage
id='domain_block_modal.you_will_lose_num_followers'
defaultMessage='You will lose {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} followers}} and {followingCount, plural, one {{followingCountDisplay} person you follow} other {{followingCountDisplay} people you follow}}.'
values={{
followersCount: preview.followers_count,
followersCountDisplay: (
<ShortNumber value={preview.followers_count} />
),
followingCount: preview.following_count,
followingCountDisplay: (
<ShortNumber value={preview.following_count} />
),
}}
/>
</strong>
</div>
</div>
)}
{preview === 'error' && (
<div>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={PersonRemoveIcon} />
</div>
<div>
<strong>
<FormattedMessage
id='domain_block_modal.you_will_lose_relationships'
defaultMessage='You will lose all followers and people you follow from this server.'
/>
</strong>
</div>
</div>
)}
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={CampaignIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_wont_know'
defaultMessage="They won't know they've been blocked."
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={VisibilityOffIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.you_wont_see_posts'
defaultMessage="You won't see posts or notifications from users on this server."
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={ReplyIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_cant_follow'
defaultMessage='Nobody from this server can follow you.'
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={HistoryIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_can_interact_with_old_posts'
defaultMessage='People from this server can interact with your old posts.'
/>
</div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<Button onClick={handleSecondaryClick} secondary>
<FormattedMessage
id='domain_block_modal.block_account_instead'
defaultMessage='Block @{name} instead'
values={{ name: acct.split('@')[0] }}
/>
</Button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'
/>
</button>
<Button onClick={handleClick} dangerous aria-busy={loading}>
{loading ? (
<LoadingIndicator />
) : (
<FormattedMessage
id='domain_block_modal.block'
defaultMessage='Block server'
/>
)}
</Button>
</div>
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default DomainBlockModal;

View File

@@ -17,7 +17,7 @@ export default class ImageLoader extends PureComponent {
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool,
zoomedIn: PropTypes.bool,
};
static defaultProps = {
@@ -134,7 +134,7 @@ export default class ImageLoader extends PureComponent {
};
render () {
const { alt, lang, src, width, height, onClick } = this.props;
const { alt, lang, src, width, height, onClick, zoomedIn } = this.props;
const { loading } = this.state;
const className = classNames('image-loader', {
@@ -149,6 +149,7 @@ export default class ImageLoader extends PureComponent {
<div className='loading-bar__container' style={{ width: this.state.width || width }}>
<LoadingBar className='loading-bar' loading={1} />
</div>
<canvas
className='image-loader__preview-canvas'
ref={this.setCanvasRef}
@@ -164,7 +165,7 @@ export default class ImageLoader extends PureComponent {
onClick={onClick}
width={width}
height={height}
zoomButtonHidden={this.props.zoomButtonHidden}
zoomedIn={zoomedIn}
/>
)}
</div>

View File

@@ -12,6 +12,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import FitScreenIcon from '@/material-icons/400-24px/fit_screen.svg?react';
import ActualSizeIcon from '@/svg-icons/actual_size.svg?react';
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
import { GIFV } from 'flavours/glitch/components/gifv';
import { Icon } from 'flavours/glitch/components/icon';
@@ -26,6 +28,8 @@ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
zoomIn: { id: 'lightbox.zoom_in', defaultMessage: 'Zoom to actual size' },
zoomOut: { id: 'lightbox.zoom_out', defaultMessage: 'Zoom to fit' },
});
class MediaModal extends ImmutablePureComponent {
@@ -46,30 +50,39 @@ class MediaModal extends ImmutablePureComponent {
state = {
index: null,
navigationHidden: false,
zoomButtonHidden: false,
zoomedIn: false,
};
handleZoomClick = () => {
this.setState(prevState => ({
zoomedIn: !prevState.zoomedIn,
}));
};
handleSwipe = (index) => {
this.setState({ index: index % this.props.media.size });
this.setState({
index: index % this.props.media.size,
zoomedIn: false,
});
};
handleTransitionEnd = () => {
this.setState({
zoomButtonHidden: false,
zoomedIn: false,
});
};
handleNextClick = () => {
this.setState({
index: (this.getIndex() + 1) % this.props.media.size,
zoomButtonHidden: true,
zoomedIn: false,
});
};
handlePrevClick = () => {
this.setState({
index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
zoomButtonHidden: true,
zoomedIn: false,
});
};
@@ -78,7 +91,7 @@ class MediaModal extends ImmutablePureComponent {
this.setState({
index: index % this.props.media.size,
zoomButtonHidden: true,
zoomedIn: false,
});
};
@@ -130,15 +143,22 @@ class MediaModal extends ImmutablePureComponent {
return this.state.index !== null ? this.state.index : this.props.index;
}
toggleNavigation = () => {
handleToggleNavigation = () => {
this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden,
}));
};
setRef = c => {
this.setState({
viewportWidth: c?.clientWidth,
viewportHeight: c?.clientHeight,
});
};
render () {
const { media, statusId, lang, intl, onClose } = this.props;
const { navigationHidden } = this.state;
const { navigationHidden, zoomedIn, viewportWidth, viewportHeight } = this.state;
const index = this.getIndex();
@@ -160,8 +180,8 @@ class MediaModal extends ImmutablePureComponent {
alt={description}
lang={lang}
key={image.get('url')}
onClick={this.toggleNavigation}
zoomButtonHidden={this.state.zoomButtonHidden}
onClick={this.handleToggleNavigation}
zoomedIn={zoomedIn}
/>
);
} else if (image.get('type') === 'video') {
@@ -229,9 +249,12 @@ class MediaModal extends ImmutablePureComponent {
));
}
const currentMedia = media.get(index);
const zoomable = currentMedia.get('type') === 'image' && (currentMedia.getIn(['meta', 'original', 'width']) > viewportWidth || currentMedia.getIn(['meta', 'original', 'height']) > viewportHeight);
return (
<div className='modal-root__modal media-modal'>
<div className='media-modal__closer' role='presentation' onClick={onClose} >
<div className='modal-root__modal media-modal' ref={this.setRef}>
<div className='media-modal__closer' role='presentation' onClick={onClose}>
<ReactSwipeableViews
style={swipeableViewsStyle}
containerStyle={containerStyle}
@@ -245,7 +268,10 @@ class MediaModal extends ImmutablePureComponent {
</div>
<div className={navigationClassName}>
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={40} />
<div className='media-modal__buttons'>
{zoomable && <IconButton title={intl.formatMessage(zoomedIn ? messages.zoomOut : messages.zoomIn)} iconComponent={zoomedIn ? FitScreenIcon : ActualSizeIcon} onClick={this.handleZoomClick} />}
<IconButton title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} />
</div>
{leftNav}
{rightNav}

View File

@@ -122,14 +122,17 @@ class NavigationPanel extends Component {
let banner = undefined;
if(transientSingleColumn)
banner = (<div className='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}
{" "}
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>);
if (transientSingleColumn) {
banner = (
<div className='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}
{" "}
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>
);
}
return (
<div className='navigation-panel'>
@@ -139,55 +142,59 @@ class NavigationPanel extends Component {
</div>
}
{signedIn && (
<>
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
<NotificationsLink />
<FollowRequestsLink />
</>
)}
<div className='navigation-panel__menu'>
{signedIn && (
<>
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
<NotificationsLink />
<FollowRequestsLink />
</>
)}
{trendsEnabled ? (
<ColumnLink transparent to='/explore' icon='explore' iconComponent={ExploreIcon} activeIconComponent={ExploreActiveIcon} text={intl.formatMessage(messages.explore)} />
) : (
<ColumnLink transparent to='/search' icon='search' iconComponent={SearchIcon} text={intl.formatMessage(messages.search)} />
)}
{trendsEnabled ? (
<ColumnLink transparent to='/explore' icon='explore' iconComponent={ExploreIcon} activeIconComponent={ExploreActiveIcon} text={intl.formatMessage(messages.explore)} />
) : (
<ColumnLink transparent to='/search' icon='search' iconComponent={SearchIcon} text={intl.formatMessage(messages.search)} />
)}
{(signedIn || timelinePreview) && (
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
)}
{(signedIn || timelinePreview) && (
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.firehose)} />
)}
{!signedIn && (
<div className='navigation-panel__sign-in-banner'>
{!signedIn && (
<div className='navigation-panel__sign-in-banner'>
<hr />
{ disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner /> }
</div>
)}
{signedIn && (
<>
<ColumnLink transparent to='/conversations' icon='at' iconComponent={MailIcon} activeIconComponent={MailActiveIcon} text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/bookmarks' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} />
<ListPanel />
<hr />
{!!preferencesLink && <ColumnLink transparent href={preferencesLink} icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} />}
<ColumnLink transparent onClick={onOpenSettings} icon='cogs' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.app_settings)} />
{canManageReports(permissions) && <ColumnLink optional transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
{canViewAdminDashboard(permissions) && <ColumnLink optional transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
</>
)}
<div className='navigation-panel__legal'>
<hr />
{ disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner /> }
<ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} />
</div>
)}
{signedIn && (
<>
<ColumnLink transparent to='/conversations' icon='at' iconComponent={MailIcon} activeIconComponent={MailActiveIcon} text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/bookmarks' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} />
<ListPanel />
<hr />
{!!preferencesLink && <ColumnLink transparent href={preferencesLink} icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} />}
<ColumnLink transparent onClick={onOpenSettings} icon='cogs' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.app_settings)} />
{canManageReports(permissions) && <ColumnLink transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
{canViewAdminDashboard(permissions) && <ColumnLink transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
</>
)}
<div className='navigation-panel__legal'>
<hr />
<ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} />
</div>
<div className='flex-spacer' />
<NavigationPortal />
</div>
);

View File

@@ -1,17 +1,6 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import FullscreenExitIcon from '@/material-icons/400-24px/fullscreen_exit.svg?react';
import RectangleIcon from '@/material-icons/400-24px/rectangle.svg?react';
import { IconButton } from 'flavours/glitch/components/icon_button';
const messages = defineMessages({
compress: { id: 'lightbox.compress', defaultMessage: 'Compress image view box' },
expand: { id: 'lightbox.expand', defaultMessage: 'Expand image view box' },
});
const MIN_SCALE = 1;
const MAX_SCALE = 4;
const NAV_BAR_HEIGHT = 66;
@@ -104,8 +93,7 @@ class ZoomableImage extends PureComponent {
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool,
intl: PropTypes.object.isRequired,
zoomedIn: PropTypes.bool,
};
static defaultProps = {
@@ -131,8 +119,6 @@ class ZoomableImage extends PureComponent {
translateX: null,
translateY: null,
},
zoomState: 'expand', // 'expand' 'compress'
navigationHidden: false,
dragPosition: { top: 0, left: 0, x: 0, y: 0 },
dragged: false,
lockScroll: { x: 0, y: 0 },
@@ -169,35 +155,20 @@ class ZoomableImage extends PureComponent {
this.container.addEventListener('DOMMouseScroll', handler);
this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler));
this.initZoomMatrix();
this._initZoomMatrix();
}
componentWillUnmount () {
this.removeEventListeners();
this._removeEventListeners();
}
componentDidUpdate () {
this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' });
if (this.state.scale === MIN_SCALE) {
this.container.style.removeProperty('cursor');
componentDidUpdate (prevProps) {
if (prevProps.zoomedIn !== this.props.zoomedIn) {
this._toggleZoom();
}
}
UNSAFE_componentWillReceiveProps () {
// reset when slide to next image
if (this.props.zoomButtonHidden) {
this.setState({
scale: MIN_SCALE,
lockTranslate: { x: 0, y: 0 },
}, () => {
this.container.scrollLeft = 0;
this.container.scrollTop = 0;
});
}
}
removeEventListeners () {
_removeEventListeners () {
this.removers.forEach(listeners => listeners());
this.removers = [];
}
@@ -220,9 +191,6 @@ class ZoomableImage extends PureComponent {
};
mouseDownHandler = e => {
this.container.style.cursor = 'grabbing';
this.container.style.userSelect = 'none';
this.setState({ dragPosition: {
left: this.container.scrollLeft,
top: this.container.scrollTop,
@@ -246,9 +214,6 @@ class ZoomableImage extends PureComponent {
};
mouseUpHandler = () => {
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
this.image.removeEventListener('mousemove', this.mouseMoveHandler);
this.image.removeEventListener('mouseup', this.mouseUpHandler);
};
@@ -276,13 +241,13 @@ class ZoomableImage extends PureComponent {
const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate);
const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance);
this.zoom(scale, midpoint);
this._zoom(scale, midpoint);
this.lastMidpoint = midpoint;
this.lastDistance = distance;
};
zoom(nextScale, midpoint) {
_zoom(nextScale, midpoint) {
const { scale, zoomMatrix } = this.state;
const { scrollLeft, scrollTop } = this.container;
@@ -318,14 +283,13 @@ class ZoomableImage extends PureComponent {
if (dragged) return;
const handler = this.props.onClick;
if (handler) handler();
this.setState({ navigationHidden: !this.state.navigationHidden });
};
handleMouseDown = e => {
e.preventDefault();
};
initZoomMatrix = () => {
_initZoomMatrix = () => {
const { width, height } = this.props;
const { clientWidth, clientHeight } = this.container;
const { offsetWidth, offsetHeight } = this.image;
@@ -357,10 +321,7 @@ class ZoomableImage extends PureComponent {
});
};
handleZoomClick = e => {
e.preventDefault();
e.stopPropagation();
_toggleZoom () {
const { scale, zoomMatrix } = this.state;
if ( scale >= zoomMatrix.rate ) {
@@ -394,10 +355,7 @@ class ZoomableImage extends PureComponent {
this.container.scrollTop = zoomMatrix.scrollTop;
});
}
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
};
}
setContainerRef = c => {
this.container = c;
@@ -408,52 +366,37 @@ class ZoomableImage extends PureComponent {
};
render () {
const { alt, lang, src, width, height, intl } = this.props;
const { scale, lockTranslate } = this.state;
const { alt, lang, src, width, height } = this.props;
const { scale, lockTranslate, dragged } = this.state;
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
const cursor = scale === MIN_SCALE ? null : (dragged ? 'grabbing' : 'grab');
return (
<>
<IconButton
className={`media-modal__zoom-button ${zoomButtonShouldHide}`}
title={zoomButtonTitle}
icon={this.state.zoomState}
iconComponent={this.state.zoomState === 'compress' ? FullscreenExitIcon : RectangleIcon}
onClick={this.handleZoomClick}
size={40}
<div
className='zoomable-image'
ref={this.setContainerRef}
style={{ overflow, cursor, userSelect: 'none' }}
>
<img
role='presentation'
ref={this.setImageRef}
alt={alt}
title={alt}
lang={lang}
src={src}
width={width}
height={height}
style={{
fontSize: '30px', /* Fontawesome's fa-compress fa-expand is larger than fa-close */
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
transformOrigin: '0 0',
}}
draggable={false}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
/>
<div
className='zoomable-image'
ref={this.setContainerRef}
style={{ overflow }}
>
<img
role='presentation'
ref={this.setImageRef}
alt={alt}
title={alt}
lang={lang}
src={src}
width={width}
height={height}
style={{
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
transformOrigin: '0 0',
}}
draggable={false}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
/>
</div>
</>
</div>
);
}
}
export default injectIntl(ZoomableImage);
export default ZoomableImage;

View File

@@ -196,7 +196,7 @@ class SwitchingColumnsArea extends PureComponent {
{redirect}
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={{...this.props.location, pathname: pathName.slice(5)}} /> : null}
{/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */}
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}

View File

@@ -0,0 +1,31 @@
import { useMemo, useCallback } from 'react';
import { useLocation, useHistory } from 'react-router';
export function useSearchParams() {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
}
export function useSearchParam(name: string, defaultValue?: string) {
const searchParams = useSearchParams();
const history = useHistory();
const value = searchParams.get(name) ?? defaultValue;
const setValue = useCallback(
(value: string | null) => {
if (value === null) {
searchParams.delete(name);
} else {
searchParams.set(name, value);
}
history.push({ search: searchParams.toString() });
},
[history, name, searchParams],
);
return [value, setValue] as const;
}

View File

@@ -2,6 +2,7 @@
"about.fork_disclaimer": "جلتش-سوك هو برنامج حر مفتوح المصدر متفرع عن ماستدون.",
"account.disclaimer_full": "قد لا تعكِس المعلومات أدناه كامل الملف الشخصي للمستخدِم.",
"account.follows": "يتابِع",
"account.follows_you": "Follows you",
"account.suspended_disclaimer_full": "تم تعليق هذا المستخدم من قبل المشرف.",
"account.view_full_profile": "عرض الملف الشخصي كاملاً",
"boost_modal.missing_description": "هذا المنشور يحتوي على وسائط بلا وصف",
@@ -13,6 +14,7 @@
"column_subheading.lists": "القوائم",
"column_subheading.navigation": "التنقل",
"community.column_settings.allow_local_only": "إظهار المنشورات المحلية فقط",
"compose.attach.doodle": "الرسوم و التخمين",
"compose.change_federation": "تغيير اعدادات الفيديرالية",
"compose.content-type.change": "تغيير خيارات التنسيق المتقدمة",
"compose.content-type.html": "HTML",
@@ -26,13 +28,8 @@
"confirmations.deprecated_settings.message": "تم استبدال بعض من الجهاز الخاص بالماستدون {preferences} الذي تستخدمه {app_settings} الخاص بجهاز ماستدون سيتم تجاوزه:",
"confirmations.missing_media_description.confirm": "أرسل على أيّة حال",
"confirmations.missing_media_description.edit": "تعديل الوسائط",
"confirmations.unfilter.author": "المؤلف",
"confirmations.unfilter.confirm": "عرض",
"confirmations.unfilter.edit_filter": "تعديل عامل التصفية",
"confirmations.unfilter.filters": "مطابقة {count, plural, zero {}one {فلتر} two {فلاتر} few {فلاتر} many {فلاتر} other {فلاتر}}",
"direct.group_by_conversations": "تجميع حسب المحادثة",
"endorsed_accounts_editor.endorsed_accounts": "الحسابات المميزة",
"favourite_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة",
"federation.federated.long": "السماح لهذا المنشور الوصول إلى خوادم أخرى",
"federation.local_only.long": "منع هذا المنشور من الوصول إلى الخوادم الأخرى",
"federation.local_only.short": "محلي فقط",

View File

@@ -6,7 +6,6 @@
"confirmation_modal.do_not_ask_again": "Příště se už neptat",
"direct.group_by_conversations": "Seskupit do konverzací",
"endorsed_accounts_editor.endorsed_accounts": "Vybrané účty",
"favourite_modal.combo": "Příště můžete pro přeskočení stisknout {combo}",
"home.column_settings.advanced": "Pokročilé",
"home.column_settings.filter_regex": "Filtrovat podle regulárních výrazů",
"home.column_settings.show_direct": "Zobrazit přímé zprávy",

View File

@@ -18,9 +18,6 @@
"compose.content-type.plain": "Testun plaen",
"confirmations.missing_media_description.confirm": "Anfon beth bynnag",
"confirmations.missing_media_description.edit": "Golygu cyfryngau",
"confirmations.unfilter.author": "Awdur",
"confirmations.unfilter.confirm": "Dangos",
"confirmations.unfilter.edit_filter": "Golygi hidlydd",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View File

@@ -34,13 +34,9 @@
"confirmations.missing_media_description.confirm": "Trotzdem absenden",
"confirmations.missing_media_description.edit": "Anhänge bearbeiten",
"confirmations.missing_media_description.message": "Mindestens einem Anhang fehlt eine Beschreibung. Denke darüber nach, alle Anhänge für Sehbeeinträchtigte zu beschreiben, bevor du den Toot absendest.",
"confirmations.unfilter.author": "Urheber",
"confirmations.unfilter.confirm": "Anzeigen",
"confirmations.unfilter.edit_filter": "Filter bearbeiten",
"confirmations.unfilter.filters": "Passende{count, plural, one {r} other {}} Filter",
"direct.group_by_conversations": "Nach Unterhaltung gruppieren",
"endorsed_accounts_editor.endorsed_accounts": "Empfohlene Konten",
"favourite_modal.combo": "Mit {combo} wird dieses Fenster beim nächsten Mal nicht mehr angezeigt",
"favourite_modal.favourite": "Beitrag favorisieren?",
"federation.federated.long": "Erlaube diesem Beitrag, andere Server zu erreichen",
"federation.federated.short": "Föderiert",
"federation.local_only.long": "Verhindere, dass dieser Post andere Server erreicht",
@@ -128,6 +124,7 @@
"settings.shared_settings_link": "Nutzereinstellungen",
"settings.show_action_bar": "Aktions-Knöpfe in eingeklappten Toots anzeigen",
"settings.show_content_type_choice": "Auswahl für die Inhaltsart beim Verfassen von Toots anzeigen",
"settings.show_published_toast": "Meldung beim Veröffentlichen/Speichern eines Beitrags anzeigen",
"settings.show_reply_counter": "Schätzung der Antwortanzahl anzeigen",
"settings.side_arm": "Sekundärer Toot-Knopf:",
"settings.side_arm.none": "Nichts",
@@ -147,12 +144,17 @@
"settings.wide_view": "Breite Ansicht (nur für den Desktop-Modus)",
"settings.wide_view_hint": "Verbreitert Spalten, um den verfügbaren Platz besser zu füllen.",
"status.collapse": "Einklappen",
"status.filtered": "Gefiltert",
"status.has_audio": "Hat angehängte Audiodateien",
"status.has_pictures": "Hat angehängte Bilder",
"status.has_preview_card": "Hat eine Vorschaukarte",
"status.has_video": "Hat angehängte Videos",
"status.hide": "Beitrag ausblenden",
"status.in_reply_to": "Dieser Toot ist eine Antwort",
"status.is_poll": "Dieser Toot ist eine Umfrage",
"status.local_only": "Nur auf deiner Instanz sichtbar",
"status.show_filter_reason": "Trotzdem anzeigen",
"status.show_less": "Weniger anzeigen",
"status.show_more": "Mehr anzeigen",
"status.uncollapse": "Ausklappen"
}

View File

@@ -22,10 +22,6 @@
"confirmations.missing_media_description.confirm": "Sendi ĉiuokaze",
"confirmations.missing_media_description.edit": "Redakti aŭdovidaĵon",
"confirmations.missing_media_description.message": "Unu aŭ pli da plurmedioj mankas priskribo. Bonvolu priskribi ĉiujn plurmediojn por la vida-malkapabluloj antaŭ ol sendi vian afiŝon.",
"confirmations.unfilter.author": "Aŭtoro",
"confirmations.unfilter.confirm": "Montri",
"confirmations.unfilter.edit_filter": "Redakti filtrilon",
"confirmations.unfilter.filters": "{count, plural, one {# filtrilo} other {# filtriloj}} kongruas",
"home.column_settings.filter_regex": "Filtri per regulaj esprimoj",
"navigation_bar.keyboard_shortcuts": "Fulmoklavoj",
"notification_purge.btn_all": "Selekti ĉiujn",

View File

@@ -34,13 +34,9 @@
"confirmations.missing_media_description.confirm": "Enviar de todos modos",
"confirmations.missing_media_description.edit": "Editar medios",
"confirmations.missing_media_description.message": "Al menos a un adjunto le falta una descripción. Considera describir toda la multimedia para los débiles visuales antes de mandar el toot.",
"confirmations.unfilter.author": "Publicado por",
"confirmations.unfilter.confirm": "Mostrar",
"confirmations.unfilter.edit_filter": "Editar filtro",
"confirmations.unfilter.filters": "Coincidencia con {count, plural, one {filtro} other {filtros}}",
"direct.group_by_conversations": "Agrupar por conversación",
"endorsed_accounts_editor.endorsed_accounts": "Cuentas destacadas",
"favourite_modal.combo": "Puedes presionar {combo} para omitir esto la próxima vez",
"favourite_modal.favourite": "¿Marcar mensaje como favorito?",
"federation.federated.long": "Permitir que este mensaje llegue a otros servidores",
"federation.federated.short": "Federado",
"federation.local_only.long": "Evitar que este mensaje llegue a otros servidores",
@@ -148,12 +144,17 @@
"settings.wide_view": "Vista amplia (solo modo de escritorio)",
"settings.wide_view_hint": "Expande las columnas para llenar mejor el espacio disponible.",
"status.collapse": "Colapsar",
"status.filtered": "Filtrado",
"status.has_audio": "Contiene archivos de audio",
"status.has_pictures": "Contiene imágenes adjuntas",
"status.has_preview_card": "Contiene una tarjeta de vista previa adjunta",
"status.has_video": "Contiene videos adjuntos",
"status.hide": "Ocultar mensaje",
"status.in_reply_to": "Esta publicación es una respuesta",
"status.is_poll": "Esta publicación es una encuesta",
"status.local_only": "Sólo visible para tu instancia",
"status.show_filter_reason": "Mostrar de todos modos",
"status.show_less": "Mostrar menos",
"status.show_more": "Mostrar más",
"status.uncollapse": "Descolapsar"
}

View File

@@ -31,13 +31,8 @@
"confirmations.missing_media_description.confirm": "Enviar de todos modos",
"confirmations.missing_media_description.edit": "Editar medios",
"confirmations.missing_media_description.message": "Al menos a un adjunto le falta una descripción. Considera describir toda la multimedia para los débiles visuales antes de mandar el toot.",
"confirmations.unfilter.author": "Publicado por",
"confirmations.unfilter.confirm": "Mostrar",
"confirmations.unfilter.edit_filter": "Editar filtro",
"confirmations.unfilter.filters": "Coincidencia con {count, plural, one {filtro} other {filtros}}",
"direct.group_by_conversations": "Agrupar por conversación",
"endorsed_accounts_editor.endorsed_accounts": "Cuentas destacadas",
"favourite_modal.combo": "Puedes presionar {combo} para omitir esto la próxima vez",
"federation.federated.long": "Permitir que esta publicación llegue a otros servidores",
"federation.federated.short": "Federado",
"federation.local_only.long": "Evitar que esta publicación llegue a otros servidores",

View File

@@ -31,13 +31,8 @@
"confirmations.missing_media_description.confirm": "Enviar de todos modos",
"confirmations.missing_media_description.edit": "Editar medios",
"confirmations.missing_media_description.message": "Al menos a un adjunto le falta una descripción. Considera describir toda la multimedia para los débiles visuales antes de mandar el toot.",
"confirmations.unfilter.author": "Autor",
"confirmations.unfilter.confirm": "Mostrar",
"confirmations.unfilter.edit_filter": "Editar filtro",
"confirmations.unfilter.filters": "Coincidiendo {count, plural, one {filtro} other {filtros}}",
"direct.group_by_conversations": "Agrupar por conversación",
"endorsed_accounts_editor.endorsed_accounts": "Cuentas destacadas",
"favourite_modal.combo": "Puedes presionar {combo} para omitir esto la próxima vez",
"federation.federated.long": "Permitir que esta publicación llegue a otros servidores",
"federation.federated.short": "Federado",
"federation.local_only.long": "Evitar que esta publicación llegue a otros servidores",

View File

@@ -7,9 +7,6 @@
"compose.content-type.markdown": "مارک‌دون",
"compose.content-type.plain": "متن ساده",
"confirmations.missing_media_description.edit": "ویرایش رسانه",
"confirmations.unfilter.author": "نویسنده",
"confirmations.unfilter.confirm": "نمایش",
"confirmations.unfilter.edit_filter": "ویرایش پالایه",
"endorsed_accounts_editor.endorsed_accounts": "حساب‌های پیشنهاد شده",
"home.column_settings.advanced": "پیشرفته",
"navigation_bar.app_settings": "تنظیمات کاره",

View File

@@ -31,13 +31,8 @@
"confirmations.missing_media_description.confirm": "Envoyer quand même",
"confirmations.missing_media_description.edit": "Modifier le média",
"confirmations.missing_media_description.message": "Au moins un média joint manque d'une description. Pensez à décrire tous les médias attachés pour les malvoyant·e·s avant de publier votre post.",
"confirmations.unfilter.author": "Auteur",
"confirmations.unfilter.confirm": "Afficher",
"confirmations.unfilter.edit_filter": "Modifier le filtre",
"confirmations.unfilter.filters": "Correspondance avec {count, plural, one {un filtre} other {plusieurs filtres}}",
"direct.group_by_conversations": "Grouper par conversation",
"endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant",
"favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois",
"federation.federated.long": "Permettre à ce post datteindre d'autres serveurs",
"federation.federated.short": "Fédéré",
"federation.local_only.long": "Empêcher ce post datteindre d'autres serveurs",

View File

@@ -31,13 +31,8 @@
"confirmations.missing_media_description.confirm": "Envoyer quand même",
"confirmations.missing_media_description.edit": "Modifier le média",
"confirmations.missing_media_description.message": "Au moins un média joint manque d'une description. Pensez à décrire tous les médias attachés pour les malvoyant·e·s avant de publier votre post.",
"confirmations.unfilter.author": "Auteur",
"confirmations.unfilter.confirm": "Afficher",
"confirmations.unfilter.edit_filter": "Modifier le filtre",
"confirmations.unfilter.filters": "Correspondance avec {count, plural, one {un filtre} other {plusieurs filtres}}",
"direct.group_by_conversations": "Grouper par conversation",
"endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant",
"favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois",
"federation.federated.long": "Permettre à ce post datteindre d'autres serveurs",
"federation.federated.short": "Fédéré",
"federation.local_only.long": "Empêcher ce post datteindre d'autres serveurs",

View File

@@ -27,13 +27,8 @@
"confirmations.missing_media_description.confirm": "Tetap kirim",
"confirmations.missing_media_description.edit": "Sunting media",
"confirmations.missing_media_description.message": "Setidaknya satu lampiran media tidak memiliki deskripsi. Pertimbangkan untuk mendeskripsikan semua lampiran media untuk pengguna tunanetra sebelum mengirim toot Anda.",
"confirmations.unfilter.author": "Penulis",
"confirmations.unfilter.confirm": "Tampilkan",
"confirmations.unfilter.edit_filter": "Ubah saringan",
"confirmations.unfilter.filters": "Mencocokkan {count, plural, other {filter}}",
"direct.group_by_conversations": "Grupkan berdasarkan percakapan",
"endorsed_accounts_editor.endorsed_accounts": "Akun pilihan",
"favourite_modal.combo": "Anda dapat menekan {combo} untuk melewati ini lain kali",
"federation.federated.long": "Izinkan postingan ini menjangkau server lain",
"federation.federated.short": "Difederasi",
"federation.local_only.long": "Cegah postingan ini menjangkau server lain",

View File

@@ -26,13 +26,8 @@
"confirmations.missing_media_description.confirm": "このまま投稿",
"confirmations.missing_media_description.edit": "メディアを編集",
"confirmations.missing_media_description.message": "少なくとも1つの画像に視覚障害者のための画像説明が付与されていません。すべての画像に対して説明を付与することを望みます。",
"confirmations.unfilter.author": "筆者",
"confirmations.unfilter.confirm": "見る",
"confirmations.unfilter.edit_filter": "フィルターを編集",
"confirmations.unfilter.filters": "適用されたフィルター",
"direct.group_by_conversations": "会話でグループ化",
"endorsed_accounts_editor.endorsed_accounts": "紹介しているユーザー",
"favourite_modal.combo": "次からは {combo} を押せば、これをスキップできます。",
"federation.federated.short": "連合",
"federation.local_only.short": "ローカル限定",
"home.column_settings.advanced": "高度",

View File

@@ -34,13 +34,9 @@
"confirmations.missing_media_description.confirm": "그냥 보내기",
"confirmations.missing_media_description.edit": "미디어 편집",
"confirmations.missing_media_description.message": "하나 이상의 미디어에 대해 설명을 작성하지 않았습니다. 시각장애인을 위해 모든 미디어에 설명을 추가하는 것을 고려해주세요.",
"confirmations.unfilter.author": "작성자",
"confirmations.unfilter.confirm": "보기",
"confirmations.unfilter.edit_filter": "필터 편집",
"confirmations.unfilter.filters": "적용된 {count, plural, one {필터} other {필터들}}",
"direct.group_by_conversations": "대화별로 묶기",
"endorsed_accounts_editor.endorsed_accounts": "추천하는 계정들",
"favourite_modal.combo": "다음엔 {combo}를 눌러 건너뛸 수 있습니다",
"favourite_modal.favourite": "관심글로 지정할까요?",
"federation.federated.long": "이 게시물이 다른 서버에 전달되는 것을 허용",
"federation.federated.short": "연합됨",
"federation.local_only.long": "이 게시물이 다른 서버에 전달되는 것을 막기",
@@ -148,12 +144,17 @@
"settings.wide_view": "넓은 뷰 (데스크탑 모드 전용)",
"settings.wide_view_hint": "컬럼들을 늘려서 활용 가능한 공간을 사용합니다.",
"status.collapse": "접기",
"status.filtered": "걸러짐",
"status.has_audio": "소리 파일이 첨부되어 있습니다",
"status.has_pictures": "그림 파일이 첨부되어 있습니다",
"status.has_preview_card": "미리보기 카드가 첨부되어 있습니다",
"status.has_video": "영상이 첨부되어 있습니다",
"status.hide": "게시물 숨기기",
"status.in_reply_to": "이 글은 답글입니다",
"status.is_poll": "이 글은 설문입니다",
"status.local_only": "당신의 서버에서만 보입니다",
"status.show_filter_reason": "그냥 표시하기",
"status.show_less": "접기",
"status.show_more": "더보기",
"status.uncollapse": "펼치기"
}

View File

@@ -18,9 +18,6 @@
"confirmations.missing_media_description.confirm": "Toch verzenden",
"confirmations.missing_media_description.edit": "Media bewerken",
"confirmations.missing_media_description.message": "Minstens één media-bijlage mist een beschrijving. Overweeg om alle mediabijlagen voor slechtzienden te beschrijven voordat u uw toot verstuurt.",
"confirmations.unfilter.author": "Auteur",
"confirmations.unfilter.confirm": "Weergeven",
"confirmations.unfilter.edit_filter": "Filter bewerken",
"direct.group_by_conversations": "Groeperen op gesprek",
"endorsed_accounts_editor.endorsed_accounts": "Aanbevolen accounts",
"home.column_settings.advanced": "Geavanceerd",

View File

@@ -22,12 +22,8 @@
"confirmations.missing_media_description.confirm": "Zignoruj i wyślij",
"confirmations.missing_media_description.edit": "Edytuj załącznik multimedialny",
"confirmations.missing_media_description.message": "Co najmniej jednemu załącznikowi multimedialnemu brakuje opisu. Z uwagi na osoby z zaburzeniami widzenia rozważ opisanie wszystkich załączników przed opublikowaniem wpisu.",
"confirmations.unfilter.author": "Autor",
"confirmations.unfilter.confirm": "Pokaż",
"confirmations.unfilter.edit_filter": "Edytuj filtr",
"direct.group_by_conversations": "Grupuj rozmowami",
"endorsed_accounts_editor.endorsed_accounts": "Wybrane konta",
"favourite_modal.combo": "Możesz nacisnąć {combo}, aby pominąć to następnym razem",
"home.column_settings.advanced": "Zaawansowane",
"home.column_settings.filter_regex": "Filtruj, używając wyrażeń regularnych",
"home.column_settings.show_direct": "Pokaż wiadomości bezpośrednie",

View File

@@ -22,13 +22,8 @@
"confirmations.missing_media_description.confirm": "Enviar mesmo assim",
"confirmations.missing_media_description.edit": "Editar mídia",
"confirmations.missing_media_description.message": "Pelo menos um anexo de mídia não tem uma descrição. Considere descrever todos os anexos de mídia para deficientes visuais antes de enviar seu toot.",
"confirmations.unfilter.author": "Autor",
"confirmations.unfilter.confirm": "Exibir",
"confirmations.unfilter.edit_filter": "Editar filtro",
"confirmations.unfilter.filters": "Correspondência de {count, plural, one {filtro} other {filtros}}",
"direct.group_by_conversations": "Agrupar por conversa",
"endorsed_accounts_editor.endorsed_accounts": "Contas em destaque",
"favourite_modal.combo": "Você pode pressionar {combo} para pular isso da próxima vez",
"home.column_settings.advanced": "Avançado",
"home.column_settings.filter_regex": "Filtrar com uma expressão regular",
"home.column_settings.show_direct": "Mostrar DMs",

View File

@@ -20,13 +20,8 @@
"confirmations.missing_media_description.confirm": "Lägg ut ändå",
"confirmations.missing_media_description.edit": "Redigera media",
"confirmations.missing_media_description.message": "Minst en mediebilaga saknar beskrivning. Överväg att beskriva all media för synskadade innan du skickar din toot.",
"confirmations.unfilter.author": "Användare",
"confirmations.unfilter.confirm": "Visa",
"confirmations.unfilter.edit_filter": "Redigera filter",
"confirmations.unfilter.filters": "Matchande {count, plural, one {filter} other {filters}}",
"direct.group_by_conversations": "Sortera efter konversation",
"endorsed_accounts_editor.endorsed_accounts": "Utvalda konton",
"favourite_modal.combo": "Du kan trycka på {combo} för att skippa detta nästa gång",
"firehose.column_settings.allow_local_only": "Visa endast lokala inlägg i \"Alla\"",
"home.column_settings.advanced": "Avancerat",
"home.column_settings.filter_regex": "Filtrera bort med reguljära uttryck",

View File

@@ -21,13 +21,8 @@
"confirmations.missing_media_description.confirm": "Yine de gönder",
"confirmations.missing_media_description.edit": "Medyayı düzenle",
"confirmations.missing_media_description.message": "En az bir medya eki açıklaması eksik. Gönderinizi göndermeden önce görme engelliler için tüm medya eklerini açıklamayı ön görün.",
"confirmations.unfilter.author": "Yazar",
"confirmations.unfilter.confirm": "Göster",
"confirmations.unfilter.edit_filter": "Filtreyi düzenle",
"confirmations.unfilter.filters": "Eşleşen {count, plural, one {filter} other {filters}}",
"direct.group_by_conversations": "Grup sohbeti",
"endorsed_accounts_editor.endorsed_accounts": "Öne çıkan hesaplar",
"favourite_modal.combo": "Bir sonraki sefer {combo} tuşuna basabilirsiniz",
"settings.always_show_spoilers_field": "Her zaman İçerik Uyarısı alanını etkinleştir",
"settings.auto_collapse": "Otomatik küçülme",
"settings.auto_collapse_all": "Her şey",

View File

@@ -22,13 +22,8 @@
"confirmations.missing_media_description.confirm": "Все одно надіслати",
"confirmations.missing_media_description.edit": "Редагувати медіа",
"confirmations.missing_media_description.message": "Принаймні одна медіа-прикріплення не має опису. Подумайте про описання всіх медіавкладень для людей з порушеннями зору перед відправкою дмуху.",
"confirmations.unfilter.author": "Автор",
"confirmations.unfilter.confirm": "Показати",
"confirmations.unfilter.edit_filter": "Редагувати фільтр",
"confirmations.unfilter.filters": "Відповідність {count, plural, one {filter} other {filters}}",
"direct.group_by_conversations": "Групування за розмовами",
"endorsed_accounts_editor.endorsed_accounts": "Рекомендовані облікові записи",
"favourite_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
"firehose.column_settings.allow_local_only": "Відображати локальні повідомлення в \"Все\"",
"home.column_settings.advanced": "Додатково",
"home.column_settings.filter_regex": "Відфільтрувати за допомогою регулярних виразів",

View File

@@ -34,13 +34,8 @@
"confirmations.missing_media_description.confirm": "仍然发送",
"confirmations.missing_media_description.edit": "编辑媒体",
"confirmations.missing_media_description.message": "你没有为一个或多个媒体撰写描述。请考虑为视障人士添加描述。",
"confirmations.unfilter.author": "作者",
"confirmations.unfilter.confirm": "显示",
"confirmations.unfilter.edit_filter": "编辑筛选器",
"confirmations.unfilter.filters": "应用{count, plural, other {筛选器}}",
"direct.group_by_conversations": "按对话分组",
"endorsed_accounts_editor.endorsed_accounts": "精选账户",
"favourite_modal.combo": "下次你可以按 {combo} 跳过这个",
"federation.federated.long": "允许此嘟文到达其它服务器",
"federation.federated.short": "联动",
"federation.local_only.long": "阻止此嘟文到达其它服务器",
@@ -148,12 +143,17 @@
"settings.wide_view": "宽视图(仅限于桌面模式)",
"settings.wide_view_hint": "拉伸列宽以更好地填充可用空间。",
"status.collapse": "折叠",
"status.filtered": "已过滤",
"status.has_audio": "附带音频",
"status.has_pictures": "附带图片",
"status.has_preview_card": "附带预览卡片",
"status.has_video": "附带视频",
"status.hide": "隐藏嘟文",
"status.in_reply_to": "此嘟文是回复",
"status.is_poll": "此嘟文是投票",
"status.local_only": "此嘟文仅本站可见",
"status.show_filter_reason": "仍然显示",
"status.show_less": "部分显示",
"status.show_more": "完全显示",
"status.uncollapse": "展开"
}

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