Compare commits

...

330 Commits

Author SHA1 Message Date
Claire
b554ecfcb4 Merge pull request #3292 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 01cf5c103d into stable-4.4
2025-11-20 15:25:12 +01:00
Claire
50244ba682 Merge commit '01cf5c103de6c46b649d4db38b517b51f8bdfb56' into glitch-soc/merge-4.4 2025-11-20 14:58:00 +01:00
Claire
01cf5c103d Bump version to v4.4.9 (#36946) 2025-11-20 14:41:12 +01:00
Claire
5bda54d15a Merge pull request #3290 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to c49e261ad0 into stable-4.4
2025-11-19 22:59:55 +01:00
Claire
07f5573cd6 [Glitch] Fix filters not being applied to quotes in detailed view
Port 16ee628d24 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-19 22:30:31 +01:00
Claire
2b0b537152 Merge commit 'c49e261ad07d46515af50cdf103524ecf554b2f8' into glitch-soc/merge-4.4 2025-11-19 22:29:14 +01:00
Claire
c49e261ad0 Update dependency glob (#36942) 2025-11-19 16:29:45 +01:00
Shugo Maeda
915bcb267f Fix ArgumentError of tootctl upgrade storage-schema (#36914) 2025-11-19 13:55:30 +01:00
Claire
ff37011057 update dependency js-yaml to v4.1.1 2025-11-19 13:54:58 +01:00
Claire
8f5e95a159 Fix Update importing old previously-unknown activities and treating them as recent ones (#36848) 2025-11-19 13:50:16 +01:00
Claire
16ee628d24 Fix filters not being applied to quotes in detailed view (#36843) 2025-11-19 13:47:19 +01:00
Claire
64a0b060a8 Update security policy for 4.3 (#36755) 2025-11-06 14:58:24 +01:00
Claire
fa52f4361a Merge pull request #3248 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to c2fb12d22d to stable-4.4
2025-10-21 15:33:28 +02:00
Claire
7f7d6697c1 Merge commit 'c2fb12d22d52872b5905822c3d05f65516fa7f1d' into glitch-soc/merge-4.4 2025-10-21 15:14:15 +02:00
Claire
c2fb12d22d Bump version to v4.4.8 (#36542) 2025-10-21 15:12:37 +02:00
Claire
2dc4552229 Merge commit from fork
* Add validation to reject quotes of reblogs

* Do not process quotes of reblogs as potentially valid quotes

* Refuse to serve quoted reblogs over REST API
2025-10-21 15:00:28 +02:00
Claire
95868643a2 Merge pull request #3237 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 8965e1bfa9 into stable-4.4
2025-10-15 11:32:36 +02:00
Claire
7c46fdfbf1 Merge commit '8965e1bfa9ce958ab16083a555ec6677b5f0f690' into glitch-soc/merge-4.4 2025-10-15 11:16:53 +02:00
Claire
8965e1bfa9 Bump version to v4.4.7 (#36473) 2025-10-15 10:12:23 +02:00
Claire
1e27ab0885 Fix moderation warning e-mails that include posts (#36462) 2025-10-14 17:15:58 +02:00
Jonathan de Jong
cef2c50a71 Fix allow_referrer_origin typo (#36460) 2025-10-14 17:15:58 +02:00
Claire
a54af6b06c Merge pull request #3232 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to d7f4eca801 into stable-4.4
2025-10-13 16:13:31 +02:00
Claire
81cf6715de Merge commit 'd7f4eca801bb702f487287eb218a9e7a133ee341' into glitch-soc/merge-4.4 2025-10-13 15:56:31 +02:00
Claire
d7f4eca801 Fix streaming still being authorized for suspended accounts (#36449) 2025-10-13 15:35:58 +02:00
Claire
adf291631e Bump version to v4.4.6 (#36444) 2025-10-13 14:43:01 +02:00
Emelia Smith
cbef4c9e65 Merge commit from fork 2025-10-13 14:20:57 +02:00
Claire
1631fb80e8 Merge commit from fork
* Ensure tootctl revokes sessions, access tokens and web push subscriptions

* Fix test coverage

---------

Co-authored-by: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
2025-10-13 14:20:23 +02:00
Claire
8477bec2f2 Merge commit from fork
* Streaming: Ensure disabled users cannot connect to streaming

* Streaming: Disconnect when the user is disabled

---------

Co-authored-by: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
2025-10-13 14:19:14 +02:00
Claire
6796765363 Update dependency openssl 2025-10-13 11:03:33 +02:00
Claire
044a20f12d Update dependency rack 2025-10-13 11:03:33 +02:00
Claire
15d05121df Merge pull request #3228 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to e4bdbccba8 into stable-4.4
2025-10-10 20:27:24 +02:00
Claire
07dea4e140 Merge commit 'e4bdbccba802318ebde6ab17f5adc1959bd82374' into glitch-soc/merge-4.4 2025-10-10 19:19:16 +02:00
github-actions[bot]
3c2570c88a New Crowdin Translations for stable-4.4 (automated) (#3226)
* New Crowdin translations

* Fix bogus no.yml

* Fix bogus simple_form.no.yml

---------

Co-authored-by: GitHub Actions <noreply@github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-10-10 19:08:08 +02:00
Claire
81955c10b1 Fix crowdin download script for glitch-soc stable branches 2025-10-10 18:45:27 +02:00
github-actions[bot]
e4bdbccba8 New Crowdin Translations for stable-4.4 (automated) (#36431)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-10 18:19:25 +02:00
Claire
958d4df6cf Merge pull request #3222 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to c858fc77ef to stable-4.4
2025-10-10 10:29:48 +02:00
Claire
21d4abf7cc Merge commit 'd7d6407d4196ab6783485b20a3d9e2fbfc00ef01' into glitch-soc/merge-4.4 2025-10-09 18:15:33 +02:00
Claire
d7d6407d41 Explicitly record Tombstone quotes as deleted
This adds a `deleted` state to the internal representation, but this does
not change the API, which already included such a state.
2025-10-09 17:37:23 +02:00
Claire
a186bad399 Fix "quote": { "type": "Tombstone" } being ignored 2025-10-09 17:37:23 +02:00
Claire
67575e59e6 Fix quote post state sometimes not being updated through streaming server (#36408) 2025-10-09 17:37:23 +02:00
Matt Jankowski
d9113976c8 Use tag filter for pending tag count on admin dashboard (#36404) 2025-10-09 17:37:23 +02:00
Claire
3386f225e4 Merge pull request #3218 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 670316499f into stable-4.4
2025-10-09 14:08:25 +02:00
diondiondion
97bc82c710 [Glitch] Allow quotes to be displayed in the featured carousel
Port 0d7af7e1fe to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-08 20:24:22 +02:00
Claire
0e7cb713d1 Merge commit '670316499fb4439a28d93aca2f40617dfb9bb2b0' into glitch-soc/merge-4.4 2025-10-08 18:00:57 +02:00
Claire
670316499f Update dependency uri 2025-10-08 16:26:13 +02:00
Claire
3c725240fd Update dependency rack 2025-10-08 16:26:13 +02:00
Claire
d8ddf95485 Fix JSON payload being potentially mutated when processing interaction policies (#36392) 2025-10-08 16:26:13 +02:00
Emelia Smith
4c12c2ed60 Add integration tests for mastodon-streaming (#36025)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Co-authored-by: David Roetzel <david@roetzel.de>
2025-10-08 16:26:13 +02:00
diondiondion
636ecd1d03 Display quotes in email notifications (#36379) 2025-10-08 16:26:13 +02:00
Claire
cb0065cfe9 Fix redirect to external object when URL is missing or malformed (#36347) 2025-10-08 16:26:13 +02:00
diondiondion
6ae1b4fae9 Allow quotes to be displayed in the featured carousel (#36335) 2025-10-08 16:26:13 +02:00
Eugen Rochko
fd7adcc9d4 [Glitch] Fix wrong styles on rules and buttons in the sign-up form
Port 2df86d6413 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-06 20:12:00 +02:00
Claire
3c5f07c28e Merge pull request #3195 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to dc6d8f8825 in stable-4.4
2025-09-23 18:51:40 +02:00
Claire
048a42b8a7 [Glitch] Add click-through for quoted limited accounts
Port c8551a3eca to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-23 18:17:30 +02:00
Claire
c475623418 Merge commit 'dc6d8f882528803c06b2a71c23d6c1467880a7e1' into glitch-soc/merge-4.4 2025-09-23 18:10:55 +02:00
Claire
dc6d8f8825 Bump version to v4.4.5 2025-09-23 14:33:16 +02:00
Claire
dd0647ca45 Update dependency rexml 2025-09-23 14:33:16 +02:00
Claire
70e2eb49df Add support for has:quote in search (#36217) 2025-09-23 14:33:16 +02:00
Claire
bef28b2e51 Fix processing of out-of-order Update as implicit updates (#36190) 2025-09-22 16:54:04 +02:00
Claire
0b66bd591f Fix getting Create and Update out of order (#36176) 2025-09-22 16:54:04 +02:00
Claire
a94d7bf520 Change quoted posts from silenced accounts not to be hidden (#36166) 2025-09-22 16:54:04 +02:00
Claire
c8551a3eca Add click-through for quoted limited accounts (#36167) 2025-09-22 16:54:04 +02:00
Claire
df322e50c0 Merge pull request #3186 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 06c2393805 into stable-4.4
2025-09-17 19:12:14 +02:00
Claire
1f3588a5a7 Merge commit '06c2393805eee6b2e4b4aa17787129a2d542b71d' into glitch-soc/merge-4.4 2025-09-17 18:09:26 +02:00
Claire
06c2393805 Fix quote with CW but no text being shown without CW (#36150) 2025-09-17 18:01:19 +02:00
Claire
4e85b9073b Fix typo in changelog (#36140) 2025-09-16 17:39:45 +02:00
Claire
cd573a346d Merge pull request #3182 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to c966d75600 in stable-4.4
2025-09-16 15:13:03 +02:00
Claire
793296b5eb [Glitch] Fix unresponsive areas around GIFV modals in some cases
Port 2347354bba to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-16 14:14:10 +02:00
Claire
661dbede1c [Glitch] Move quote post fallback removal import-time
Port 1c8990927a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-16 14:14:10 +02:00
Claire
89f423aa00 [Glitch] Fix missing beforeUnload confirmation when a poll is being authored
Port 0d93801bde to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-16 14:14:10 +02:00
Claire
19c89f1bbd Merge commit 'c966d756007e0e4c4eb020055c999e4cf7a07ed0' into glitch-soc/merge-4.4 2025-09-16 14:14:05 +02:00
Claire
c966d75600 Bump version to v4.4.4 2025-09-16 13:54:27 +02:00
Claire
6f1fd0c2a7 Update axios dependency 2025-09-16 13:54:27 +02:00
Claire
68c219e753 Update vite dependency 2025-09-16 13:54:27 +02:00
Claire
a795743c3f Update rails dependencies 2025-09-16 13:54:27 +02:00
github-actions[bot]
1ab5ea9bfb New Crowdin Translations for stable-4.4 (automated) (#36122)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-09-15 14:31:53 +02:00
Claire
48f55e3224 Fix quote posts with CW and no text being rejected
Fixes #36077
2025-09-12 16:27:29 +02:00
Claire
6044270d69 Fix missing memoization in Web::PushNotificationWorker (#36085) 2025-09-12 16:27:29 +02:00
Claire
be1bd91e6d Fix unresponsive areas around GIFV modals in some cases (#36059) 2025-09-12 16:27:29 +02:00
Claire
34cd5a716f Move quote post fallback removal import-time (#36055) 2025-09-12 16:27:29 +02:00
Claire
ec5128bc1f Fix missing beforeUnload confirmation when a poll is being authored (#36030) 2025-09-12 16:27:29 +02:00
Claire
28a79eb239 Merge pull request #3174 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to c0f9e7f4c3 into stable-4.4
2025-09-05 19:24:43 +02:00
Emelia Smith
1e2a9167bc [Glitch] Support displaying polls in Admin UI
Port 1137a0ca3a to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-04 19:06:10 +02:00
diondiondion
1947f3a18b [Glitch] Fix error alerts for deleted quotes
Port 1faf520ce4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-04 19:06:10 +02:00
Claire
5a46e3a234 [Glitch] Fix API return types for interaction API helpers
Port 8777443c9b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-04 19:06:10 +02:00
Claire
75ba0f757a [Glitch] Fix WebUI fetching deleted quote in an endless loop
Port bd6d1f0e3f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-04 19:06:10 +02:00
Claire
62ed8d633e [Glitch] Fix Edit as well as “Delete & Redraft” on a poll not inserting empty option
Port a48567784c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-04 19:06:10 +02:00
Echo
02d92d3b68 [Glitch] Redirect on success for standalone compose
Port 1675eab561 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-04 19:06:10 +02:00
Emelia Smith
2a87fe5860 [Glitch] Refactor to reuse the one status partial across moderation tools
Port cbb9a4dbe3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-09-04 18:38:00 +02:00
Claire
00c61317d3 Merge remote-tracking branch 'upstream/stable-4.4' into HEAD
Conflicts:
- `app/views/layouts/application.html.haml`:
  Conflict because of glitch-soc's theming system.
  Updated the line as upstream did.
2025-09-04 18:33:21 +02:00
fiona
c0f9e7f4c3 Fix handling of edited status with new media and no text (#35970) 2025-09-04 10:45:54 +02:00
Emelia Smith
1137a0ca3a Support displaying polls in Admin UI (#35933)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-09-04 10:45:54 +02:00
diondiondion
1faf520ce4 Fix error alerts for deleted quotes (#35918) 2025-09-04 10:45:54 +02:00
Claire
8777443c9b Fix API return types for interaction API helpers (#35915) 2025-09-04 10:45:54 +02:00
Claire
bd6d1f0e3f Fix WebUI fetching deleted quote in an endless loop (#35909) 2025-09-04 10:45:54 +02:00
Claire
1a1a23f6f0 Consolidate labels for quote policy settings (#35893) 2025-09-04 10:45:54 +02:00
Claire
a48567784c Fix Edit as well as “Delete & Redraft” on a poll not inserting empty option (#35892) 2025-09-04 10:45:54 +02:00
Shlee
b71216a08a Add crossorigin back to inert css (regression? of #30687) (#35876) 2025-09-04 10:45:54 +02:00
Matt Jankowski
36974aaa99 Use debug? query method on httplog initializer check (#35833) 2025-09-04 10:45:54 +02:00
Claire
567f337db3 Fix self-destruct scheduler behavior on some Redis setups (#35823) 2025-09-04 10:45:54 +02:00
Matt Jankowski
97f118013a Include update in the resources args for api/web/push_subscriptions route (#35801) 2025-09-04 10:45:54 +02:00
Claire
ea5d1f0297 Fix tootctl admin create not bypassing reserved username checks (#35779) 2025-09-04 10:45:54 +02:00
Matt Jankowski
7a862d3308 First pass coverage addition for antispam class (#35771) 2025-09-04 10:45:54 +02:00
Echo
1675eab561 Redirect on success for standalone compose (#35763) 2025-09-04 10:45:54 +02:00
Claire
5f4116a311 Fix interaction policy changes in implicit updates not being saved (#35751) 2025-09-04 10:45:54 +02:00
Claire
0741381670 Add test for Delete of inlined QuoteAuthorization (#35724) 2025-09-04 10:45:54 +02:00
Claire
e61900cadc Fix quote revocation not being streamed (#35710) 2025-09-04 10:45:54 +02:00
Emelia Smith
cbb9a4dbe3 Refactor to reuse the one status partial across moderation tools (#35644) 2025-09-04 10:45:54 +02:00
Claire
4ef0ce033e Fix export of large user archives on 4.4 by enabling Zip64 (#35850) 2025-08-23 01:59:11 +02:00
Claire
48315a719d Change glitch flavor to show approximate reply count by default (#3157) 2025-08-22 22:31:39 +02:00
Claire
45932983fc Merge pull request #3152 from ClearlyClaire/glitch-soc/backports-4.4
Merge upstream changes up to v4.4.3
2025-08-05 15:42:23 +02:00
Claire
1ed3e4cc77 Merge commit '5478ef9b32be0c7ac8f86330760485a7531d218a' into glitch-soc/backports-4.4 2025-08-05 15:24:44 +02:00
Claire
5478ef9b32 Bump version to v4.4.3 (#35686) 2025-08-05 15:20:40 +02:00
Claire
e2592419d9 Merge commit from fork 2025-08-05 14:53:04 +02:00
github-actions[bot]
e330447b0e New Crowdin Translations for stable-4.4 (automated) (#35681)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-08-05 14:02:35 +02:00
Claire
b15861528c Update dependency ruby-saml to v1.18.1 2025-08-05 11:43:15 +02:00
Claire
83dc7dc16e Disable ActiveRecord query cache in Create critical path (#35662) 2025-08-05 11:43:15 +02:00
Claire
7d3cc51148 Avoid nested transactions when fetching quote posts (#35657) 2025-08-05 11:43:15 +02:00
Claire
cabb33bc49 Fix WebUI crashing for accounts with null URL (#35651) 2025-08-05 11:43:15 +02:00
Claire
ba6f4de3e4 Merge pull request #3150 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 208cb8276a into stable-4.4
2025-08-04 22:42:04 +02:00
Claire
6af9646bbd [Glitch] Fix “Expand this post” link including user @undefined
Port 5429351889 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-08-04 21:50:10 +02:00
Claire
fcb7917344 Merge commit '208cb8276ae618aa36aec5fad0dc31341402c133' into glitch-soc/merge-4.4
Conflicts:
- `app/models/trends/statuses.rb`:
  Conflict because of glitch-soc's setting to allow CWs in trends.
  Kept glitch-soc's setting but followed upstream's refactor.
2025-08-04 21:48:37 +02:00
Claire
208cb8276a Fix friends-of-friends recommendations suggesting already-requested accounts (#35604) 2025-08-01 11:34:27 +02:00
Claire
4ae47f4263 Change StatusReachFinder to consider quotes as well as reblogs (#35601) 2025-08-01 11:34:27 +02:00
Claire
08b2f255fc Fix synchronous recursive fetching of deeply-nested quoted posts (#35600) 2025-08-01 11:34:27 +02:00
Claire
8242f06eca Add restrictions on which quote posts can trend (#35507) 2025-08-01 11:34:27 +02:00
Claire
5429351889 Fix “Expand this post” link including user @undefined (#35478) 2025-08-01 11:34:27 +02:00
Claire
6ff4e83937 Change quote verification to not bypass authorization flow for mentions (#35528) 2025-08-01 11:34:27 +02:00
Claire
5e639d7384 [Glitch] Fix replying from media modal or pop-in-player tagging user @undefined
Port e7c5c25dce81bd4e32a4bf599c759303420c47e3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-25 23:43:01 +02:00
Claire
26e524836d Merge pull request #3140 from ClearlyClaire/glitch-soc/backports-4.4
Merge upstream changes up to v4.4.2 into stable-4.4
2025-07-23 18:39:12 +02:00
Claire
cfc4bb1dc0 Fix links in posts always having noreferrer in glitch flavor (#3135)
Fixes #3128
2025-07-23 18:20:21 +02:00
diondiondion
d7099b1b38 [Glitch] refactor: Disable useDrag hook when main menu is not openable
Port a842b14c84 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:20:08 +02:00
diondiondion
69fb382424 [Glitch] fix: Add lang attribute to current composer language in alt text modal
Port 138746bdcc to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:19:55 +02:00
diondiondion
47d469ec5e [Glitch] fix: Fix quote posts styling on notifications page
Port 3771f9e04b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:19:42 +02:00
diondiondion
4d3e2efb69 [Glitch] fix: Improve Dropdown component accessibility
Port 82a6ff091f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:19:08 +02:00
diondiondion
92fc7a30dc [Glitch] fix: Fix selected item in poll select menus is unreadable in Firefox
Port 558b9c90a6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:16:49 +02:00
diondiondion
4311369ab8 [Glitch] refactor: Only remove pointer-events when necessary
Port 74fc4dbacf to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:16:34 +02:00
diondiondion
18653ce15d [Glitch] fix: Improve a11y of custom select menus in notifications settings
Port faffb73cbd to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:16:13 +02:00
Echo
71a35d3953 [Glitch] Make bio hashtags open the local page instead of the remote instance
Port 853a0c466e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 18:15:44 +02:00
Claire
e69c1479a8 Merge commit '77d2cdb30230ae6292bd247f6c6f97d00bd38084' into glitch-soc/backports-4.4 2025-07-23 18:10:05 +02:00
github-actions[bot]
77d2cdb302 New Crowdin Translations for stable-4.4 (automated) (#35477)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-23 16:28:50 +02:00
David Roetzel
c727197760 Combine two items 2025-07-23 16:08:43 +02:00
David Roetzel
d6859c9658 Update CHANGELOG.md
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 16:08:43 +02:00
David Roetzel
7a9e98f4d6 Update CHANGELOG.md
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 16:08:43 +02:00
David Roetzel
7924a27ae7 Update CHANGELOG.md
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-07-23 16:08:43 +02:00
David Roetzel
d664b9d8ff Update "Security" section...
...to account for multiple updates that have been added since.
2025-07-23 16:08:43 +02:00
David Roetzel
4558cfadd8 Update dependency thor 2025-07-23 16:08:43 +02:00
David Roetzel
713965467d Update dependency axios 2025-07-23 16:08:43 +02:00
David Roetzel
aec6d0f807 Bump version to v4.4.2 2025-07-23 16:08:43 +02:00
diondiondion
e103815d2d Don't require JSDoc params & return in TS (#35426) 2025-07-23 16:08:43 +02:00
renovate[bot]
d73b9fba90 chore(deps): update dependency nokogiri to v1.18.9 (#35433)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-23 16:08:43 +02:00
diondiondion
a89d11bc08 refactor: Disable useDrag hook when main menu is not openable (#35414) 2025-07-23 16:08:43 +02:00
diondiondion
a250928934 fix: Add lang attribute to current composer language in alt text modal (#35412) 2025-07-23 16:08:43 +02:00
diondiondion
1d1b17b04b fix: Fix quote posts styling on notifications page (#35411) 2025-07-23 16:08:43 +02:00
diondiondion
2aff51013c fix: Improve a11y of custom select menus in notifications settings (#35403) 2025-07-23 16:08:43 +02:00
diondiondion
8c3c1faaec fix: Fix selected item in poll select menus is unreadable in Firefox (#35402) 2025-07-23 16:08:43 +02:00
diondiondion
a2888f1bb2 refactor: Only remove pointer-events when necessary (#35390) 2025-07-23 16:08:43 +02:00
diondiondion
77fe044f03 Update age limit wording (#35387) 2025-07-23 16:08:43 +02:00
Claire
da0cc0f5b9 Fix support for quote verification in implicit status updates (#35384) 2025-07-23 16:08:43 +02:00
Claire
ee83f3a8b9 Always give local quote of remote posts a quote request URI (#35383) 2025-07-23 16:08:43 +02:00
Claire
7ae78b1032 Refactor ActivityPub::Activity::Accept and ActivityPub::Activity::Reject specs (#35382) 2025-07-23 16:08:43 +02:00
Claire
c4b7c3bdda Fix quoteAuthorization type in JSON-LD context (#35380) 2025-07-23 16:08:43 +02:00
diondiondion
a79dbf8334 fix: Improve Dropdown component accessibility (#35373) 2025-07-23 16:08:43 +02:00
Claire
ef6f5f9357 Fix quote attributes missing from Mastodon's context (#35354) 2025-07-23 16:08:43 +02:00
Echo
f65f6ad6f1 Make bio hashtags open the local page instead of the remote instance (#35349) 2025-07-23 16:08:43 +02:00
Claire
c0e242cb73 Fix styling of external log-in button (#35320) 2025-07-23 16:08:43 +02:00
Claire
6c1cc5b25a Merge pull request #3126 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 609a40181e
2025-07-09 17:48:59 +02:00
Claire
ec6f93ae45 Merge commit '609a40181e0f3f505707d196985dfb78ab3b3f88' into glitch-soc/merge-4.4 2025-07-09 17:31:28 +02:00
Claire
609a40181e Bump version to v4.4.1 2025-07-09 17:16:57 +02:00
Claire
93ce44d21d Fix nearly every sub-directory being crawled as part of Vite build (#35323) 2025-07-09 17:16:57 +02:00
David Roetzel
fb3ff194b5 Relax error restriction in initializer (#35321) 2025-07-09 17:16:57 +02:00
Claire
81b363b338 Fix replying from media modal or pop-in-player tagging user @undefined (#35317) 2025-07-09 17:16:57 +02:00
Claire
1151b05c2d Fix support for special characters in various environment variables (#35314)
Co-authored-by: Matt Jankowski <matt@jankowski.online>
2025-07-09 17:16:57 +02:00
Matt Jankowski
f96743fcfb Use if_exists: true when removing duplicate indexes (#35309) 2025-07-09 17:16:57 +02:00
Claire
7a51ad7ebd Merge pull request #3121 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 69e14246b8
2025-07-08 16:30:51 +02:00
Claire
06a46e77b8 Merge commit '69e14246b838443985a541e97327494b8d2fdffb' into glitch-soc/merge-4.4 2025-07-08 16:15:29 +02:00
Claire
69e14246b8 Fix 4.4 container images not being marked as latest (#35294) 2025-07-08 16:07:41 +02:00
Claire
174370dec2 Merge pull request #3120 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to c1794fb948
2025-07-08 16:06:31 +02:00
Claire
18e08bf493 Merge commit 'c1794fb948dfb13865214bce8a90e88da44d4ff6' into glitch-soc/merge-4.4 2025-07-08 15:51:09 +02:00
Claire
c1794fb948 Bump version to v4.4.0 (#35268) 2025-07-08 15:25:26 +02:00
Claire
061c966ab3 Merge pull request #3119 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 333a17a478
2025-07-08 14:02:48 +02:00
diondiondion
326f6bc12a [Glitch] fix: Fix can't skip search field by tabbing
Port 2dcededcf0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-08 13:14:27 +02:00
Claire
bd442485d0 Merge commit '333a17a478f0ddcee4115a50f01077cb1dc5c22e' into glitch-soc/merge-4.4 2025-07-08 13:13:07 +02:00
David Roetzel
333a17a478 Better error response to malformed headers (#35278) 2025-07-08 11:45:24 +02:00
github-actions[bot]
388e09e1a3 New Crowdin Translations for stable-4.4 (automated) (#35288)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-08 11:22:46 +02:00
diondiondion
2dcededcf0 fix: Fix can't skip search field by tabbing (#35281) 2025-07-07 17:48:13 +02:00
github-actions[bot]
2db8a328cd New Crowdin Translations (automated) (#35269)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-07 17:48:13 +02:00
Matt Jankowski
b4a950c2fc Remove unused scopes in Account model (#35276) 2025-07-07 17:48:13 +02:00
Claire
194645aada Add ability to manually trigger i18n uploads (#35279) 2025-07-07 15:40:54 +02:00
Claire
48aaecec7b Merge pull request #3118 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 0c5ce23ae4
2025-07-04 18:27:47 +02:00
diondiondion
6cac651ff2 [Glitch] fix: Remove focus highlight when status is clicked in light mode
Port 921af5d27d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-04 17:58:26 +02:00
Claire
385dd5ea37 Merge commit '0c5ce23ae496af26b96aaab742800af93f552f44' into glitch-soc/merge-4.4 2025-07-04 17:56:10 +02:00
Claire
0c5ce23ae4 Fix incorrect name in scheduler configuration (#35263) 2025-07-04 15:10:17 +02:00
github-actions[bot]
cb937a920e New Crowdin Translations (automated) (#35261)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-04 15:10:17 +02:00
David Roetzel
7051458467 Raise better exception on FASP error responses (#35262) 2025-07-04 15:10:17 +02:00
Matt Jankowski
025abf7325 Fix intermittent failure of TOS model spec from effective date collision (#35244) 2025-07-04 15:10:17 +02:00
Matt Jankowski
28373a9c88 Use ActiveModel::Attributes in admin/status_batch_action (#35255) 2025-07-04 15:10:17 +02:00
Claire
42884d8727 Fix error handling for blank actions in account moderation action form (#35246) 2025-07-04 15:10:17 +02:00
github-actions[bot]
000ff9c05f New Crowdin Translations (automated) (#35250)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-04 15:10:17 +02:00
diondiondion
921af5d27d fix: Remove focus highlight when status is clicked in light mode (#35251) 2025-07-04 15:10:17 +02:00
Matt Jankowski
878e1e65eb Use ActiveModel::Attributes for admin/account_action boolean values (#35247) 2025-07-04 15:10:17 +02:00
Matt Jankowski
06f5f270cc Use Account#targeted_reports association where needed (#35249) 2025-07-04 15:10:17 +02:00
Matt Jankowski
961c22a6fd Add coverage for TOS interstitial interruption flow of web app controller concern (#35235) 2025-07-04 15:10:17 +02:00
github-actions[bot]
07b4fa55c8 New Crowdin Translations (automated) (#35238)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-04 15:10:17 +02:00
Matt Jankowski
041bce9ed6 Add coverage for valid_locale_or_nil languages helper method (#34866) 2025-07-04 15:10:17 +02:00
Claire
d7a08d81b6 Fix error on log-in from old users requiring ToS interstitial when said ToS has been removed (#35233) 2025-07-04 15:10:17 +02:00
Claire
8bb81f9496 Merge pull request #3115 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to a203a05eb1
2025-07-01 20:51:10 +02:00
diondiondion
605df74f06 [Glitch] fix: Fix column header overlapping mobile menu on old Safari
Port e6e8974785 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-07-01 18:48:16 +02:00
Claire
98e90b7c1f Merge commit 'a203a05eb10db82e1db2d75398e0261cfe4d33e4' into glitch-soc/merge-upstream 2025-07-01 18:47:15 +02:00
Claire
a203a05eb1 Fix missing newline in changelog (#35227) 2025-07-01 12:31:55 +00:00
Claire
68090cd8be Bump version to v4.4.0-rc.1 (#35196) 2025-07-01 09:21:32 +00:00
github-actions[bot]
dd064aaa36 New Crowdin Translations (automated) (#35224)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-01 09:02:26 +00:00
diondiondion
e6e8974785 fix: Fix column header overlapping mobile menu on old Safari (#35225) 2025-07-01 08:53:43 +00:00
Claire
5e95c63b5b Merge pull request #3114 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to c357a7f8d6
2025-07-01 10:45:20 +02:00
Renaud Chaput
498af63b85 chore: validate the project funding.json association (#35221) 2025-06-30 16:21:52 +00:00
diondiondion
0bb2dc9d26 [Glitch] fix: Fix popover/dialog backgrounds not blurred on older Webkit browsers
Port e8a603b18f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-30 17:56:29 +02:00
Claire
2eec4da8fc Merge commit 'c357a7f8d697ede4df4be74456b0497118c9d049' into glitch-soc/merge-upstream 2025-06-30 17:52:22 +02:00
David Roetzel
c357a7f8d6 Add optional bulk mailer settings (#35203) 2025-06-30 14:49:14 +00:00
David Roetzel
bae258925c Persist follow recommendations from FASP (#35218) 2025-06-30 13:39:36 +00:00
diondiondion
e8a603b18f fix: Fix popover/dialog backgrounds not blurred on older Webkit browsers (#35220) 2025-06-30 12:16:54 +00:00
renovate[bot]
f00c8e3245 chore(deps): update dependency haml_lint to v0.64.0 (#35215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 11:25:47 +00:00
Claire
153af19f55 Add specs for PublicFileServer middleware (#35219) 2025-06-30 11:23:11 +00:00
Matt Jankowski
964916c71b Add coverage for TermsOfService scopes/validations (#35204) 2025-06-30 10:28:14 +00:00
github-actions[bot]
8782e860b6 New Crowdin Translations (automated) (#35208)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-30 09:33:15 +00:00
renovate[bot]
641c0c6393 fix(deps): update dependency pg to v8.16.3 (#35213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 09:33:09 +00:00
renovate[bot]
0383100b0e fix(deps): update dependency ws to v8.18.3 (#35214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 09:33:03 +00:00
Jeong Arm
87db28cebc Fix unexpected "cache-control: no-cache" header in public file server (#35209) 2025-06-30 09:06:18 +00:00
David Roetzel
ac4b735c67 Add FASP account search support (#34033) 2025-06-30 07:42:34 +00:00
Claire
bc3dacc371 Merge pull request #3113 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 0d650780e2
2025-06-27 18:10:54 +02:00
github-actions[bot]
6d017dbf10 New Crowdin Translations (automated) (#35202)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-27 08:37:39 +00:00
diondiondion
f21c92bb45 [Glitch] fix: Fix outdated icon in notifications permissions banner
Port 9576434d47 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-26 18:07:16 +02:00
diondiondion
7b98298f85 [Glitch] refactor: Tweak wording of "discard draft?" confirmation dialogs
Port b804ed0cba to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-26 18:06:55 +02:00
diondiondion
1f8378c12d [Glitch] fix: Prevent scrolling behind menus and modals in Safari iOS
Port c1ef1f62d5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-26 18:06:16 +02:00
Claire
f7b4580b49 Merge commit '0d650780e26735621f08bbdd545b162871e4562c' into glitch-soc/merge-upstream
Conflicts:
- `.prettierignore`:
  Upstream added a file, glitch-soc had extra files.
  Took upstream's changes and moved glitch-soc's additions at the end.
2025-06-26 18:04:37 +02:00
renovate[bot]
0d650780e2 fix(deps): update dependency postcss-preset-env to v10.2.4 (#35194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 14:17:31 +00:00
Eugen Rochko
1804a87193 Change terms of service generator to not be displayed (#35127) 2025-06-26 13:26:41 +00:00
diondiondion
9576434d47 fix: Fix outdated icon in notifications permissions banner (#35193) 2025-06-26 13:25:12 +00:00
diondiondion
b804ed0cba refactor: Tweak wording of "discard draft?" confirmation dialogs (#35192) 2025-06-26 13:03:24 +00:00
David Roetzel
48451b782d Move email env var reading to yml files (#35191) 2025-06-26 12:18:30 +00:00
Claire
2e0a00ab46 Fix search operators sometimes getting lost (#35190) 2025-06-26 10:35:49 +00:00
Claire
e4618a6ba5 Merge pull request #3111 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to dbb20f76a7
2025-06-26 12:01:09 +02:00
github-actions[bot]
a9f2ec45da New Crowdin Translations (automated) (#35189)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-26 08:40:39 +00:00
diondiondion
c1ef1f62d5 fix: Prevent scrolling behind menus and modals in Safari iOS (#35183) 2025-06-25 19:22:11 +00:00
diondiondion
d285b07774 [Glitch] fix: Fix search icon overlapping text on Trending page
Port 8fa32ca8ba to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-25 19:46:43 +02:00
diondiondion
0156ed6641 [Glitch] fix: Prevent content scrolling behind main menu (part 1)
Port c6dddbb66e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-25 19:46:18 +02:00
Echo
9e5b9433f8 [Glitch] Storybook Helpers
Port c52848b444 to glitch-soc, kinda

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-25 19:44:38 +02:00
Emelia Smith
34b8ff8267 [Glitch] Implement Instance Moderation Notes
Port CSS changes from 72f2f35bfb to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-25 19:42:23 +02:00
Claire
c9a1e27a49 Merge commit 'dbb20f76a781defe35d077529c8269d712c1fbd2' into glitch-soc/merge-upstream
Conflicts:
- `tsconfig.json`:
  glitch-soc had extra paths under `app/javascript/flavours`, but upstream
  added `app/javascript` as a whole, so updated to upstream's.
2025-06-25 19:29:09 +02:00
Claire
dbb20f76a7 Fix crash in development environment with no prebuilt assets and no vite dev server running (#35177) 2025-06-25 14:20:07 +00:00
renovate[bot]
91741214e1 chore(deps): update node.js to 22.17 (#35166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 14:01:46 +00:00
diondiondion
8fa32ca8ba fix: Fix search icon overlapping text on Trending page (#35175) 2025-06-25 13:26:44 +00:00
Matt Jankowski
8285194451 Move layout setup for OAuth views to controllers (#35176) 2025-06-25 13:26:17 +00:00
Claire
392eaf1010 Ensure consistent ordering of rule translations in admin interface (#35174) 2025-06-25 13:15:59 +00:00
Claire
fa9318083e Merge pull request #3110 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 8ba1487f30
2025-06-25 14:51:27 +02:00
diondiondion
c6dddbb66e fix: Prevent content scrolling behind main menu (part 1) (#35173) 2025-06-25 12:12:49 +00:00
Echo
c52848b444 Storybook Helpers (#35158) 2025-06-25 11:20:11 +00:00
Claire
0a7418e6d8 Change rule translation interface to display english name and populate empty translations (#35170) 2025-06-25 10:02:19 +00:00
Emelia Smith
72f2f35bfb Implement Instance Moderation Notes (#31529) 2025-06-25 08:15:44 +00:00
github-actions[bot]
0f9f27972d New Crowdin Translations (automated) (#35165)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-25 08:05:57 +00:00
Matt Jankowski
9f16f41678 Remove patch for unsupported redis version (#35155) 2025-06-25 07:53:38 +00:00
Matt Jankowski
47fda2df2c Update OAuth inflection to match spec (#35160) 2025-06-25 07:52:30 +00:00
Matt Jankowski
377289c961 Add coverage for doorkeeper model extensions (#35161) 2025-06-25 07:50:20 +00:00
Matt Jankowski
f852da50f6 Add User#email_domain method to extract domain from email address (#35159) 2025-06-25 07:22:19 +00:00
diondiondion
e44143db8c [Glitch] fix: Fix inaccessible "Clear search" button
Port 8ba1487f30 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 23:10:50 +02:00
diondiondion
73f77edf40 [Glitch] feat: More obvious loading state when submitting a post
Port 644da36336 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 22:54:00 +02:00
diondiondion
eb1674ec50 [Glitch] feat: Add Storybook for component documentation, testing, and development
Port f2cfa4f482 to glitch-soc

Co-authored-by: Echo <ChaosExAnima@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 22:53:02 +02:00
diondiondion
c9f17899a6 [Glitch] fix: Improve status focus indicators
Port fb5b8ae0a5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 22:50:00 +02:00
Echo
97d3dac4b6 [Glitch] Adds Redux and React-Intl to storybook
Port 8ee8231a43 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 22:49:40 +02:00
Claire
e44b333660 [Glitch] Fix Firefox not updating spellcheck language in textarea
Port c4128d89c9 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 22:48:35 +02:00
Claire
26ee915d0b [Glitch] Fix “Alt text” button submitting form in moderation interface
Port 9954acf61d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 22:48:07 +02:00
Claire
93bdb16817 Merge commit '8ba1487f30685fff4555a7537d3e6c765c73a07c' into glitch-soc/merge-upstream
Conflicts:
- `spec/models/concerns/account/interactions_spec.rb`:
  Conflict due to glitch-soc having modified specs ages ago.
  The covered code is the same as upstream, though.
  Took upstream's version of the specs.
- `spec/models/status_spec.rb`:
  Conflict because glitch-soc tests for an extra glitch-soc-specific
  method.
  Added upstream's changes while keeping the glitch-soc method.
2025-06-24 22:43:43 +02:00
Claire
f723718576 Merge pull request #3109 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to b9b1500fc5
2025-06-24 22:37:18 +02:00
diondiondion
f7c36f44a4 [Glitch] fix: Update hashtags when (un)following a hashtag
Port b9b1500fc5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 22:21:05 +02:00
diondiondion
c6a99eaf5b [Glitch] refactor: Use new main menu as "Getting started" column in Advanced Web UI
Port d28a4428b5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 22:21:05 +02:00
Claire
d8b0beb70d Merge commit 'b9b1500fc516ea31ab21441737c600f9b571a07d' into glitch-soc/merge-upstream 2025-06-24 21:44:33 +02:00
Claire
eb3823f0cf Merge pull request #3108 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 6166e61638
2025-06-24 21:41:28 +02:00
diondiondion
7fff0d24c8 [Glitch] fix: Keep user on Compose page when changing screen size, #34937
Port 6166e61638 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-24 21:26:08 +02:00
Claire
9fccf0a8c6 Merge commit '6166e616389b051039dc76635048e2519271832a' into glitch-soc/merge-upstream 2025-06-24 20:45:46 +02:00
diondiondion
8ba1487f30 fix: Fix inaccessible "Clear search" button (#35152) 2025-06-24 14:36:05 +00:00
diondiondion
644da36336 feat: More obvious loading state when submitting a post (#35153) 2025-06-24 14:08:48 +00:00
diondiondion
fb5b8ae0a5 fix: Improve status focus indicators (#35150) 2025-06-24 09:34:43 +00:00
Matt Jankowski
fd902c04f7 Use config_for for omniauth enabled values (#35015) 2025-06-24 09:32:13 +00:00
Echo
8ee8231a43 Adds Redux and React-Intl to storybook (#35094) 2025-06-24 09:31:27 +00:00
Claire
c4128d89c9 Fix Firefox not updating spellcheck language in textarea (#35148) 2025-06-24 09:08:00 +00:00
Claire
9954acf61d Fix “Alt text” button submitting form in moderation interface (#35147) 2025-06-24 09:04:26 +00:00
renovate[bot]
0276354775 fix(deps): update dependency @vitejs/plugin-react to v4.6.0 (#35137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 08:05:24 +00:00
github-actions[bot]
dba636da7a New Crowdin Translations (automated) (#35144)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-24 07:58:50 +00:00
renovate[bot]
43e9186e5d chore(deps): update dependency haml_lint to v0.63.0 (#35146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 07:54:49 +00:00
Matt Jankowski
0338733531 Add model coverage and scopes to RuleTranslation class (#35098) 2025-06-24 07:44:50 +00:00
Eugen Rochko
1be48d0cab Change terms of service e-mail job to be iterable (#35126) 2025-06-24 07:41:39 +00:00
renovate[bot]
e60014ed9c fix(deps): update dependency pg to v8.16.2 (#35111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 07:40:46 +00:00
Matt Jankowski
0d7f1584bc Move remaining _map method specs from account to mappings spec (#35142) 2025-06-24 07:40:24 +00:00
Matt Jankowski
36f01af6c4 Add Status#only_reblogs scope for annual report classes (#35141) 2025-06-24 06:54:55 +00:00
renovate[bot]
16057f550d fix(deps): update dependency pg-connection-string to v2.9.1 (#35112)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 06:51:37 +00:00
renovate[bot]
e79ecabd0a chore(deps): update dependency strong_migrations to v2.4.0 (#35140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 06:51:09 +00:00
Matt Jankowski
c023ebc87a Limit count to pending&trending on admin/trends/tags page (#35120) 2025-06-23 13:30:12 +00:00
Matt Jankowski
ebc6897afb Extract method to DRY up month/year grouping in AnnualReport::TimeSeries class (#35113) 2025-06-23 12:18:29 +00:00
Matt Jankowski
b08ccaa5b3 Extract Account::Mappings concern from "interactions" (#35119) 2025-06-23 12:02:14 +00:00
diondiondion
b9b1500fc5 fix: Update hashtags when (un)following a hashtag (#35101) 2025-06-23 11:44:59 +00:00
diondiondion
d28a4428b5 refactor: Use new main menu as "Getting started" column in Advanced Web UI (#35117) 2025-06-23 09:59:47 +00:00
diondiondion
6166e61638 fix: Keep user on Compose page when changing screen size, #34937 (#35105) 2025-06-23 09:53:21 +00:00
github-actions[bot]
e5aa8c1ff3 New Crowdin Translations (automated) (#35090)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-23 08:43:53 +00:00
Matt Jankowski
8837fd8c54 Update rubocop to version 1.77.0 (#35128) 2025-06-23 07:40:11 +00:00
Claire
94c644983e Merge pull request #3107 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to ac039d5f13
2025-06-22 11:36:37 +02:00
Claire
13a07e44f1 [Glitch] Fix clicking a status multiple times causing duplicate entries in browser history
Port ac039d5f13 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-21 23:06:28 +02:00
diondiondion
f41981e772 [Glitch] fix: Fix SCSS lint warnings
Port 3f743b1a07 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-21 23:04:16 +02:00
Claire
9cb5a77c3e Merge commit 'ac039d5f1323c46062d004896996f50549bfa38b' into glitch-soc/merge-upstream 2025-06-21 22:59:04 +02:00
Claire
ac039d5f13 Fix clicking a status multiple times causing duplicate entries in browser history (#35118) 2025-06-21 09:00:38 +00:00
David Roetzel
adf812efb3 Fix missing terms of services link (#35115) 2025-06-21 08:59:47 +00:00
diondiondion
3f743b1a07 fix: Fix SCSS lint warnings (#35102) 2025-06-21 08:58:12 +00:00
Claire
2b360c479c Merge pull request #3106 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 0ec6c26af3
2025-06-20 13:52:06 +02:00
renovate[bot]
204ff46f7e chore(deps): update dependency rspec-rails to v8.0.1 (#35110)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 10:20:24 +00:00
Matt Jankowski
54f9a1b43b Extract secret size constants in Webhook model (#35104) 2025-06-20 10:05:24 +00:00
Matt Jankowski
e9b1c1edfe Simplify WebauthnCredential constant limit math (#35107) 2025-06-20 10:04:14 +00:00
diondiondion
455df074fe [Glitch] fix: Prevent click on content warning banner in notification from opening the post
Port 08597a1819 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-20 12:03:17 +02:00
diondiondion
68fb65b08d [Glitch] fix: Prevent mobile navbar from overscrolling
Port 102a7635d6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-20 12:02:49 +02:00
diondiondion
c9d3b8e3a5 [Glitch] fix: Tweak focus style & spacing of list/hashtags expand/collapse button
Port 474464ffff to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-20 12:02:20 +02:00
Claire
f8f458e5e6 Merge commit '0ec6c26af3d7dc9a0eeb5631ebb9f56b724aaa8e' into glitch-soc/merge-upstream 2025-06-20 12:01:02 +02:00
David Roetzel
0ec6c26af3 Fix error when RFC9421 signatures are used (#35109) 2025-06-20 09:44:26 +00:00
diondiondion
08597a1819 fix: Prevent click on content warning banner in notification from opening the post (#35096) 2025-06-20 09:41:24 +00:00
diondiondion
102a7635d6 fix: Prevent mobile navbar from overscrolling (#35074) 2025-06-18 11:55:16 +00:00
Matt Jankowski
b1fe35d7d2 Update rubocop to version 1.76.2 (#35070) 2025-06-18 09:54:17 +00:00
renovate[bot]
adf01b021c chore(deps): update dependency debug to v1.11.0 (#35079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 09:53:05 +00:00
Matt Jankowski
aac51707d1 Use ENV.fetch for ffmpeg/ffprobe defaults (#35081) 2025-06-18 09:43:25 +00:00
renovate[bot]
aa345c4630 chore(deps): update dependency opentelemetry-instrumentation-http to '~> 0.25.0' (#35088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 09:42:53 +00:00
renovate[bot]
70c6e09e0f chore(deps): update dependency annotaterb to v4.16.0 (#35087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 09:42:17 +00:00
renovate[bot]
1a7fd2f446 chore(deps): update dependency faraday-httpclient to v2.0.2 (#35082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 09:41:24 +00:00
Claire
33402722f3 Merge pull request #3105 from Plastikmensch/fix-inverted-regex-filter
Fix inverted regex filter condition
2025-06-18 08:52:41 +02:00
Plastikmensch
cc6a16e62c Fix inverted regex filter condition
The inverted condition caused only own toots and toots matching the regex to be shown instead of matches being filtered.

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>
2025-06-18 02:04:20 +02:00
diondiondion
474464ffff fix: Tweak focus style & spacing of list/hashtags expand/collapse button (#35075) 2025-06-17 17:54:23 +00:00
704 changed files with 17554 additions and 7904 deletions

View File

@@ -0,0 +1 @@
https://joinmastodon.org/funding.json

View File

@@ -20,7 +20,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
@@ -37,7 +37,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}

View File

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

View File

@@ -14,6 +14,7 @@ on:
- config/locales-glitch/devise.en.yml
- config/locales-glitch/doorkeeper.en.yml
- .github/workflows/crowdin-upload.yml
workflow_dispatch:
jobs:
upload-translations:

2
.nvmrc
View File

@@ -1 +1 @@
22.16
22.17

View File

@@ -82,6 +82,9 @@ AUTHORS.md
# Process a few selected JS files
!lint-staged.config.js
# Ignore config YAML files that include ERB/ruby code prettier does not understand
/config/email.yml
# Ignore glitch-soc emoji map file
/app/javascript/flavours/glitch/features/emoji/emoji_map.json
/app/javascript/flavours/glitch/features/emoji/emoji_data.json
@@ -94,4 +97,4 @@ AUTHORS.md
app/javascript/flavours/glitch/styles/reset.scss
# Ignore win95 theme
app/javascript/styles/win95.scss
app/javascript/styles/win95.scss

View File

@@ -23,5 +23,6 @@ RSpec/SpecFilePathFormat:
ActivityPub: activitypub
DeepL: deepl
FetchOEmbedService: fetch_oembed_service
OAuth: oauth
OEmbedController: oembed_controller
OStatus: ostatus

View File

@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.76.1.
# using RuboCop version 1.77.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -28,7 +28,7 @@ Metrics/PerceivedComplexity:
Max: 27
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedVars.
# Configuration parameters: AllowedVars, DefaultToNil.
Style/FetchEnvVar:
Exclude:
- 'config/initializers/paperclip.rb'

View File

@@ -11,6 +11,21 @@ const config: StorybookConfig = {
name: '@storybook/react-vite',
options: {},
},
staticDirs: [
'./static',
// We need to manually specify the assets because of the symlink in public/sw.js
...[
'avatars',
'emoji',
'headers',
'sounds',
'badge.png',
'loading.gif',
'loading.png',
'oops.gif',
'oops.png',
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
],
};
export default config;

View File

@@ -1,29 +0,0 @@
import type { Preview } from '@storybook/react-vite';
// If you want to run the dark theme during development,
// you can change the below to `/application.scss`
import '../app/javascript/styles/mastodon-light.scss';
const preview: Preview = {
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
parameters: {
layout: 'centered',
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
},
};
export default preview;

146
.storybook/preview.tsx Normal file
View File

@@ -0,0 +1,146 @@
import { useEffect, useState } from 'react';
import { IntlProvider } from 'react-intl';
import { MemoryRouter, Route } from 'react-router';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import type { Preview } from '@storybook/react-vite';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { action } from 'storybook/actions';
import type { LocaleData } from '@/mastodon/locales';
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
import { defaultMiddleware } from '@/mastodon/store/store';
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
// If you want to run the dark theme during development,
// you can change the below to `/application.scss`
import '../app/javascript/styles/mastodon-light.scss';
const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
query: { as: 'json' },
});
// Initialize MSW
initialize({
onUnhandledRequest: unhandledRequestHandler,
});
const preview: Preview = {
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
globalTypes: {
locale: {
description: 'Locale for the story',
toolbar: {
title: 'Locale',
icon: 'globe',
items: Object.keys(localeFiles).map((path) =>
path.replace('/mastodon/locales/', '').replace('.json', ''),
),
dynamicTitle: true,
},
},
},
initialGlobals: {
locale: 'en',
},
decorators: [
(Story, { parameters }) => {
const { state = {} } = parameters;
let reducer = rootReducer;
if (typeof state === 'object' && state) {
reducer = reducerWithInitialState(state as Record<string, unknown>);
}
const store = configureStore({
reducer,
middleware(getDefaultMiddleware) {
return getDefaultMiddleware(defaultMiddleware);
},
});
return (
<Provider store={store}>
<Story />
</Provider>
);
},
(Story, { globals }) => {
const currentLocale = (globals.locale as string) || 'en';
const [messages, setMessages] = useState<
Record<string, Record<string, string>>
>({});
const currentLocaleData = messages[currentLocale];
useEffect(() => {
async function loadLocaleData() {
const { default: localeFile } = (await import(
`@/mastodon/locales/${currentLocale}.json`
)) as { default: LocaleData['messages'] };
setMessages((prevLocales) => ({
...prevLocales,
[currentLocale]: localeFile,
}));
}
if (!currentLocaleData) {
void loadLocaleData();
}
}, [currentLocale, currentLocaleData]);
return (
<IntlProvider
locale={currentLocale}
messages={currentLocaleData}
textComponent='span'
>
<Story />
</IntlProvider>
);
},
(Story) => (
<MemoryRouter>
<Story />
<Route
path='*'
// eslint-disable-next-line react/jsx-no-bind
render={({ location }) => {
if (location.pathname !== '/') {
action(`route change to ${location.pathname}`)(location);
}
return null;
}}
/>
</MemoryRouter>
),
],
loaders: [mswLoader],
parameters: {
layout: 'centered',
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
state: {},
docs: {},
msw: {
handlers: mockHandlers,
},
},
};
export default preview;

View File

@@ -0,0 +1,344 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.10.2'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
*/
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}

View File

@@ -2,7 +2,151 @@
All notable changes to this project will be documented in this file.
## [4.4.0] - UNRELEASED
## [4.4.9] - 2025-11-20
### Fixed
- Fix `tootctl upgrade storage-schema` failing with `ArgumentError` (#36914 by @shugo)
- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire)
- Fix filters not being applied to quotes in detailed view (#36843 by @ClearlyClaire)
## [4.4.8] - 2025-10-21
### Security
- Fix quote control bypass ([GHSA-8h43-rcqj-wpc6](https://github.com/mastodon/mastodon/security/advisories/GHSA-8h43-rcqj-wpc6))
## [4.4.7] - 2025-10-15
### Fixed
- Fix forwarder being called with `nil` status when quote post is soft-deleted (#36463 by @ClearlyClaire)
- Fix moderation warning e-mails that include posts (#36462 by @ClearlyClaire)
- Fix allow_referrer_origin typo (#36460 by @ShadowJonathan)
## [4.4.6] - 2025-10-13
### Security
- Update dependencies `rack` and `uri`
- Fix streaming server connection not being closed on user suspension (by @ThisIsMissEm, [GHSA-r2fh-jr9c-9pxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-r2fh-jr9c-9pxh))
- Fix password change through admin CLI not invalidating existing sessions and access tokens (by @ThisIsMissEm, [GHSA-f3q3-rmf7-9655](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q3-rmf7-9655))
- Fix streaming server allowing access to public timelines even without the `read` or `read:statuses` OAuth scopes (by @ThisIsMissEm, [GHSA-7gwh-mw97-qjgp](https://github.com/mastodon/mastodon/security/advisories/GHSA-7gwh-mw97-qjgp))
### Added
- Add support for processing quotes of deleted posts signaled through a `Tombstone` (#36381 by @ClearlyClaire)
### Fixed
- Fix quote post state sometimes not being updated through streaming server (#36408 by @ClearlyClaire)
- Fix inconsistent “pending tags” count on admin dashboard (#36404 by @mjankowski)
- Fix JSON payload being potentially mutated when processing interaction policies (#36392 by @ClearlyClaire)
- Fix quotes not being displayed in email notifications (#36379 by @diondiondion)
- Fix redirect to external object when URL is missing or malformed (#36347 by @ClearlyClaire)
- Fix quotes not being displayed in the featured carousel (#36335 by @diondiondion)
## [4.4.5] - 2025-09-23
### Security
- Update dependencies
### Added
- Add support for `has:quote` in search (#36217 by @ClearlyClaire)
### Changed
- Change quoted posts from silenced accounts to use a click-through rather than being hidden (#36166 and #36167 by @ClearlyClaire)
### Fixed
- Fix processing of out-of-order `Update` as implicit updates (#36190 by @ClearlyClaire)
- Fix getting `Create` and `Update` out of order (#36176 by @ClearlyClaire)
- Fix quotes with Content Warnings but no text being shown without Content Warnings (#36150 by @ClearlyClaire)
## [4.4.4] - 2025-09-16
### Security
- Update dependencies
### Fixed
- Fix missing memoization in `Web::PushNotificationWorker` (#36085 by @ClearlyClaire)
- Fix unresponsive areas around GIFV modals in some cases (#36059 by @ClearlyClaire)
- Fix missing `beforeUnload` confirmation when a poll is being authored (#36030 by @ClearlyClaire)
- Fix processing of remote edited statuses with new media and no text (#35970 by @unfokus)
- Fix polls not being displayed in moderation interface (#35644 and #35933 by @ThisIsMissEm)
- Fix WebUI handling of deleted quoted posts (#35909 and #35918 by @ClearlyClaire and @diondiondion)
- Fix “Edit” and “Delete & Redraft” on a poll not inserting empty option (#35892 by @ClearlyClaire)
- Fix loading of some compatibility CSS on some configurations (#35876 by @shleeable)
- Fix HttpLog not being enabled with `RAILS_LOG_LEVEL=debug` (#35833 by @mjankowski)
- Fix self-destruct scheduler behavior on some Redis setups (#35823 by @ClearlyClaire)
- Fix `tootctl admin create` not bypassing reserved username checks (#35779 by @ClearlyClaire)
- Fix interaction policy changes in implicit updates not being saved (#35751 by @ClearlyClaire)
- Fix quote revocation not being streamed (#35710 by @ClearlyClaire)
- Fix export of large user archives by enabling Zip64 (#35850 by @ClearlyClaire)
### Changed
- Change labels for quote policy settings (#35893 by @ClearlyClaire)
- Change standalone “Share” page to redirect to web interface after posting (#35763 by @ChaosExAnima)
## [4.4.3] - 2025-08-05
### Security
- Update dependencies
- Fix incorrect rate-limit handling [GHSA-84ch-6436-c7mg](https://github.com/mastodon/mastodon/security/advisories/GHSA-84ch-6436-c7mg)
### Fixed
- Fix race condition caused by ActiveRecord query cache in `Create` critical path (#35662 by @ClearlyClaire)
- Fix race condition caused by quote post processing (#35657 by @ClearlyClaire)
- Fix WebUI crashing for accounts with `null` URL (#35651 by @ClearlyClaire)
- Fix friends-of-friends recommendations suggesting already-requested accounts (#35604 by @ClearlyClaire)
- Fix synchronous recursive fetching of deeply-nested quoted posts (#35600 by @ClearlyClaire)
- Fix “Expand this post” link including user `@undefined` (#35478 by @ClearlyClaire)
### Changed
- Change `StatusReachFinder` to consider quotes as well as reblogs (#35601 by @ClearlyClaire)
- Add restrictions on which quote posts can trend (#35507 by @ClearlyClaire)
- Change quote verification to not bypass authorization flow for mentions (#35528 by @ClearlyClaire)
## [4.4.2] - 2025-07-23
### Security
- Update dependencies
### Fixed
- Fix menu not clickable in Firefox (#35390 and #35414 by @diondiondion)
- Add `lang` attribute to current composer language in alt text modal (#35412 by @diondiondion)
- Fix quote posts styling on notifications page (#35411 by @diondiondion)
- Improve a11y of custom select menus in notifications settings (#35403 by @diondiondion)
- Fix selected item in poll select menus is unreadable in Firefox (#35402 by @diondiondion)
- Update age limit wording (#35387 by @diondiondion)
- Fix support for quote verification in implicit status updates (#35384 by @ClearlyClaire)
- Improve `Dropdown` component accessibility (#35373 by @diondiondion)
- Fix processing some incoming quotes failing because of missing JSON-LD context (#35354 and #35380 by @ClearlyClaire)
- Make bio hashtags open the local page instead of the remote instance (#35349 by @ChaosExAnima)
- Fix styling of external log-in button (#35320 by @ClearlyClaire)
## [4.4.1] - 2025-07-09
### Fixed
- Fix nearly every sub-directory being crawled as part of Vite build (#35323 by @ClearlyClaire)
- Fix assets not building when Redis is unavailable (#35321 by @oneiros)
- Fix replying from media modal or pop-in-player tagging user `@undefined` (#35317 by @ClearlyClaire)
- Fix support for special characters in various environment variables (#35314 by @mjankowski and @ClearlyClaire)
- Fix some database migrations failing for indexes manually removed by admins (#35309 by @mjankowski)
## [4.4.0] - 2025-07-08
### Added
@@ -23,7 +167,7 @@ All notable changes to this project will be documented in this file.
Support for verifying remote quotes according to [FEP-044f](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md) and displaying them in the Web UI has been implemented.\
Quoting other people is not implemented yet, and it is currently not possible to mark your own posts as allowing quotes. However, a new “Who can quote” setting has been added to the “Posting defaults” section of the user settings. This setting allows you to set a default that will be used for new posts made on Mastodon 4.5 and newer, when quote posts will be fully implemented.\
In the REST API, quote posts are represented by a new `quote` attribute on `Status` and `StatusEdit` entities: https://docs.joinmastodon.org/entities/StatusEdit/#quote https://docs.joinmastodon.org/entities/Status/#quote
- Add ability to reorder and translate server rules (#34637, #34737, #34494, #34756, #34820 and #34997 by @ChaosExAnima and @ClearlyClaire)\
- Add ability to reorder and translate server rules (#34637, #34737, #34494, #34756, #34820, #34997, #35170, #35174 and #35174 by @ChaosExAnima and @ClearlyClaire)\
Rules are now shown in the users language, if a translation has been set.\
In the REST API, `Rule` entities now have a new `translations` attribute: https://docs.joinmastodon.org/entities/Rule/#translations
- Add emoji from Twemoji 15.1.0, including in the emoji picker/completion (#33395, #34321, #34620, and #34677 by @ChaosExAnima, @ClearlyClaire, @TheEssem, and @eramdam)
@@ -38,8 +182,11 @@ All notable changes to this project will be documented in this file.
Server administrators can now chose to opt in to transmit referrer information when following an external link. Only the domain name is transmitted, not the referrer path.
- Add double tap to zoom and swipe to dismiss to media modal in web UI (#34210 by @Gargron)
- Add link from Web UI for Hashtags to the Moderation UI (#31448 by @ThisIsMissEm)
- **Add terms of service** (#33055, #33233, #33230, #33703, #33699, #33994, #33993, #34105, #34122, #34200, #34527 and #35053 by @ClearlyClaire, @Gargron, @mjankowski, and @oneiros)\
Server administrators can now fill in Terms of Service, optionally using a provided template.
- **Add terms of service** (#33055, #33233, #33230, #33703, #33699, #33994, #33993, #34105, #34122, #34200, #34527, #35053, #35115, #35126, #35127 and #35233 by @ClearlyClaire, @Gargron, @mjankowski, and @oneiros)\
Server administrators can now fill in Terms of Service and notify their users of upcoming changes.
- Add optional bulk mailer settings (#35191 and #35203 by @oneiros)\
This adds the optional environment variables `BULK_SMTP_PORT`, `BULK_SMTP_SERVER`, `BULK_SMTP_LOGIN` and so on analogous to `SMTP_PORT`, `SMTP_SERVER`, `SMTP_LOGIN` and related SMTP configuration environment variables.\
When `BULK_SMTP_SERVER` is set, this group of variables is used instead of the regular ones for sending announcement notification emails and Terms of Service notification emails.
- **Add age verification on sign-up** (#34150, #34663, and #34636 by @ClearlyClaire and @Gargron)\
Server administrators now have a setting to set a minimum age requirement for creating a new server, asking users for their date of birth. The date of birth is checked against the minimum age requirement server-side but not stored.\
The following REST API changes have been made to accommodate this:
@@ -48,10 +195,12 @@ All notable changes to this project will be documented in this file.
- Add ability to dismiss alt text badge by tapping it in web UI (#33737 by @Gargron)
- Add loading indicator to timeline gap indicators in web UI (#33762 by @Gargron)
- Add interaction modal when trying to interact with a poll while logged out (#32609 by @ThisIsMissEm)
- **Add experimental FASP support** (#34031, #34415, #34765, #34965, and #34964 by @oneiros)\
- **Add experimental FASP support** (#34031, #34415, #34765, #34965, #34964, #34033, #35218, #35262 and #35263 by @oneiros)\
This is a first step towards supporting “Fediverse Auxiliary Service Providers” (https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications). This is mostly interesting to developers who would like to implement their own FASP, but also includes the capability to share data with a discovery provider (see https://www.fediscovery.org).
- Add ability for admins to send announcements to all users via email (#33928 and #34411 by @ClearlyClaire)\
This is meant for critical announcements only, as this will potentially send a lot of emails and cannot be opted out of by users.
- Add Server Moderation Notes (#31529 by @ThisIsMissEm)
- Add loading spinner to “Post” button when sending a post (#35153 by @diondiondion)
- Add option to use system scrollbar styling (#32117 by @vmstan)
- Add hover cards to follow suggestions (#33749 by @ClearlyClaire)
- Add `t` hotkey for post translations (#33441 by @ClearlyClaire)
@@ -59,7 +208,7 @@ All notable changes to this project will be documented in this file.
- Add dropdown menu with quick actions to lists of accounts in web UI (#34391, #34709, and #34767 by @Gargron, @diondiondion, and @mkljczk)
- Add support for displaying “year in review” notification in web UI (#32710, #32765, #32709, #32807, #32914, #33148, and #33882 by @Gargron and @mjankowski)\
Note that the notification is currently not generated automatically, and at the moment requires a manual undocumented administrator action.
- Add experimental support for receiving HTTP Message Signatures (RFC9421) (#34814 and #35033 by @oneiros)\
- Add experimental support for receiving HTTP Message Signatures (RFC9421) (#34814, #35033, #35109 and #35278 by @oneiros)\
For now, this needs to be explicitly enabled through the `http_message_signatures` feature flag (`EXPERIMENTAL_FEATURES=http_message_signatures`). This currently only covers verifying such signatures (inbound HTTP requests), not issuing them (outbound HTTP requests).
- Add experimental Async Refreshes API (#34918 by @oneiros)
- Add experimental server-side feature to fetch remote replies (#32615, #34147, #34149, #34151, #34615, #34682, and #34702 by @ClearlyClaire and @sneakers-the-rat)\
@@ -112,7 +261,7 @@ All notable changes to this project will be documented in this file.
### Changed
- Change design of navigation panel in Web UI, change layout on narrow screens (#34910, #34987, #35017, #34986, #35029, #35065, #35067 and #35072 by @ClearlyClaire, @Gargron, and @diondiondion)
- Change design of navigation panel in Web UI, change layout on narrow screens (#34910, #34987, #35017, #34986, #35029, #35065, #35067, #35072, #35074, #35075, #35101, #35173, #35183, #35193 and #35225 by @ClearlyClaire, @Gargron, and @diondiondion)
- Change design of lists in web UI (#32881, #33054, and #33036 by @Gargron)
- Change design of edit media modal in web UI (#33516, #33702, #33725, #33725, #33771, and #34345 by @Gargron)
- Change design of audio player in web UI (#34520, #34740, #34865, #34929, #34933, and #35034 by @ClearlyClaire, @Gargron, and @diondiondion)
@@ -126,15 +275,17 @@ All notable changes to this project will be documented in this file.
Moderators will still be able to access them while they are kept, but they won't be accessible to the public in the meantime.
- Change language names in compose box language picker to be localized (#33402 by @c960657)
- Change onboarding flow in web UI (#32998, #33119, #33471 and #34962 by @ClearlyClaire and @Gargron)
- Change Advanced Web UI to use the new main menu instead of the “Getting started” column (#35117 by @diondiondion)
- Change emoji categories in admin interface to be ordered by name (#33630 by @ShadowJonathan)
- Change design of rich text elements in web UI (#32633 by @Gargron)
- Change wording of “single choice” to “pick one” in poll authoring form (#32397 by @ThisIsMissEm)
- Change returned favorite and boost counts to use those provided by the remote server, if available (#32620, #34594, #34618, and #34619 by @ClearlyClaire and @sneakers-the-rat)
- Change label of favourite notifications on private mentions (#31659 by @ClearlyClaire)
- Change wording of "discard draft?" confirmation dialogs (#35192 by @diondiondion)
- Change `libvips` to be enabled by default in place of ImageMagick (#34741 and #34753 by @ClearlyClaire and @diondiondion)
- Change avatar and header size limits from 2MB to 8MB when using libvips (#33002 by @Gargron)
- Change search to use query params in web UI (#32949 and #33670 by @ClearlyClaire and @Gargron)
- Change build system from Webpack to Vite (#34454, #34450, #34758, #34768, #34813, #34808, #34837, #34732, #35007 and #35035 by @ChaosExAnima, @ClearlyClaire, @mjankowski, and @renchap)
- Change build system from Webpack to Vite (#34454, #34450, #34758, #34768, #34813, #34808, #34837, #34732, #35007, #35035 and #35177 by @ChaosExAnima, @ClearlyClaire, @mjankowski, and @renchap)
- Change account creation API to forbid creation from user tokens (#34828 by @ThisIsMissEm)
- Change `/api/v2/instance` to be enabled without authentication when limited federation mode is enabled (#34576 by @ClearlyClaire)
- Change `DEFAULT_LOCALE` to not override unauthenticated users browser language (#34535 by @ClearlyClaire)\
@@ -202,17 +353,23 @@ All notable changes to this project will be documented in this file.
- Fix not being able to scroll dropdown on touch devices in web UI (#34873 by @Gargron)
- Fix inconsistent filtering of silenced accounts for other silenced accounts (#34863 by @ClearlyClaire)
- Fix update checker listing updates older or equal to current running version (#33906 by @ClearlyClaire)
- Fix clicking a status multiple times causing duplicate entries in browser history (#35118 by @ClearlyClaire)
- Fix Alt text button submitting form in moderation interface (#35147 by @ClearlyClaire)
- Fix Firefox sometimes not updating spellcheck language in textarea (#35148 by @ClearlyClaire)
- Fix `NoMethodError` in edge case of emoji cache handling (#34749 by @dariusk)
- Fix handling of inlined `featured` collections in ActivityPub actor objects (#34789 and #34811 by @ClearlyClaire)
- Fix long link names in admin sidebar being truncated (#34727 by @diondiondion)
- Fix admin dashboard crash on specific Elasticsearch connection errors (#34683 by @ClearlyClaire)
- Fix OIDC account creation failing for long display names (#34639 by @defnull)
- Fix use of the deprecated `/api/v1/instance` endpoint in the moderation interface (#34613 by @renchap)
- Fix inaccessible Clear search button (#35152 and #35281 by @diondiondion)
- Fix search operators sometimes getting lost (#35190 by @ClearlyClaire)
- Fix directory scroll position reset (#34560 by @przucidlo)
- Fix needlessly complex SVG paths for oEmbed and logo (#34538 by @edent)
- Fix avatar sizing with long account name in some UI elements (#34514 by @gomasy)
- Fix empty menu section in status dropdown (#34431 by @ClearlyClaire)
- Fix the delete suggestion button not working (#34396 and #34398 by @ClearlyClaire and @renchap)
- Fix popover/dialog backgrounds not being blurred on older Webkit browsers (#35220 by @diondiondion)
- Fix radio buttons not always being correctly centered (#34389 by @ChaosExAnima)
- Fix visual glitches with adding post filters (#34387 by @ChaosExAnima)
- Fix bugs with upload progress (#34325 by @ChaosExAnima)
@@ -220,7 +377,7 @@ All notable changes to this project will be documented in this file.
- Fix extra space under left-indented vertical videos (#34313 by @ClearlyClaire)
- Fix glitchy iOS media attachment drag interactions (#35057 by @diondiondion)
- Fix zoomed images being blurry in Safari (#35052 by @diondiondion)
- Fix redundant focus stop within status component in Web UI (#35037 and #35051 by @diondiondion)
- Fix redundant focus stop within status component in Web UI and make focus style more noticeable (#35037, #35051, #35096, #35150 and #35251 by @diondiondion)
- Fix digits in media player time readout not having a consistent width (#35038 by @diondiondion)
- Fix wrong text color for Open in advanced web interface banner in high-contrast theme (#35032 by @diondiondion)
- Fix hover card for limited accounts not hiding information as expected (#35024 by @diondiondion)

View File

@@ -111,7 +111,7 @@ group :opentelemetry do
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.27.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
@@ -159,6 +159,9 @@ group :test do
# Stub web requests for specs
gem 'webmock', '~> 3.18'
# Websocket driver for testing integration between rails/sidekiq and streaming
gem 'websocket-driver', '~> 0.8', require: false
end
group :development do

View File

@@ -10,29 +10,29 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
actioncable (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
actionmailbox (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
actionmailer (8.0.2.1)
actionpack (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
actionpack (8.0.2.1)
actionview (= 8.0.2.1)
activesupport (= 8.0.2.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -40,15 +40,15 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
actiontext (8.0.2.1)
actionpack (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.2)
activesupport (= 8.0.2)
actionview (8.0.2.1)
activesupport (= 8.0.2.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@@ -58,22 +58,22 @@ GEM
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (8.0.2)
activesupport (= 8.0.2)
activejob (8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.3.6)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
activemodel (8.0.2.1)
activesupport (= 8.0.2.1)
activerecord (8.0.2.1)
activemodel (= 8.0.2.1)
activesupport (= 8.0.2.1)
timeout (>= 0.4.0)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
activestorage (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activesupport (= 8.0.2.1)
marcel (~> 1.0)
activesupport (8.0.2)
activesupport (8.0.2.1)
base64
benchmark (>= 0.3)
bigdecimal
@@ -90,7 +90,9 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
android_key_attestation (0.3.0)
annotaterb (4.15.0)
annotaterb (4.16.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.3.2)
@@ -114,12 +116,12 @@ GEM
base64 (0.3.0)
bcp47_spec (0.2.1)
bcrypt (3.1.20)
benchmark (0.4.0)
benchmark (0.4.1)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.1.9)
bigdecimal (3.2.2)
bindata (2.5.1)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
@@ -178,7 +180,7 @@ GEM
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.4.1)
debug (1.10.0)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
debug_inspector (1.2.0)
@@ -222,6 +224,7 @@ GEM
mail (~> 2.7)
email_validator (2.2.4)
activemodel
erb (5.0.1)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
@@ -236,7 +239,7 @@ GEM
logger
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-httpclient (2.0.1)
faraday-httpclient (2.0.2)
httpclient (>= 2.2)
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
@@ -284,7 +287,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.62.0)
haml_lint (0.64.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@@ -455,7 +458,7 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.8)
nokogiri (1.18.9)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.11)
@@ -491,7 +494,7 @@ GEM
tzinfo
validate_url
webfinger (~> 2.0)
openssl (3.3.0)
openssl (3.3.1)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.5.0)
@@ -550,7 +553,7 @@ GEM
opentelemetry-instrumentation-faraday (0.27.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http (0.24.0)
opentelemetry-instrumentation-http (0.25.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http_client (0.23.0)
@@ -639,7 +642,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.16)
rack (3.1.18)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (3.0.0)
@@ -665,20 +668,20 @@ GEM
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
rails (8.0.2.1)
actioncable (= 8.0.2.1)
actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.2.1)
actionpack (= 8.0.2.1)
actiontext (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activemodel (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
bundler (>= 1.15.0)
railties (= 8.0.2)
railties (= 8.0.2.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -689,9 +692,9 @@ GEM
rails-i18n (8.0.1)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
railties (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -705,7 +708,8 @@ GEM
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.7.0)
rdf (~> 3.3)
rdoc (6.13.1)
rdoc (6.14.1)
erb
psych (>= 4.0.0)
redcarpet (3.6.1)
redis (4.8.1)
@@ -721,7 +725,7 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.4.1)
rexml (3.4.4)
rotp (6.3.0)
rouge (4.5.2)
rpam2 (4.0.2)
@@ -733,17 +737,17 @@ GEM
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.3)
rspec-core (3.13.4)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.4)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-github (3.0.0)
rspec-core (~> 3.0)
rspec-mocks (3.13.4)
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (8.0.0)
rspec-rails (8.0.1)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
@@ -756,8 +760,8 @@ GEM
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
rspec-support (3.13.3)
rubocop (1.76.1)
rspec-support (3.13.4)
rubocop (1.77.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -765,7 +769,7 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.45.0, < 2.0)
rubocop-ast (>= 1.45.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.45.1)
@@ -797,7 +801,7 @@ GEM
ruby-prof (1.7.2)
base64
ruby-progressbar (1.13.0)
ruby-saml (1.18.0)
ruby-saml (1.18.1)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.4)
@@ -851,8 +855,8 @@ GEM
stoplight (4.1.1)
redlock (~> 1.0)
stringio (3.1.7)
strong_migrations (2.3.0)
activerecord (>= 7)
strong_migrations (2.4.0)
activerecord (>= 7.1)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
@@ -865,7 +869,7 @@ GEM
terrapin (1.1.0)
climate_control
test-prof (1.4.4)
thor (1.3.2)
thor (1.4.0)
tilt (2.6.0)
timeout (0.4.3)
tpm-key_attestation (0.14.1)
@@ -895,7 +899,7 @@ GEM
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
uri (1.0.4)
useragent (0.16.11)
validate_url (1.0.15)
activemodel (>= 3.0.0)
@@ -928,7 +932,7 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket-driver (0.7.7)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@@ -936,7 +940,7 @@ GEM
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.2)
zeitwerk (2.7.3)
PLATFORMS
ruby
@@ -1027,7 +1031,7 @@ DEPENDENCIES
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-instrumentation-excon (~> 0.23.0)
opentelemetry-instrumentation-faraday (~> 0.27.0)
opentelemetry-instrumentation-http (~> 0.24.0)
opentelemetry-instrumentation-http (~> 0.25.0)
opentelemetry-instrumentation-http_client (~> 0.23.0)
opentelemetry-instrumentation-net_http (~> 0.23.0)
opentelemetry-instrumentation-pg (~> 0.30.0)
@@ -1092,6 +1096,7 @@ DEPENDENCIES
webauthn (~> 3.0)
webmock (~> 3.18)
webpush!
websocket-driver (~> 0.8)
xorcist (~> 1.1)
RUBY VERSION

View File

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

View File

@@ -14,16 +14,20 @@ module Admin
def create
authorize @account, :show?
account_action = Admin::AccountAction.new(resource_params)
account_action.target_account = @account
account_action.current_account = current_account
@account_action = Admin::AccountAction.new(resource_params)
@account_action.target_account = @account
@account_action.current_account = current_account
account_action.save!
if account_action.with_report?
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
if @account_action.save
if @account_action.with_report?
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
else
redirect_to admin_account_path(@account.id)
end
else
redirect_to admin_account_path(@account.id)
@warning_presets = AccountWarningPreset.all
render :new
end
end

View File

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

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
class Admin::Instances::ModerationNotesController < Admin::BaseController
before_action :set_instance, only: [:create]
before_action :set_instance_note, only: [:destroy]
def create
authorize :instance_moderation_note, :create?
@instance_moderation_note = current_account.instance_moderation_notes.new(content: resource_params[:content], domain: @instance.domain)
if @instance_moderation_note.save
redirect_to admin_instance_path(@instance.domain, anchor: helpers.dom_id(@instance_moderation_note)), notice: I18n.t('admin.instances.moderation_notes.created_msg')
else
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
render 'admin/instances/show'
end
end
def destroy
authorize @instance_moderation_note, :destroy?
@instance_moderation_note.destroy!
redirect_to admin_instance_path(@instance_moderation_note.domain, anchor: 'instance-notes'), notice: I18n.t('admin.instances.moderation_notes.destroyed_msg')
end
private
def resource_params
params
.expect(instance_moderation_note: [:content])
end
def set_instance
domain = params[:instance_id]&.strip
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
end
def set_instance_note
@instance_moderation_note = InstanceModerationNote.find(params[:id])
end
end

View File

@@ -14,6 +14,9 @@ module Admin
def show
authorize :instance, :show?
@instance_moderation_note = @instance.moderation_notes.new
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
end
@@ -52,7 +55,8 @@ module Admin
private
def set_instance
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(params[:id]&.strip))
domain = params[:id]&.strip
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
end
def set_instances

View File

@@ -17,6 +17,9 @@ module Admin
def edit
authorize @rule, :update?
missing_languages = RuleTranslation.languages - @rule.translations.pluck(:language)
missing_languages.each { |lang| @rule.translations.build(language: lang) }
end
def create

View File

@@ -4,7 +4,7 @@ class Admin::Trends::TagsController < Admin::BaseController
def index
authorize :tag, :review?
@pending_tags_count = Tag.pending_review.async_count
@pending_tags_count = pending_tags.async_count
@tags = filtered_tags.page(params[:page])
@form = Trends::TagBatch.new
end
@@ -22,6 +22,10 @@ class Admin::Trends::TagsController < Admin::BaseController
private
def pending_tags
Trends::TagFilter.new(status: :pending_review).results
end
def filtered_tags
Trends::TagFilter.new(filter_params).results
end

View File

@@ -32,7 +32,7 @@ class Api::V1::FiltersController < Api::BaseController
ApplicationRecord.transaction do
@filter.update!(keyword_params)
@filter.custom_filter.assign_attributes(filter_params)
raise Mastodon::ValidationError, I18n.t('filters.errors.deprecated_api_multiple_keywords') if @filter.custom_filter.changed? && @filter.custom_filter.keywords.count > 1
raise Mastodon::ValidationError, I18n.t('filters.errors.deprecated_api_multiple_keywords') if @filter.custom_filter.changed? && @filter.custom_filter.keywords.many?
@filter.custom_filter.save!
end

View File

@@ -15,8 +15,9 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo
if params[:date].present?
TermsOfService.published.find_by!(effective_date: params[:date])
else
TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet
TermsOfService.current
end
end
not_found if @terms_of_service.nil?
end
end

View File

@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Api::V2::SearchController < Api::BaseController
include AsyncRefreshesConcern
include Authorization
RESULTS_LIMIT = (ENV['MAX_SEARCH_RESULTS'] || 20).to_i
@@ -13,6 +14,7 @@ class Api::V2::SearchController < Api::BaseController
before_action :remote_resolve_error, if: :remote_resolve_requested?
end
before_action :require_valid_pagination_options!
before_action :handle_fasp_requests
def index
@search = Search.new(search_results)
@@ -37,6 +39,21 @@ class Api::V2::SearchController < Api::BaseController
render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401
end
def handle_fasp_requests
return unless Mastodon::Feature.fasp_enabled?
return if params[:q].blank?
# Do not schedule a new retrieval if the request is a follow-up
# to an earlier retrieval
return if request.headers['Mastodon-Async-Refresh-Id'].present?
refresh_key = "fasp:account_search:#{Digest::MD5.base64digest(params[:q])}"
return if AsyncRefresh.new(refresh_key).running?
add_async_refresh_header(AsyncRefresh.create(refresh_key))
@query_fasp = true
end
def remote_resolve_requested?
truthy_param?(:resolve)
end
@@ -58,7 +75,8 @@ class Api::V2::SearchController < Api::BaseController
search_params.merge(
resolve: truthy_param?(:resolve),
exclude_unreviewed: truthy_param?(:exclude_unreviewed),
following: truthy_param?(:following)
following: truthy_param?(:following),
query_fasp: @query_fasp
)
end

View File

@@ -101,7 +101,7 @@ class ApplicationController < ActionController::Base
end
def after_sign_out_path_for(_resource_or_scope)
if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true'
if ENV['OMNIAUTH_ONLY'] == 'true' && Rails.configuration.x.omniauth.oidc_enabled?
'/auth/auth/openid_connect/logout'
else
new_user_session_path

View File

@@ -64,6 +64,9 @@ module SignatureVerification
return (@signed_request_actor = actor) if signed_request.verified?(actor)
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri}"
rescue Mastodon::MalformedHeaderError => e
@signature_verification_failure_code = 400
fail_with! e.message
rescue Mastodon::SignatureVerificationError => e
fail_with! e.message
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
@@ -82,7 +85,7 @@ module SignatureVerification
end
def actor_from_key_id
key_id = signature_key_id
key_id = signed_request.key_id
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)

View File

@@ -50,6 +50,13 @@ module WebAppControllerConcern
return unless current_user&.require_tos_interstitial?
@terms_of_service = TermsOfService.published.first
# Handle case where terms of service have been removed from the database
if @terms_of_service.nil?
current_user.update(require_tos_interstitial: false)
return
end
render 'terms_of_service_interstitial/show', layout: 'auth'
end

View File

@@ -1,11 +1,13 @@
# frozen_string_literal: true
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
class OAuth::AuthorizationsController < Doorkeeper::AuthorizationsController
skip_before_action :authenticate_resource_owner!
before_action :store_current_location
before_action :authenticate_resource_owner!
layout 'modal'
content_security_policy do |p|
p.form_action(false)
end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
class OAuth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
skip_before_action :authenticate_resource_owner!
before_action :store_current_location
@@ -11,6 +11,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
skip_before_action :require_functional!
layout 'admin'
include Localized
def destroy

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Oauth::TokensController < Doorkeeper::TokensController
class OAuth::TokensController < Doorkeeper::TokensController
def revoke
unsubscribe_for_token if token.present? && authorized? && token.accessible?

View File

@@ -1,11 +1,11 @@
# frozen_string_literal: true
class Oauth::UserinfoController < Api::BaseController
class OAuth::UserinfoController < Api::BaseController
before_action -> { doorkeeper_authorize! :profile }, only: [:show]
before_action :require_user!
def show
@account = current_account
render json: @account, serializer: OauthUserinfoSerializer
render json: @account, serializer: OAuthUserinfoSerializer
end
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
module WellKnown
class OauthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
class OAuthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
include CacheConcern
# Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user`
@@ -13,8 +13,8 @@ module WellKnown
# new OAuth scopes are added), we don't use expires_in to cache upstream,
# instead just caching in the rails cache:
render_with_cache(
json: ::OauthMetadataPresenter.new,
serializer: ::OauthMetadataSerializer,
json: ::OAuthMetadataPresenter.new,
serializer: ::OAuthMetadataSerializer,
content_type: 'application/json',
expires_in: 15.minutes
)

View File

@@ -66,7 +66,7 @@ module ApplicationHelper
def provider_sign_in_link(provider)
label = Devise.omniauth_configs[provider]&.strategy&.display_name.presence || I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize)
link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
link_to label, omniauth_authorize_path(:user, provider), class: "btn button-#{provider}", method: :post
end
def locale_direction
@@ -102,6 +102,16 @@ module ApplicationHelper
policy(record).public_send(:"#{action}?")
end
def conditional_link_to(condition, name, options = {}, html_options = {}, &block)
if condition && !current_page?(block_given? ? name : options)
link_to(name, options, html_options, &block)
elsif block_given?
content_tag(:span, options, html_options, &block)
else
content_tag(:span, name, html_options)
end
end
def material_symbol(icon, attributes = {})
safe_join(
[

View File

@@ -27,6 +27,12 @@ module ContextHelper
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
quotes: {
'quote' => 'https://w3id.org/fep/044f#quote',
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
'_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
},
interaction_policies: {
'gts' => 'https://gotosocial.org/ns#',
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },

View File

@@ -101,12 +101,17 @@ export const ensureComposeIsVisible = (getState) => {
};
export function setComposeToStatus(status, text, spoiler_text, content_type) {
return{
type: COMPOSE_SET_STATUS,
status,
text,
spoiler_text,
content_type,
return (dispatch, getState) => {
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
dispatch({
type: COMPOSE_SET_STATUS,
status,
text,
spoiler_text,
content_type,
maxOptions
});
};
}
@@ -193,8 +198,9 @@ export function directCompose(account) {
/**
* @param {null | string} overridePrivacy
* @param {undefined | Function} successCallback
*/
export function submitCompose(overridePrivacy = null) {
export function submitCompose(overridePrivacy = null, successCallback = undefined) {
return function (dispatch, getState) {
let status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
@@ -259,6 +265,9 @@ export function submitCompose(overridePrivacy = null) {
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
if (typeof successCallback === 'function') {
successCallback(response.data);
}
// To make the app more responsive, immediately push the status
// into the columns

View File

@@ -21,6 +21,15 @@ export function normalizeFilterResult(result) {
return normalResult;
}
function stripQuoteFallback(text) {
const wrapper = document.createElement('div');
wrapper.innerHTML = text;
wrapper.querySelector('.quote-inline')?.remove();
return wrapper.innerHTML;
}
export function normalizeStatus(status, normalOldStatus, settings) {
const normalStatus = { ...status };
@@ -78,6 +87,11 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
if (normalStatus.quote) {
normalStatus.contentHtml = stripQuoteFallback(normalStatus.contentHtml);
}
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
normalStatus.url = null;
}
@@ -117,6 +131,11 @@ export function normalizeStatusTranslation(translation, status) {
spoiler_text: translation.spoiler_text,
};
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
if (status.get('quote')) {
normalTranslation.contentHtml = stripQuoteFallback(normalTranslation.contentHtml);
}
return normalTranslation;
}

View File

@@ -3,7 +3,7 @@ import { browserHistory } from 'flavours/glitch/components/router';
import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { importFetchedStatus, importFetchedAccount } from './importer';
import { fetchContext } from './statuses_typed';
import { deleteFromTimelines } from './timelines';
@@ -48,7 +48,18 @@ export function fetchStatusRequest(id, skipLoading) {
};
}
export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
/**
* @param {string} id
* @param {Object} [options]
* @param {boolean} [options.forceFetch]
* @param {boolean} [options.alsoFetchContext]
* @param {string | null | undefined} [options.parentQuotePostId]
*/
export function fetchStatus(id, {
forceFetch = false,
alsoFetchContext = true,
parentQuotePostId,
} = {}) {
return (dispatch, getState) => {
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
@@ -66,7 +77,7 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(skipLoading));
}).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading));
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
});
};
}
@@ -78,22 +89,28 @@ export function fetchStatusSuccess(skipLoading) {
};
}
export function fetchStatusFail(id, error, skipLoading) {
export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
return {
type: STATUS_FETCH_FAIL,
id,
error,
parentQuotePostId,
skipLoading,
skipAlert: true,
};
}
export function redraft(status, raw_text, content_type) {
return {
type: REDRAFT,
status,
raw_text,
content_type,
return (dispatch, getState) => {
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
dispatch({
type: REDRAFT,
status,
raw_text,
content_type,
maxOptions,
});
};
}

View File

@@ -1,12 +1,30 @@
import { createAction } from '@reduxjs/toolkit';
import {
apiGetTag,
apiFollowTag,
apiUnfollowTag,
apiFeatureTag,
apiUnfeatureTag,
apiGetFollowedTags,
} from 'flavours/glitch/api/tags';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
export const fetchFollowedHashtags = createDataLoadingThunk(
'tags/fetch-followed',
async ({ next }: { next?: string } = {}) => {
const response = await apiGetFollowedTags(next);
return {
...response,
replace: !next,
};
},
);
export const markFollowedHashtagsStale = createAction(
'tags/mark-followed-stale',
);
export const fetchHashtag = createDataLoadingThunk(
'tags/fetch',
({ tagId }: { tagId: string }) => apiGetTag(tagId),
@@ -15,6 +33,9 @@ export const fetchHashtag = createDataLoadingThunk(
export const followHashtag = createDataLoadingThunk(
'tags/follow',
({ tagId }: { tagId: string }) => apiFollowTag(tagId),
(_, { dispatch }) => {
void dispatch(markFollowedHashtagsStale());
},
);
export const unfollowHashtag = createDataLoadingThunk(

View File

@@ -1,10 +1,11 @@
import { apiRequestPost } from 'flavours/glitch/api';
import type { Status, StatusVisibility } from 'flavours/glitch/models/status';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
import type { StatusVisibility } from 'flavours/glitch/models/status';
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
apiRequestPost<{ reblog: ApiStatusJSON }>(`v1/statuses/${statusId}/reblog`, {
visibility,
});
export const apiUnreblog = (statusId: string) =>
apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
apiRequestPost<ApiStatusJSON>(`v1/statuses/${statusId}/unreblog`);

View File

@@ -1,12 +1,30 @@
import { useCallback } from 'react';
import { useLinks } from 'flavours/glitch/hooks/useLinks';
export const AccountBio: React.FC<{
interface AccountBioProps {
note: string;
className: string;
}> = ({ note, className }) => {
const handleClick = useLinks();
dropdownAccountId?: string;
}
if (note.length === 0 || note === '<p></p>') {
export const AccountBio: React.FC<AccountBioProps> = ({
note,
className,
dropdownAccountId,
}) => {
const handleClick = useLinks(!!dropdownAccountId);
const handleNodeChange = useCallback(
(node: HTMLDivElement | null) => {
if (!dropdownAccountId || !node || node.childNodes.length === 0) {
return;
}
addDropdownToHashtags(node, dropdownAccountId);
},
[dropdownAccountId],
);
if (note.length === 0) {
return null;
}
@@ -15,6 +33,28 @@ export const AccountBio: React.FC<{
className={`${className} translate`}
dangerouslySetInnerHTML={{ __html: note }}
onClickCapture={handleClick}
ref={handleNodeChange}
/>
);
};
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
if (!node) {
return;
}
for (const childNode of node.childNodes) {
if (!(childNode instanceof HTMLElement)) {
continue;
}
if (
childNode instanceof HTMLAnchorElement &&
(childNode.classList.contains('hashtag') ||
childNode.innerText.startsWith('#')) &&
!childNode.dataset.menuHashtag
) {
childNode.dataset.menuHashtag = accountId;
} else if (childNode.childNodes.length > 0) {
addDropdownToHashtags(childNode, accountId);
}
}
}

View File

@@ -33,6 +33,7 @@ export const AltTextBadge: React.FC<{
return (
<>
<button
type='button'
ref={anchorRef}
className='media-gallery__alt__label'
onClick={handleClick}

View File

@@ -162,6 +162,14 @@ const AutosuggestTextarea = forwardRef(({
}
}, [suggestions, textareaRef, setSuggestionsHidden]);
// Hack to force Firefox to change language in autocorrect
useEffect(() => {
if (lang && textareaRef.current && textareaRef.current === document.activeElement) {
textareaRef.current.blur();
textareaRef.current.focus();
}
}, [lang]);
const renderSuggestion = (suggestion, i) => {
let inner, key;

View File

@@ -3,12 +3,15 @@ import { useCallback } from 'react';
import classNames from 'classnames';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
interface BaseProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
block?: boolean;
secondary?: boolean;
compact?: boolean;
dangerous?: boolean;
loading?: boolean;
}
interface PropsChildren extends PropsWithChildren<BaseProps> {
@@ -22,6 +25,10 @@ interface PropsWithText extends BaseProps {
type Props = PropsWithText | PropsChildren;
/**
* Primary UI component for user interaction that doesn't result in navigation.
*/
export const Button: React.FC<Props> = ({
type = 'button',
onClick,
@@ -30,6 +37,7 @@ export const Button: React.FC<Props> = ({
secondary,
compact,
dangerous,
loading,
className,
title,
text,
@@ -38,13 +46,18 @@ export const Button: React.FC<Props> = ({
}) => {
const handleClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(e) => {
if (!disabled && onClick) {
if (disabled || loading) {
e.stopPropagation();
e.preventDefault();
} else if (onClick) {
onClick(e);
}
},
[disabled, onClick],
[disabled, loading, onClick],
);
const label = text ?? children;
return (
<button
className={classNames('button', className, {
@@ -52,14 +65,27 @@ export const Button: React.FC<Props> = ({
'button--compact': compact,
'button--block': block,
'button--dangerous': dangerous,
loading,
})}
disabled={disabled}
// Disabled buttons can't have focus, so we don't really
// disable the button during loading
disabled={disabled && !loading}
aria-disabled={loading}
// If the loading prop is used, announce label changes
aria-live={loading !== undefined ? 'polite' : undefined}
onClick={handleClick}
title={title}
type={type}
{...props}
>
{text ?? children}
{loading ? (
<>
<span className='button__label-wrapper'>{label}</span>
<LoadingIndicator role='none' />
</>
) : (
label
)}
</button>
);
};

View File

@@ -18,7 +18,7 @@ import { useIdentity } from 'flavours/glitch/identity_context';
import { useAppHistory } from './router';
const messages = defineMessages({
export const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: {

View File

@@ -5,6 +5,7 @@ import {
useCallback,
cloneElement,
Children,
useId,
} from 'react';
import classNames from 'classnames';
@@ -16,6 +17,7 @@ import Overlay from 'react-overlays/Overlay';
import type {
OffsetValue,
UsePopperOptions,
Placement,
} from 'react-overlays/esm/usePopper';
import { fetchRelationships } from 'flavours/glitch/actions/accounts';
@@ -295,6 +297,11 @@ interface DropdownProps<Item = MenuItem> {
title?: string;
disabled?: boolean;
scrollable?: boolean;
placement?: Placement;
/**
* Prevent the `ScrollableList` with this scrollKey
* from being scrolled while the dropdown is open
*/
scrollKey?: string;
status?: ImmutableMap<string, unknown>;
forceDropdown?: boolean;
@@ -316,6 +323,7 @@ export const Dropdown = <Item = MenuItem,>({
title = 'Menu',
disabled,
scrollable,
placement = 'bottom',
status,
forceDropdown = false,
renderItem,
@@ -331,16 +339,15 @@ export const Dropdown = <Item = MenuItem,>({
);
const [currentId] = useState(id++);
const open = currentId === openDropdownId;
const activeElement = useRef<HTMLElement | null>(null);
const targetRef = useRef<HTMLButtonElement | null>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const menuId = useId();
const prefetchAccountId = status
? status.getIn(['account', 'id'])
: undefined;
const handleClose = useCallback(() => {
if (activeElement.current) {
activeElement.current.focus({ preventScroll: true });
activeElement.current = null;
if (buttonRef.current) {
buttonRef.current.focus({ preventScroll: true });
}
dispatch(
@@ -375,7 +382,7 @@ export const Dropdown = <Item = MenuItem,>({
[handleClose, onItemClick, items],
);
const handleClick = useCallback(
const toggleDropdown = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const { type } = e;
@@ -423,38 +430,6 @@ export const Dropdown = <Item = MenuItem,>({
],
);
const handleMouseDown = useCallback(() => {
if (!open && document.activeElement instanceof HTMLElement) {
activeElement.current = document.activeElement;
}
}, [open]);
const handleButtonKeyDown = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case ' ':
case 'Enter':
handleMouseDown();
break;
}
},
[handleMouseDown],
);
const handleKeyPress = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case ' ':
case 'Enter':
handleClick(e);
e.stopPropagation();
e.preventDefault();
break;
}
},
[handleClick],
);
useEffect(() => {
return () => {
if (currentId === openDropdownId) {
@@ -465,14 +440,16 @@ export const Dropdown = <Item = MenuItem,>({
let button: React.ReactElement;
const buttonProps = {
disabled,
onClick: toggleDropdown,
'aria-expanded': open,
'aria-controls': menuId,
ref: buttonRef,
};
if (children) {
button = cloneElement(Children.only(children), {
onClick: handleClick,
onMouseDown: handleMouseDown,
onKeyDown: handleButtonKeyDown,
onKeyPress: handleKeyPress,
ref: targetRef,
});
button = cloneElement(Children.only(children), buttonProps);
} else if (icon && iconComponent) {
button = (
<IconButton
@@ -480,12 +457,7 @@ export const Dropdown = <Item = MenuItem,>({
iconComponent={iconComponent}
title={title}
active={open}
disabled={disabled}
onClick={handleClick}
onMouseDown={handleMouseDown}
onKeyDown={handleButtonKeyDown}
onKeyPress={handleKeyPress}
ref={targetRef}
{...buttonProps}
/>
);
} else {
@@ -499,13 +471,13 @@ export const Dropdown = <Item = MenuItem,>({
<Overlay
show={open}
offset={offset}
placement='bottom'
placement={placement}
flip
target={targetRef}
target={buttonRef}
popperConfig={popperConfig}
>
{({ props, arrowProps, placement }) => (
<div {...props}>
<div {...props} id={menuId}>
<div className={`dropdown-animation dropdown-menu ${placement}`}>
<div
className={`dropdown-menu__arrow ${placement}`}

View File

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

View File

@@ -13,14 +13,13 @@ interface Props extends React.SVGProps<SVGSVGElement> {
children?: never;
id: string;
icon: IconProp;
title?: string;
}
export const Icon: React.FC<Props> = ({
id,
icon: IconComponent,
className,
title: titleProp,
'aria-label': ariaLabel,
...other
}) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -34,18 +33,19 @@ export const Icon: React.FC<Props> = ({
IconComponent = CheckBoxOutlineBlankIcon;
}
const ariaHidden = titleProp ? undefined : true;
const ariaHidden = ariaLabel ? undefined : true;
const role = !ariaHidden ? 'img' : undefined;
// Set the title to an empty string to remove the built-in SVG one if any
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const title = titleProp || '';
const title = ariaLabel || '';
return (
<IconComponent
className={classNames('icon', `icon-${id}`, className)}
title={title}
aria-hidden={ariaHidden}
aria-label={ariaLabel}
role={role}
{...other}
/>

View File

@@ -14,7 +14,6 @@ interface Props {
onClick?: React.MouseEventHandler<HTMLButtonElement>;
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
active?: boolean;
expanded?: boolean;
style?: React.CSSProperties;
@@ -47,7 +46,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
activeStyle,
onClick,
onKeyDown,
onKeyPress,
onMouseDown,
active = false,
disabled = false,
@@ -89,16 +87,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
[disabled, onClick],
);
const handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> =
useCallback(
(e) => {
if (!disabled) {
onKeyPress?.(e);
}
},
[disabled, onKeyPress],
);
const handleMouseDown: React.MouseEventHandler<HTMLButtonElement> =
useCallback(
(e) => {
@@ -166,7 +154,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
onClick={handleClick}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPress} // eslint-disable-line @typescript-eslint/no-deprecated
style={buttonStyle}
tabIndex={tabIndex}
disabled={disabled}

View File

@@ -6,15 +6,34 @@ const messages = defineMessages({
loading: { id: 'loading_indicator.label', defaultMessage: 'Loading…' },
});
export const LoadingIndicator: React.FC = () => {
interface LoadingIndicatorProps {
/**
* Use role='none' to opt out of the current default role 'progressbar'
* and aria attributes which we should re-visit to check if they're appropriate.
* In Firefox the aria-label is not applied, instead an implied value of `50` is
* used as the label.
*/
role?: string;
}
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
role = 'progressbar',
}) => {
const intl = useIntl();
const a11yProps =
role === 'progressbar'
? ({
role,
'aria-busy': true,
'aria-live': 'polite',
} as const)
: undefined;
return (
<div
className='loading-indicator'
role='progressbar'
aria-busy
aria-live='polite'
{...a11yProps}
aria-label={intl.formatMessage(messages.loading)}
>
<CircularProgress size={50} strokeWidth={6} />

View File

@@ -48,7 +48,7 @@ export const MediaIcon: React.FC<{
className={className}
id={icon}
icon={iconComponents[icon]}
title={intl.formatMessage(messages[icon])}
aria-label={intl.formatMessage(messages[icon])}
aria-hidden='true'
/>
);

View File

@@ -318,7 +318,7 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
id='check'
icon={CheckIcon}
className='poll__voted__mark'
title={intl.formatMessage(messages.voted)}
aria-label={intl.formatMessage(messages.voted)}
/>
</span>
)}

View File

@@ -377,7 +377,11 @@ class Status extends ImmutablePureComponent {
if (newTab) {
window.open(path, '_blank', 'noopener');
} else {
history.push(path);
if (history.location.pathname.replace('/deck/', '/') === path) {
history.replace(path);
} else {
history.push(path);
}
}
};

View File

@@ -8,6 +8,10 @@ export enum BannerVariant {
Filter = 'filter',
}
const stopPropagation: MouseEventHandler = (e) => {
e.stopPropagation();
};
export const StatusBanner: React.FC<{
children: React.ReactNode;
variant: BannerVariant;
@@ -38,6 +42,7 @@ export const StatusBanner: React.FC<{
: 'content-warning content-warning--filter'
}
onClick={forwardClick}
onMouseUp={stopPropagation}
>
<p id={descriptionId}>{children}</p>

View File

@@ -190,7 +190,7 @@ class StatusContent extends PureComponent {
link.classList.add('unhandled-link');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener nofollow noreferrer');
link.setAttribute('rel', 'noopener nofollow');
try {
if (tagLinks && isLinkMisleading(link)) {

View File

@@ -61,16 +61,14 @@ class StatusIcons extends PureComponent {
className='status__reply-icon'
id='comment'
icon={ForumIcon}
aria-hidden='true'
title={intl.formatMessage(messages.inReplyTo)}
aria-label={intl.formatMessage(messages.inReplyTo)}
/>
) : null}
{settings.get('local_only') && status.get('local_only') &&
<Icon
id='home'
icon={HomeIcon}
aria-hidden='true'
title={intl.formatMessage(messages.localOnly)}
aria-label={intl.formatMessage(messages.localOnly)}
/>}
{settings.get('media') && !!mediaIcons && mediaIcons.map(icon => (<MediaIcon key={`media-icon--${icon}`} className='status__media-icon' icon={icon} />))}
{settings.get('visibility') && <VisibilityIcon visibility={status.get('visibility')} />}

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -12,12 +12,15 @@ import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import StatusContainer from 'flavours/glitch/containers/status_container';
import { domain } from 'flavours/glitch/initial_state';
import type { Status } from 'flavours/glitch/models/status';
import type { RootState } from 'flavours/glitch/store';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { revealAccount } from '../actions/accounts_typed';
import { fetchStatus } from '../actions/statuses';
import { makeGetStatus } from '../selectors';
import { getAccountHidden } from '../selectors/accounts';
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
@@ -37,9 +40,7 @@ const QuoteWrapper: React.FC<{
);
};
const NestedQuoteLink: React.FC<{
status: Status;
}> = ({ status }) => {
const NestedQuoteLink: React.FC<{ status: Status }> = ({ status }) => {
const accountId = status.get('account') as string;
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
@@ -75,24 +76,74 @@ type GetStatusSelector = (
props: { id?: string | null; contextType?: string },
) => Status | null;
const LimitedAccountHint: React.FC<{ accountId: string }> = ({ accountId }) => {
const dispatch = useAppDispatch();
const reveal = useCallback(() => {
dispatch(revealAccount({ id: accountId }));
}, [dispatch, accountId]);
return (
<>
<FormattedMessage
id='status.quote_error.limited_account_hint.title'
defaultMessage='This account has been hidden by the moderators of {domain}.'
values={{ domain }}
/>
<button onClick={reveal} className='link-button'>
<FormattedMessage
id='status.quote_error.limited_account_hint.action'
defaultMessage='Show anyway'
/>
</button>
</>
);
};
export const QuotedStatus: React.FC<{
quote: QuoteMap;
contextType?: string;
parentQuotePostId?: string | null;
variant?: 'full' | 'link';
nestingLevel?: number;
}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => {
}> = ({
quote,
contextType,
parentQuotePostId,
nestingLevel = 1,
variant = 'full',
}) => {
const dispatch = useAppDispatch();
const quoteState = useAppSelector((state) =>
parentQuotePostId
? state.statuses.getIn([parentQuotePostId, 'quote', 'state'])
: quote.get('state'),
);
const quotedStatusId = quote.get('quoted_status');
const quoteState = quote.get('state');
const status = useAppSelector((state) =>
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
);
const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
const accountId: string | null = status?.get('account', null) as
| string
| null;
const hiddenAccount = useAppSelector(
(state) => accountId && getAccountHidden(state, accountId),
);
useEffect(() => {
if (!status && quotedStatusId) {
dispatch(fetchStatus(quotedStatusId));
if (shouldLoadQuote && quotedStatusId) {
dispatch(
fetchStatus(quotedStatusId, {
parentQuotePostId,
alsoFetchContext: false,
}),
);
}
}, [status, quotedStatusId, dispatch]);
}, [shouldLoadQuote, quotedStatusId, parentQuotePostId, dispatch]);
// In order to find out whether the quoted post should be completely hidden
// due to a matching filter, we run it through the selector used by `status_container`.
@@ -147,6 +198,8 @@ export const QuotedStatus: React.FC<{
defaultMessage='This post cannot be displayed.'
/>
);
} else if (hiddenAccount && accountId) {
quoteError = <LimitedAccountHint accountId={accountId} />;
}
if (quoteError) {
@@ -173,6 +226,7 @@ export const QuotedStatus: React.FC<{
{canRenderChildQuote && (
<QuotedStatus
quote={childQuote}
parentQuotePostId={quotedStatusId}
contextType={contextType}
variant={
nestingLevel === MAX_QUOTE_POSTS_NESTING_LEVEL ? 'link' : 'full'
@@ -209,7 +263,11 @@ export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
return (
/* @ts-expect-error Status is not yet typed */
<StatusContainer {...props}>
<QuotedStatus quote={quote} contextType={props.contextType} />
<QuotedStatus
quote={quote}
parentQuotePostId={status?.get('id') as string}
contextType={props.contextType}
/>
</StatusContainer>
);
}

View File

@@ -58,7 +58,7 @@ export const VisibilityIcon: React.FC<{ visibility: StatusVisibility }> = ({
<Icon
id={visibilityIcon.icon}
icon={visibilityIcon.iconComponent}
title={visibilityIcon.text}
aria-label={visibilityIcon.text}
className={'status__visibility-icon'}
/>
);

View File

@@ -19,6 +19,7 @@ import initialState, { title as siteTitle } from 'flavours/glitch/initial_state'
import { IntlProvider } from 'flavours/glitch/locales';
import { store } from 'flavours/glitch/store';
import { isProduction } from 'flavours/glitch/utils/environment';
import { BodyScrollLock } from 'flavours/glitch/features/ui/components/body_scroll_lock';
const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`;
@@ -63,6 +64,7 @@ export default class Mastodon extends PureComponent {
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
<Route path='/' component={UI} />
</ScrollContext>
<BodyScrollLock />
</Router>
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />

View File

@@ -14,7 +14,6 @@ import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
import { Video } from 'flavours/glitch/features/video';
import { IntlProvider } from 'flavours/glitch/locales';
import { createPollFromServerJSON } from 'flavours/glitch/models/poll';
import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar';
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
@@ -34,9 +33,6 @@ export default class MediaContainer extends PureComponent {
};
handleOpenMedia = (media, index, lang) => {
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media, index, lang });
};
@@ -45,16 +41,10 @@ export default class MediaContainer extends PureComponent {
const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
const mediaList = fromJS(media);
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media: mediaList, lang, options });
};
handleCloseMedia = () => {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = '0';
this.setState({
media: null,
index: null,

View File

@@ -6,6 +6,7 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
import { AccountBio } from '@/flavours/glitch/components/account_bio';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@@ -772,12 +773,11 @@ export const AccountHeader: React.FC<{
<Icon
id='lock'
icon={LockIcon}
title={intl.formatMessage(messages.account_locked)}
aria-label={intl.formatMessage(messages.account_locked)}
/>
);
}
const content = { __html: account.note_emojified };
const displayNameHtml = { __html: account.display_name_html };
const fields = account.fields;
const isLocal = !account.acct.includes('@');
@@ -901,12 +901,11 @@ export const AccountHeader: React.FC<{
<AccountNote accountId={accountId} />
)}
{account.note.length > 0 && account.note !== '<p></p>' && (
<div
className='account__header__content translate'
dangerouslySetInnerHTML={content}
/>
)}
<AccountBio
note={account.note_emojified}
dropdownAccountId={accountId}
className='account__header__content'
/>
<div className='account__header__fields'>
<dl>

View File

@@ -261,7 +261,9 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
);
const lang = useAppSelector(
(state) =>
(state.compose as ImmutableMap<string, unknown>).get('lang') as string,
(state.compose as ImmutableMap<string, unknown>).get(
'language',
) as string,
);
const focusX =
(media?.getIn(['meta', 'focus', 'x'], 0) as number | undefined) ?? 0;

View File

@@ -12,9 +12,10 @@ import { length } from 'stringz';
import { missingAltTextModal } from 'flavours/glitch/initial_state';
import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import { Button } from '../../../components/button';
import AutosuggestInput from 'flavours/glitch/components/autosuggest_input';
import AutosuggestTextarea from 'flavours/glitch/components/autosuggest_textarea';
import { Button } from 'flavours/glitch/components/button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollButtonContainer from '../containers/poll_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
@@ -80,6 +81,7 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool,
lang: PropTypes.string,
maxChars: PropTypes.number,
redirectOnSuccess: PropTypes.bool,
};
static defaultProps = {
@@ -242,9 +244,8 @@ class ComposeForm extends ImmutablePureComponent {
};
render () {
const { intl, onPaste, autoFocus, withoutNavigation, maxChars } = this.props;
const { intl, onPaste, autoFocus, withoutNavigation, maxChars, isSubmitting } = this.props;
const { highlighted } = this.state;
const disabled = this.props.isSubmitting;
return (
<form className='compose-form' onSubmit={this.handleSubmit}>
@@ -263,7 +264,7 @@ class ComposeForm extends ImmutablePureComponent {
<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
disabled={disabled}
disabled={isSubmitting}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
ref={this.setSpoilerText}
@@ -285,7 +286,7 @@ class ComposeForm extends ImmutablePureComponent {
<AutosuggestTextarea
ref={this.textareaRef}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
disabled={isSubmitting}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
@@ -331,9 +332,15 @@ class ComposeForm extends ImmutablePureComponent {
<Button
type='submit'
compact
text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
disabled={!this.canSubmit()}
/>
loading={isSubmitting}
>
{intl.formatMessage(
this.props.isEditing ?
messages.saveChanges :
(this.props.isInReply ? messages.reply : messages.publish)
)}
</Button>
</div>
</div>
</div>

View File

@@ -29,6 +29,7 @@ import { HASHTAG_REGEX } from 'flavours/glitch/utils/hashtags';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
clearSearch: { id: 'search.clear', defaultMessage: 'Clear search' },
placeholderSignedIn: {
id: 'search.search_or_paste',
defaultMessage: 'Search or paste URL',
@@ -46,8 +47,32 @@ const labelForRecentSearch = (search: RecentSearch) => {
}
};
const unfocus = () => {
document.querySelector('.ui')?.parentElement?.focus();
const ClearButton: React.FC<{
onClick: () => void;
hasValue: boolean;
}> = ({ onClick, hasValue }) => {
const intl = useIntl();
return (
<div
className={classNames('search__icon-wrapper', { 'has-value': hasValue })}
>
<Icon id='search' icon={SearchIcon} className='search__icon' />
<button
type='button'
onClick={onClick}
className='search__icon search__icon--clear-button'
tabIndex={hasValue ? undefined : -1}
aria-hidden={!hasValue}
>
<Icon
id='times-circle'
icon={CancelIcon}
aria-label={intl.formatMessage(messages.clearSearch)}
/>
</button>
</div>
);
};
interface SearchOption {
@@ -78,6 +103,11 @@ export const Search: React.FC<{
}, [initialValue]);
const searchOptions: SearchOption[] = [];
const unfocus = useCallback(() => {
document.querySelector('.ui')?.parentElement?.focus();
setExpanded(false);
}, []);
if (searchEnabled) {
searchOptions.push(
{
@@ -253,7 +283,7 @@ export const Search: React.FC<{
history.push({ pathname: '/search', search: queryParams.toString() });
unfocus();
},
[dispatch, history],
[dispatch, history, unfocus],
);
const handleChange = useCallback(
@@ -373,14 +403,15 @@ export const Search: React.FC<{
setQuickActions(newQuickActions);
},
[dispatch, history, signedIn, setValue, setQuickActions, submit],
[signedIn, dispatch, unfocus, history, submit],
);
const handleClear = useCallback(() => {
setValue('');
setQuickActions([]);
setSelectedOption(-1);
}, [setValue, setQuickActions, setSelectedOption]);
unfocus();
}, [unfocus]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -431,7 +462,7 @@ export const Search: React.FC<{
break;
}
},
[navigableOptions, value, selectedOption, setSelectedOption, submit],
[unfocus, navigableOptions, selectedOption, submit, value],
);
const handleFocus = useCallback(() => {
@@ -451,12 +482,38 @@ export const Search: React.FC<{
}, [setExpanded, setSelectedOption, singleColumn]);
const handleBlur = useCallback(() => {
setExpanded(false);
setSelectedOption(-1);
}, [setExpanded, setSelectedOption]);
}, [setSelectedOption]);
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
// If the search popover is expanded, close it when tabbing or
// clicking outside of it or the search form, while allowing
// tabbing or clicking inside of the popover
if (expanded) {
function closeOnLeave(event: FocusEvent | MouseEvent) {
const form = formRef.current;
const isClickInsideForm =
form &&
(form === event.target || form.contains(event.target as Node));
if (!isClickInsideForm) {
setExpanded(false);
}
}
document.addEventListener('focusin', closeOnLeave);
document.addEventListener('click', closeOnLeave);
return () => {
document.removeEventListener('focusin', closeOnLeave);
document.removeEventListener('click', closeOnLeave);
};
}
return () => null;
}, [expanded]);
return (
<form className={classNames('search', { active: expanded })}>
<form ref={formRef} className={classNames('search', { active: expanded })}>
<input
ref={searchInputRef}
className='search__input'
@@ -474,21 +531,9 @@ export const Search: React.FC<{
onBlur={handleBlur}
/>
<button type='button' className='search__icon' onClick={handleClear}>
<Icon
id='search'
icon={SearchIcon}
className={hasValue ? '' : 'active'}
/>
<Icon
id='times-circle'
icon={CancelIcon}
className={hasValue ? 'active' : ''}
aria-label={intl.formatMessage(messages.placeholder)}
/>
</button>
<ClearButton hasValue={hasValue} onClick={handleClear} />
<div className='search__popout'>
<div className='search__popout' tabIndex={-1}>
{!hasValue && (
<>
<h4>

View File

@@ -56,7 +56,7 @@ const mapStateToProps = state => ({
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
});
const mapDispatchToProps = (dispatch) => ({
const mapDispatchToProps = (dispatch, props) => ({
onChange (text) {
dispatch(changeCompose(text));
@@ -69,7 +69,11 @@ const mapDispatchToProps = (dispatch) => ({
modalProps: { overridePrivacy },
}));
} else {
dispatch(submitCompose(overridePrivacy));
dispatch(submitCompose(overridePrivacy, (status) => {
if (props.redirectOnSuccess) {
window.location.assign(status.url);
}
}));
}
},

View File

@@ -24,33 +24,28 @@ import { Icon } from 'flavours/glitch/components/icon';
import glitchedElephant1 from 'flavours/glitch/images/mbstobon-ui-0.png';
import glitchedElephant2 from 'flavours/glitch/images/mbstobon-ui-1.png';
import glitchedElephant3 from 'flavours/glitch/images/mbstobon-ui-2.png';
import { mascot } from 'flavours/glitch/initial_state';
import { mascot, reduceMotion } from 'flavours/glitch/initial_state';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { messages as navbarMessages } from '../ui/components/navigation_bar';
import { Search } from './components/search';
import ComposeFormContainer from './containers/compose_form_container';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: {
id: 'tabs_bar.notifications',
defaultMessage: 'Notifications',
live_feed_public: {
id: 'navigation_bar.live_feed_public',
defaultMessage: 'Live feed (public)',
},
public: {
id: 'navigation_bar.public_timeline',
defaultMessage: 'Federated timeline',
},
community: {
id: 'navigation_bar.community_timeline',
defaultMessage: 'Local timeline',
live_feed_local: {
id: 'navigation_bar.live_feed_local',
defaultMessage: 'Live feed (local)',
},
settings: {
id: 'navigation_bar.app_settings',
defaultMessage: 'App settings',
},
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
});
type ColumnMap = ImmutableMap<'id' | 'uuid' | 'params', string>;
@@ -127,19 +122,27 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
elephantUIPlane,
][elefriend];
const scrollNavbarIntoView = useCallback(() => {
const navbar = document.querySelector('.navigation-panel');
navbar?.scrollIntoView({
behavior: reduceMotion ? 'auto' : 'smooth',
});
}, []);
if (multiColumn) {
return (
<div
className='drawer'
role='region'
aria-label={intl.formatMessage(messages.compose)}
aria-label={intl.formatMessage(navbarMessages.publish)}
>
<nav className='drawer__header'>
<Link
to='/getting-started'
className='drawer__tab'
title={intl.formatMessage(messages.start)}
aria-label={intl.formatMessage(messages.start)}
title={intl.formatMessage(navbarMessages.menu)}
aria-label={intl.formatMessage(navbarMessages.menu)}
onClick={scrollNavbarIntoView}
>
<Icon id='bars' icon={MenuIcon} />
</Link>
@@ -147,8 +150,8 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<Link
to='/home'
className='drawer__tab'
title={intl.formatMessage(messages.home_timeline)}
aria-label={intl.formatMessage(messages.home_timeline)}
title={intl.formatMessage(navbarMessages.home)}
aria-label={intl.formatMessage(navbarMessages.home)}
>
<Icon id='home' icon={HomeIcon} />
</Link>
@@ -157,8 +160,8 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<Link
to='/notifications'
className='drawer__tab'
title={intl.formatMessage(messages.notifications)}
aria-label={intl.formatMessage(messages.notifications)}
title={intl.formatMessage(navbarMessages.notifications)}
aria-label={intl.formatMessage(navbarMessages.notifications)}
>
<span className='icon-badge-wrapper'>
<Icon id='bell' icon={NotificationsIcon} />
@@ -172,8 +175,8 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<Link
to='/public/local'
className='drawer__tab'
title={intl.formatMessage(messages.community)}
aria-label={intl.formatMessage(messages.community)}
title={intl.formatMessage(messages.live_feed_local)}
aria-label={intl.formatMessage(messages.live_feed_local)}
>
<Icon id='users' icon={PeopleIcon} />
</Link>
@@ -182,8 +185,8 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<Link
to='/public'
className='drawer__tab'
title={intl.formatMessage(messages.public)}
aria-label={intl.formatMessage(messages.public)}
title={intl.formatMessage(messages.live_feed_public)}
aria-label={intl.formatMessage(messages.live_feed_public)}
>
<Icon id='globe' icon={PublicIcon} />
</Link>
@@ -230,12 +233,12 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.compose)}
label={intl.formatMessage(navbarMessages.publish)}
>
<ColumnHeader
icon='pencil'
iconComponent={EditIcon}
title={intl.formatMessage(messages.compose)}
title={intl.formatMessage(navbarMessages.publish)}
multiColumn={multiColumn}
showBackButton
/>

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useEffect, useCallback, useRef } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@@ -7,8 +7,10 @@ import { Helmet } from 'react-helmet';
import { isFulfilled } from '@reduxjs/toolkit';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { unfollowHashtag } from 'flavours/glitch/actions/tags_typed';
import { apiGetFollowedTags } from 'flavours/glitch/api/tags';
import {
fetchFollowedHashtags,
unfollowHashtag,
} from 'flavours/glitch/actions/tags_typed';
import type { ApiHashtagJSON } from 'flavours/glitch/api_types/tags';
import { Button } from 'flavours/glitch/components/button';
import { Column } from 'flavours/glitch/components/column';
@@ -16,7 +18,7 @@ import type { ColumnRef } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { Hashtag } from 'flavours/glitch/components/hashtag';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import { useAppDispatch } from 'flavours/glitch/store';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
const messages = defineMessages({
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
@@ -59,55 +61,32 @@ const FollowedTag: React.FC<{
const FollowedTags: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const intl = useIntl();
const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
const [loading, setLoading] = useState(false);
const [next, setNext] = useState<string | undefined>();
const dispatch = useAppDispatch();
const { tags, loading, next, stale } = useAppSelector(
(state) => state.followedTags,
);
const hasMore = !!next;
const columnRef = useRef<ColumnRef>(null);
useEffect(() => {
setLoading(true);
void apiGetFollowedTags()
.then(({ tags, links }) => {
const next = links.refs.find((link) => link.rel === 'next');
setTags(tags);
setLoading(false);
setNext(next?.uri);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setTags, setLoading, setNext]);
if (stale) {
void dispatch(fetchFollowedHashtags());
}
}, [dispatch, stale]);
const handleLoadMore = useCallback(() => {
setLoading(true);
void apiGetFollowedTags(next)
.then(({ tags, links }) => {
const next = links.refs.find((link) => link.rel === 'next');
setLoading(false);
setTags((previousTags) => [...previousTags, ...tags]);
setNext(next?.uri);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setTags, setLoading, setNext, next]);
if (next) {
void dispatch(fetchFollowedHashtags({ next }));
}
}, [dispatch, next]);
const handleUnfollow = useCallback(
(tagId: string) => {
setTags((tags) => tags.filter((tag) => tag.name !== tagId));
void dispatch(unfollowHashtag({ tagId }));
},
[setTags],
[dispatch],
);
const columnRef = useRef<ColumnRef>(null);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);

View File

@@ -1,224 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
import AdministrationIcon from '@/material-icons/400-24px/manufacturing.svg?react';
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
import { fetchLists } from 'flavours/glitch/actions/lists';
import { openModal } from 'flavours/glitch/actions/modal';
import Column from 'flavours/glitch/features/ui/components/column';
import { LinkFooter } from 'flavours/glitch/features/ui/components/link_footer';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { canManageReports, canViewAdminDashboard } from 'flavours/glitch/permissions';
import { preferencesLink } from 'flavours/glitch/utils/backend_links';
import { me, showTrends } from '../../initial_state';
import { NavigationBar } from '../compose/components/navigation_bar';
import { ColumnLink } from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
import { Trends } from 'flavours/glitch/features/navigation_panel/components/trends';
const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
explore: { id: 'navigation_bar.explore', defaultMessage: 'Explore' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
administration: { id: 'navigation_bar.administration', defaultMessage: 'Administration' },
moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' },
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' },
misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' },
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
});
const makeMapStateToProps = () => {
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
if (!lists) {
return lists;
}
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
});
const mapStateToProps = state => ({
lists: getOrderedLists(state),
myAccount: state.getIn(['accounts', me]),
columns: state.getIn(['settings', 'columns']),
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
unreadNotifications: state.getIn(['notifications', 'unread']),
});
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
fetchFollowRequests: () => dispatch(fetchFollowRequests()),
fetchLists: () => dispatch(fetchLists()),
openSettings: () => dispatch(openModal({
modalType: 'SETTINGS',
modalProps: {},
})),
});
const badgeDisplay = (number, limit) => {
if (number === 0) {
return undefined;
} else if (limit && number >= limit) {
return `${limit}+`;
} else {
return number;
}
};
class GettingStarted extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.record,
columns: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number,
unreadNotifications: PropTypes.number,
lists: ImmutablePropTypes.list,
fetchLists: PropTypes.func.isRequired,
openSettings: PropTypes.func.isRequired,
};
UNSAFE_componentWillMount () {
this.props.fetchLists();
}
componentDidMount () {
const { fetchFollowRequests } = this.props;
const { signedIn } = this.props.identity;
if (!signedIn) {
return;
}
fetchFollowRequests();
}
render () {
const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings } = this.props;
const { signedIn, permissions } = this.props.identity;
const navItems = [];
let listItems = [];
if (multiColumn) {
if (signedIn && !columns.find(item => item.get('id') === 'HOME')) {
navItems.push(<ColumnLink key='home' icon='home' iconComponent={HomeIcon} text={intl.formatMessage(messages.home_timeline)} to='/home' />);
}
if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) {
navItems.push(<ColumnLink key='notifications' icon='bell' iconComponent={NotificationsIcon} text={intl.formatMessage(messages.notifications)} badge={badgeDisplay(unreadNotifications)} to='/notifications' />);
}
if (!columns.find(item => item.get('id') === 'COMMUNITY')) {
navItems.push(<ColumnLink key='community_timeline' icon='users' iconComponent={PeopleIcon} text={intl.formatMessage(messages.community_timeline)} to='/public/local' />);
}
if (!columns.find(item => item.get('id') === 'PUBLIC')) {
navItems.push(<ColumnLink key='public_timeline' icon='globe' iconComponent={PublicIcon} text={intl.formatMessage(messages.public_timeline)} to='/public' />);
}
}
if (showTrends) {
navItems.push(<ColumnLink key='explore' icon='explore' iconComponent={ExploreIcon} text={intl.formatMessage(messages.explore)} to='/explore' />);
}
if (signedIn) {
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
navItems.push(<ColumnLink key='conversations' icon='envelope' iconComponent={MailIcon} text={intl.formatMessage(messages.direct)} to='/conversations' />);
}
if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) {
navItems.push(<ColumnLink key='bookmarks' icon='bookmark' iconComponent={BookmarksIcon} text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />);
}
if (myAccount.get('locked') || unreadFollowRequests > 0) {
navItems.push(<ColumnLink key='follow_requests' icon='user-plus' iconComponent={PersonAddIcon} text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
}
navItems.push(<ColumnLink key='getting_started' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
listItems = listItems.concat([
<div key='9'>
<ColumnLink key='lists' icon='bars' iconComponent={ListAltIcon} text={intl.formatMessage(messages.lists)} to='/lists' />
{lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list =>
<ColumnLink key={`list-${list.get('id')}`} to={`/lists/${list.get('id')}`} icon='list-ul' iconComponent={ListAltIcon} text={list.get('title')} />,
)}
</div>,
]);
}
return (
<Column bindToDocument={!multiColumn} icon='bars' iconComponent={MenuIcon} heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
<div className='scrollable optionally-scrollable'>
<div className='getting-started__wrapper'>
{!multiColumn && signedIn && <NavigationBar account={myAccount} />}
{multiColumn && <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />}
{navItems}
{signedIn && (
<>
<ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} />
{listItems}
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
{ preferencesLink !== undefined && <ColumnLink icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> }
<ColumnLink icon='cogs' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.settings)} onClick={openSettings} />
{canManageReports(permissions) && <ColumnLink key='moderation' href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
{canViewAdminDashboard(permissions) && <ColumnLink key='administration' href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
</>
)}
</div>
<LinkFooter multiColumn />
</div>
{(multiColumn && showTrends) && <Trends />}
<Helmet>
<title>{intl.formatMessage(messages.menu)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default withIdentity(connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(GettingStarted)));

View File

@@ -0,0 +1,32 @@
import { useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { Column } from 'flavours/glitch/components/column';
import { NavigationPanel } from '../navigation_panel';
import { LinkFooter } from '../ui/components/link_footer';
const GettingStarted: React.FC = () => {
const intl = useIntl();
return (
<Column>
<NavigationPanel multiColumn />
<LinkFooter multiColumn />
<Helmet>
<title>
{intl.formatMessage({
id: 'getting_started.heading',
defaultMessage: 'Getting started',
})}
</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default GettingStarted;

View File

@@ -1,59 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import Column from 'flavours/glitch/features/ui/components/column';
import { ColumnLink } from 'flavours/glitch/features/ui/components/column_link';
import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
const messages = defineMessages({
heading: { id: 'column.heading', defaultMessage: 'Misc' },
subheading: { id: 'column.subheading', defaultMessage: 'Miscellaneous options' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
});
class GettingStartedMisc extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
};
render () {
const { intl } = this.props;
const { signedIn } = this.props.identity;
return (
<Column icon='ellipsis-h' iconComponent={MoreHorizIcon} heading={intl.formatMessage(messages.heading)} alwaysShowBackButton>
<div className='scrollable'>
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
{signedIn && (<ColumnLink key='favourites' icon='star' iconComponent={StarIcon} text={intl.formatMessage(messages.favourites)} to='/favourites' />)}
{signedIn && (<ColumnLink key='pinned' icon='thumb-tack' iconComponent={PushPinIcon} text={intl.formatMessage(messages.pins)} to='/pinned' />)}
{signedIn && (<ColumnLink key='mutes' icon='volume-off' iconComponent={VolumeOffIcon} text={intl.formatMessage(messages.mutes)} to='/mutes' />)}
{signedIn && (<ColumnLink key='blocks' icon='ban' iconComponent={BlockIcon} text={intl.formatMessage(messages.blocks)} to='/blocks' />)}
{signedIn && (<ColumnLink key='domain_blocks' icon='minus-circle' iconComponent={BlockIcon} text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' />)}
<ColumnLink key='shortcuts' icon='question' iconComponent={InfoIcon} text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />
</div>
</Column>
);
}
}
export default connect()(withIdentity(injectIntl(GettingStartedMisc)));

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { apiGetFollowedTags } from 'flavours/glitch/api/tags';
import type { ApiHashtagJSON } from 'flavours/glitch/api_types/tags';
import { fetchFollowedHashtags } from 'flavours/glitch/actions/tags_typed';
import { ColumnLink } from 'flavours/glitch/features/ui/components/column_link';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { CollapsiblePanel } from './collapsible_panel';
@@ -24,25 +24,20 @@ const messages = defineMessages({
},
});
const TAG_LIMIT = 4;
export const FollowedTagsPanel: React.FC = () => {
const intl = useIntl();
const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
const [loading, setLoading] = useState(false);
const dispatch = useAppDispatch();
const { tags, stale, loading } = useAppSelector(
(state) => state.followedTags,
);
useEffect(() => {
setLoading(true);
void apiGetFollowedTags(undefined, 4)
.then(({ tags }) => {
setTags(tags);
setLoading(false);
return '';
})
.catch(() => {
setLoading(false);
});
}, [setLoading, setTags]);
if (stale) {
void dispatch(fetchFollowedHashtags());
}
}, [dispatch, stale]);
return (
<CollapsiblePanel
@@ -54,14 +49,14 @@ export const FollowedTagsPanel: React.FC = () => {
expandTitle={intl.formatMessage(messages.expand)}
loading={loading}
>
{tags.map((tag) => (
{tags.slice(0, TAG_LIMIT).map((tag) => (
<ColumnLink
transparent
icon='hashtag'
key={tag.name}
iconComponent={TagIcon}
text={`#${tag.name}`}
to={`/tags/${tag.name}`}
transparent
/>
))}
</CollapsiblePanel>

View File

@@ -53,16 +53,22 @@ export const MoreLink: React.FC = () => {
const menu = useMemo(() => {
const arr: MenuItem[] = [
{ text: intl.formatMessage(messages.filters), href: '/filters' },
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
{
text: intl.formatMessage(messages.domainBlocks),
to: '/domain_blocks',
href: '/filters',
text: intl.formatMessage(messages.filters),
},
{
to: '/mutes',
text: intl.formatMessage(messages.mutes),
},
{
to: '/blocks',
text: intl.formatMessage(messages.blocks),
},
{
to: '/domain_blocks',
text: intl.formatMessage(messages.domainBlocks),
},
];
arr.push(
null,
{
href: '/settings/privacy',
@@ -80,7 +86,7 @@ export const MoreLink: React.FC = () => {
href: '/settings/export',
text: intl.formatMessage(messages.importExport),
},
);
];
if (canManageReports(permissions)) {
arr.push(null, {
@@ -109,7 +115,7 @@ export const MoreLink: React.FC = () => {
}, [intl, dispatch, permissions]);
return (
<Dropdown items={menu}>
<Dropdown items={menu} placement='bottom-start'>
<button className='column-link column-link--transparent'>
<Icon id='' icon={MoreHorizIcon} className='column-link__icon' />

View File

@@ -203,13 +203,186 @@ const isFirehoseActive = (
const MENU_WIDTH = 284;
export const NavigationPanel: React.FC = () => {
export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
multiColumn = false,
}) => {
const intl = useIntl();
const { signedIn, disabledAccountId } = useIdentity();
const location = useLocation();
const showSearch = useBreakpoint('full') && !multiColumn;
const dispatch = useAppDispatch();
let banner: React.ReactNode;
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>
);
}
const handleOpenSettings = useCallback<MouseEventHandler>(
(e) => {
e.preventDefault();
e.stopPropagation();
dispatch(
openModal({
modalType: 'SETTINGS',
modalProps: {},
}),
);
},
[dispatch],
);
return (
<div className='navigation-panel'>
{showSearch && <Search singleColumn />}
{!multiColumn && <ProfileCard />}
{banner && <div className='navigation-panel__banner'>{banner}</div>}
<div className='navigation-panel__menu'>
{signedIn && (
<>
{!multiColumn && (
<ColumnLink
to='/publish'
icon='plus'
iconComponent={AddIcon}
activeIconComponent={AddIcon}
text={intl.formatMessage(messages.compose)}
className='button navigation-panel__compose-button'
/>
)}
<ColumnLink
transparent
to='/home'
icon='home'
iconComponent={HomeIcon}
activeIconComponent={HomeActiveIcon}
text={intl.formatMessage(messages.home)}
/>
</>
)}
{trendsEnabled && (
<ColumnLink
transparent
to='/explore'
icon='explore'
iconComponent={TrendingUpIcon}
text={intl.formatMessage(messages.explore)}
/>
)}
{(signedIn || timelinePreview) && (
<ColumnLink
transparent
to='/public/local'
icon='globe'
iconComponent={PublicIcon}
isActive={isFirehoseActive}
text={intl.formatMessage(messages.firehose)}
/>
)}
{signedIn && (
<>
<NotificationsLink />
<FollowRequestsLink />
<hr />
<ListPanel />
<FollowedTagsPanel />
<ColumnLink
transparent
to='/favourites'
icon='star'
iconComponent={StarIcon}
activeIconComponent={StarActiveIcon}
text={intl.formatMessage(messages.favourites)}
/>
<ColumnLink
transparent
to='/bookmarks'
icon='bookmarks'
iconComponent={BookmarksIcon}
activeIconComponent={BookmarksActiveIcon}
text={intl.formatMessage(messages.bookmarks)}
/>
<ColumnLink
transparent
to='/conversations'
icon='at'
iconComponent={AlternateEmailIcon}
text={intl.formatMessage(messages.direct)}
/>
<hr />
<ColumnLink
transparent
href='/settings/preferences'
icon='cog'
iconComponent={SettingsIcon}
text={intl.formatMessage(messages.preferences)}
/>
<ColumnLink
transparent
onClick={handleOpenSettings}
icon='cogs'
iconComponent={AdministrationIcon}
text={intl.formatMessage(messages.app_settings)}
/>
<MoreLink />
</>
)}
<div className='navigation-panel__legal'>
<ColumnLink
transparent
to='/about'
icon='ellipsis-h'
iconComponent={InfoIcon}
text={intl.formatMessage(messages.about)}
/>
</div>
{!signedIn && (
<div className='navigation-panel__sign-in-banner'>
<hr />
{disabledAccountId ? <DisabledAccountBanner /> : <SignInBanner />}
</div>
)}
</div>
<div className='flex-spacer' />
<Trends />
</div>
);
};
export const CollapsibleNavigationPanel: React.FC = () => {
const open = useAppSelector((state) => state.navigation.open);
const dispatch = useAppDispatch();
const openable = useBreakpoint('openable');
const showSearch = useBreakpoint('full');
const location = useLocation();
const overlayRef = useRef<HTMLDivElement | null>(null);
@@ -293,6 +466,7 @@ export const NavigationPanel: React.FC = () => {
filterTaps: true,
bounds: isLtrDir ? { left: 0 } : { right: 0 },
rubberband: true,
enabled: openable,
},
);
@@ -311,37 +485,6 @@ export const NavigationPanel: React.FC = () => {
}
}, [open]);
let banner: React.ReactNode;
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>
);
}
const handleOpenSettings = useCallback<MouseEventHandler>(
(e) => {
e.preventDefault();
e.stopPropagation();
dispatch(
openModal({
modalType: 'SETTINGS',
modalProps: {},
}),
);
},
[dispatch],
);
const showOverlay = openable && open;
return (
@@ -357,140 +500,7 @@ export const NavigationPanel: React.FC = () => {
{...bind()}
style={openable ? { x } : undefined}
>
<div className='navigation-panel'>
{showSearch && <Search singleColumn />}
<ProfileCard />
{banner && <div className='navigation-panel__banner'>{banner}</div>}
<div className='navigation-panel__menu'>
{signedIn && (
<>
<ColumnLink
to='/publish'
icon='plus'
iconComponent={AddIcon}
activeIconComponent={AddIcon}
text={intl.formatMessage(messages.compose)}
className='button navigation-panel__compose-button'
/>
<ColumnLink
transparent
to='/home'
icon='home'
iconComponent={HomeIcon}
activeIconComponent={HomeActiveIcon}
text={intl.formatMessage(messages.home)}
/>
</>
)}
{trendsEnabled && (
<ColumnLink
transparent
to='/explore'
icon='explore'
iconComponent={TrendingUpIcon}
text={intl.formatMessage(messages.explore)}
/>
)}
{(signedIn || timelinePreview) && (
<ColumnLink
transparent
to='/public/local'
icon='globe'
iconComponent={PublicIcon}
isActive={isFirehoseActive}
text={intl.formatMessage(messages.firehose)}
/>
)}
{signedIn && (
<>
<NotificationsLink />
<FollowRequestsLink />
<hr />
<ListPanel />
<FollowedTagsPanel />
<ColumnLink
transparent
to='/favourites'
icon='star'
iconComponent={StarIcon}
activeIconComponent={StarActiveIcon}
text={intl.formatMessage(messages.favourites)}
/>
<ColumnLink
transparent
to='/bookmarks'
icon='bookmarks'
iconComponent={BookmarksIcon}
activeIconComponent={BookmarksActiveIcon}
text={intl.formatMessage(messages.bookmarks)}
/>
<ColumnLink
transparent
to='/conversations'
icon='at'
iconComponent={AlternateEmailIcon}
text={intl.formatMessage(messages.direct)}
/>
<hr />
<ColumnLink
transparent
href='/settings/preferences'
icon='cog'
iconComponent={SettingsIcon}
text={intl.formatMessage(messages.preferences)}
/>
<ColumnLink
transparent
onClick={handleOpenSettings}
icon='cogs'
iconComponent={AdministrationIcon}
text={intl.formatMessage(messages.app_settings)}
/>
<MoreLink />
</>
)}
<div className='navigation-panel__legal'>
<ColumnLink
transparent
to='/about'
icon='ellipsis-h'
iconComponent={InfoIcon}
text={intl.formatMessage(messages.about)}
/>
</div>
{!signedIn && (
<div className='navigation-panel__sign-in-banner'>
<hr />
{disabledAccountId ? (
<DisabledAccountBanner />
) : (
<SignInBanner />
)}
</div>
)}
</div>
<div className='flex-spacer' />
<Trends />
</div>
<NavigationPanel />
</animated.div>
</div>
);

View File

@@ -1,53 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import SettingsIcon from '@/material-icons/400-20px/settings.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { requestBrowserPermission } from 'flavours/glitch/actions/notifications';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
class NotificationsPermissionBanner extends PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.dispatch(requestBrowserPermission());
};
handleClose = () => {
this.props.dispatch(changeSetting(['notifications', 'dismissPermissionBanner'], true));
};
render () {
const { intl } = this.props;
return (
<div className='notifications-permission-banner'>
<div className='notifications-permission-banner__close'>
<IconButton icon='times' iconComponent={CloseIcon} onClick={this.handleClose} title={intl.formatMessage(messages.close)} />
</div>
<h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2>
<p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' icon={SettingsIcon} /> }} /></p>
<Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button>
</div>
);
}
}
export default connect()(injectIntl(NotificationsPermissionBanner));

View File

@@ -0,0 +1,74 @@
import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAppDispatch } from '@/flavours/glitch/store';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import UnfoldMoreIcon from '@/material-icons/400-24px/unfold_more.svg?react';
import { requestBrowserPermission } from 'flavours/glitch/actions/notifications';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { Button } from 'flavours/glitch/components/button';
import { messages as columnHeaderMessages } from 'flavours/glitch/components/column_header';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const NotificationsPermissionBanner: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const handleClick = useCallback(() => {
dispatch(requestBrowserPermission());
}, [dispatch]);
const handleClose = useCallback(() => {
dispatch(changeSetting(['notifications', 'dismissPermissionBanner'], true));
}, [dispatch]);
return (
<div className='notifications-permission-banner'>
<div className='notifications-permission-banner__close'>
<IconButton
icon='times'
iconComponent={CloseIcon}
onClick={handleClose}
title={intl.formatMessage(messages.close)}
/>
</div>
<h2>
<FormattedMessage
id='notifications_permission_banner.title'
defaultMessage='Never miss a thing'
/>
</h2>
<p>
<FormattedMessage
id='notifications_permission_banner.how_to_control'
defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled."
values={{
icon: (
<Icon
id='sliders'
icon={UnfoldMoreIcon}
aria-label={intl.formatMessage(columnHeaderMessages.show)}
/>
),
}}
/>
</p>
<Button onClick={handleClick}>
<FormattedMessage
id='notifications_permission_banner.enable'
defaultMessage='Enable desktop notifications'
/>
</Button>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default NotificationsPermissionBanner;

View File

@@ -122,98 +122,93 @@ export const PolicyControls: React.FC = () => {
value={notificationPolicy.for_not_following}
onChange={handleFilterNotFollowing}
options={options}
>
<strong>
label={
<FormattedMessage
id='notifications.policy.filter_not_following_title'
defaultMessage="People you don't follow"
/>
</strong>
<span className='hint'>
}
hint={
<FormattedMessage
id='notifications.policy.filter_not_following_hint'
defaultMessage='Until you manually approve them'
/>
</span>
</SelectWithLabel>
}
/>
<SelectWithLabel
value={notificationPolicy.for_not_followers}
onChange={handleFilterNotFollowers}
options={options}
>
<strong>
label={
<FormattedMessage
id='notifications.policy.filter_not_followers_title'
defaultMessage='People not following you'
/>
</strong>
<span className='hint'>
}
hint={
<FormattedMessage
id='notifications.policy.filter_not_followers_hint'
defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}'
values={{ days: 3 }}
/>
</span>
</SelectWithLabel>
}
/>
<SelectWithLabel
value={notificationPolicy.for_new_accounts}
onChange={handleFilterNewAccounts}
options={options}
>
<strong>
label={
<FormattedMessage
id='notifications.policy.filter_new_accounts_title'
defaultMessage='New accounts'
/>
</strong>
<span className='hint'>
}
hint={
<FormattedMessage
id='notifications.policy.filter_new_accounts.hint'
defaultMessage='Created within the past {days, plural, one {one day} other {# days}}'
values={{ days: 30 }}
/>
</span>
</SelectWithLabel>
}
/>
<SelectWithLabel
value={notificationPolicy.for_private_mentions}
onChange={handleFilterPrivateMentions}
options={options}
>
<strong>
label={
<FormattedMessage
id='notifications.policy.filter_private_mentions_title'
defaultMessage='Unsolicited private mentions'
/>
</strong>
<span className='hint'>
}
hint={
<FormattedMessage
id='notifications.policy.filter_private_mentions_hint'
defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender"
/>
</span>
</SelectWithLabel>
}
/>
<SelectWithLabel
value={notificationPolicy.for_limited_accounts}
onChange={handleFilterLimitedAccounts}
options={options}
>
<strong>
label={
<FormattedMessage
id='notifications.policy.filter_limited_accounts_title'
defaultMessage='Moderated accounts'
/>
</strong>
<span className='hint'>
}
hint={
<FormattedMessage
id='notifications.policy.filter_limited_accounts_hint'
defaultMessage='Limited by server moderators'
/>
</span>
</SelectWithLabel>
}
/>
</div>
</section>
);

View File

@@ -1,5 +1,5 @@
import type { PropsWithChildren } from 'react';
import { useCallback, useState, useRef } from 'react';
import { useCallback, useState, useRef, useId } from 'react';
import classNames from 'classnames';
@@ -16,6 +16,8 @@ interface DropdownProps {
options: SelectItem[];
disabled?: boolean;
onChange: (value: string) => void;
'aria-labelledby': string;
'aria-describedby'?: string;
placement?: Placement;
}
@@ -24,51 +26,33 @@ const Dropdown: React.FC<DropdownProps> = ({
options,
disabled,
onChange,
'aria-labelledby': ariaLabelledBy,
'aria-describedby': ariaDescribedBy,
placement: initialPlacement = 'bottom-end',
}) => {
const activeElementRef = useRef<Element | null>(null);
const containerRef = useRef(null);
const containerRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [isOpen, setOpen] = useState<boolean>(false);
const [placement, setPlacement] = useState<Placement>(initialPlacement);
const handleToggle = useCallback(() => {
if (
isOpen &&
activeElementRef.current &&
activeElementRef.current instanceof HTMLElement
) {
activeElementRef.current.focus({ preventScroll: true });
}
setOpen(!isOpen);
}, [isOpen, setOpen]);
const handleMouseDown = useCallback(() => {
if (!isOpen) activeElementRef.current = document.activeElement;
}, [isOpen]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case ' ':
case 'Enter':
if (!isOpen) activeElementRef.current = document.activeElement;
break;
}
},
[isOpen],
);
const uniqueId = useId();
const menuId = `${uniqueId}-menu`;
const buttonLabelId = `${uniqueId}-button`;
const handleClose = useCallback(() => {
if (
isOpen &&
activeElementRef.current &&
activeElementRef.current instanceof HTMLElement
)
activeElementRef.current.focus({ preventScroll: true });
if (isOpen && buttonRef.current) {
buttonRef.current.focus({ preventScroll: true });
}
setOpen(false);
}, [isOpen]);
const handleToggle = useCallback(() => {
if (isOpen) {
handleClose();
} else {
setOpen(true);
}
}, [isOpen, handleClose]);
const handleOverlayEnter = useCallback(
(state: Partial<PopperState>) => {
if (state.placement) setPlacement(state.placement);
@@ -82,13 +66,18 @@ const Dropdown: React.FC<DropdownProps> = ({
<div ref={containerRef}>
<button
type='button'
ref={buttonRef}
onClick={handleToggle}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
disabled={disabled}
aria-expanded={isOpen}
aria-controls={menuId}
aria-labelledby={`${ariaLabelledBy} ${buttonLabelId}`}
aria-describedby={ariaDescribedBy}
className={classNames('dropdown-button', { active: isOpen })}
>
<span className='dropdown-button__label'>{valueOption?.text}</span>
<span id={buttonLabelId} className='dropdown-button__label'>
{valueOption?.text}
</span>
<Icon id='down' icon={ArrowDropDownIcon} />
</button>
@@ -101,7 +90,7 @@ const Dropdown: React.FC<DropdownProps> = ({
popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}
>
{({ props, placement }) => (
<div {...props}>
<div {...props} id={menuId}>
<div
className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
>
@@ -123,6 +112,8 @@ const Dropdown: React.FC<DropdownProps> = ({
interface Props {
value: string;
options: SelectItem[];
label: string | React.ReactElement;
hint: string | React.ReactElement;
disabled?: boolean;
onChange: (value: string) => void;
}
@@ -130,13 +121,26 @@ interface Props {
export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
value,
options,
label,
hint,
disabled,
children,
onChange,
}) => {
const uniqueId = useId();
const labelId = `${uniqueId}-label`;
const descId = `${uniqueId}-desc`;
return (
// This label is only used for its click-forwarding behaviour,
// accessible names are assigned manually
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>{children}</div>
<div className='app-form__toggle__label'>
<strong id={labelId}>{label}</strong>
<span className='hint' id={descId}>
{hint}
</span>
</div>
<div className='app-form__toggle__toggle'>
<div>
@@ -144,6 +148,8 @@ export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
value={value}
onChange={onChange}
disabled={disabled}
aria-labelledby={labelId}
aria-describedby={descId}
options={options}
/>
</div>

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@@ -24,6 +24,10 @@ import { openModal } from 'flavours/glitch/actions/modal';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { useIdentity } from 'flavours/glitch/identity_context';
import { me } from 'flavours/glitch/initial_state';
import type { Account } from 'flavours/glitch/models/account';
import type { Status } from 'flavours/glitch/models/status';
import { makeGetStatus } from 'flavours/glitch/selectors';
import type { RootState } from 'flavours/glitch/store';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
const messages = defineMessages({
@@ -50,6 +54,11 @@ const messages = defineMessages({
open: { id: 'status.open', defaultMessage: 'Expand this status' },
});
type GetStatusSelector = (
state: RootState,
props: { id?: string | null; contextType?: string },
) => Status | null;
export const Footer: React.FC<{
statusId: string;
withOpenButton?: boolean;
@@ -59,11 +68,9 @@ export const Footer: React.FC<{
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const status = useAppSelector((state) => state.statuses.get(statusId));
const accountId = status?.get('account') as string | undefined;
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector;
const status = useAppSelector((state) => getStatus(state, { id: statusId }));
const account = status?.get('account') as Account | undefined;
const askReplyConfirmation = useAppSelector(
(state) => (state.compose.get('text') as string).trim().length !== 0,
);

View File

@@ -5,7 +5,7 @@ import ModalContainer from 'flavours/glitch/features/ui/containers/modal_contain
const Compose = () => (
<>
<ComposeFormContainer autoFocus withoutNavigation />
<ComposeFormContainer autoFocus withoutNavigation redirectOnSuccess />
<AlertsController />
<ModalContainer />
<LoadingBarContainer className='loading-bar' />

View File

@@ -38,7 +38,7 @@ const Embed: React.FC<{ id: string }> = ({ id }) => {
const dispatchRenderSignal = useRenderSignal();
useEffect(() => {
dispatch(fetchStatus(id, false, false));
dispatch(fetchStatus(id, { alsoFetchContext: false }));
}, [dispatch, id]);
const handleToggleHidden = useCallback(() => {

View File

@@ -418,7 +418,11 @@ export const DetailedStatus: React.FC<{
{hashtagBar}
{status.get('quote') && (
<QuotedStatus quote={status.get('quote')} />
<QuotedStatus
quote={status.get('quote')}
parentQuotePostId={status.get('id')}
contextType='thread'
/>
)}
</>
)}

View File

@@ -0,0 +1,30 @@
import { useLayoutEffect } from 'react';
import { createAppSelector, useAppSelector } from 'flavours/glitch/store';
const getShouldLockBodyScroll = createAppSelector(
[
(state) => state.navigation.open,
(state) => state.modal.get('stack').size > 0,
],
(isMobileMenuOpen: boolean, isModalOpen: boolean) =>
isMobileMenuOpen || isModalOpen,
);
/**
* This component locks scrolling on the body when
* `getShouldLockBodyScroll` returns true.
*/
export const BodyScrollLock: React.FC = () => {
const shouldLockBodyScroll = useAppSelector(getShouldLockBodyScroll);
useLayoutEffect(() => {
document.documentElement.classList.toggle(
'has-modal',
shouldLockBodyScroll,
);
}, [shouldLockBodyScroll]);
return null;
};

View File

@@ -19,7 +19,6 @@ export const ColumnLink: React.FC<{
method?: string;
badge?: React.ReactNode;
transparent?: boolean;
optional?: boolean;
className?: string;
id?: string;
}> = ({
@@ -34,13 +33,11 @@ export const ColumnLink: React.FC<{
method,
badge,
transparent,
optional,
...other
}) => {
const match = useRouteMatch(to ?? '');
const className = classNames('column-link', {
'column-link--transparent': transparent,
'column-link--optional': optional,
});
const badgeElement =
typeof badge !== 'undefined' ? (

View File

@@ -23,9 +23,9 @@ import { useColumnsContext } from '../util/columns_context';
import BundleColumnError from './bundle_column_error';
import { ColumnLoading } from './column_loading';
import { ComposePanel } from './compose_panel';
import { ComposePanel, RedirectToMobileComposeIfNeeded } from './compose_panel';
import DrawerLoading from './drawer_loading';
import { NavigationPanel } from 'flavours/glitch/features/navigation_panel';
import { CollapsibleNavigationPanel } from 'flavours/glitch/features/navigation_panel';
const componentMap = {
'COMPOSE': Compose,
@@ -124,6 +124,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
<RedirectToMobileComposeIfNeeded />
</div>
</div>
@@ -132,7 +133,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<NavigationPanel />
<CollapsibleNavigationPanel />
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useLayoutEffect } from 'react';
import { useLayout } from '@/flavours/glitch/hooks/useLayout';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
@@ -7,6 +7,7 @@ import {
mountCompose,
unmountCompose,
} from 'flavours/glitch/actions/compose';
import { useAppHistory } from 'flavours/glitch/components/router';
import ServerBanner from 'flavours/glitch/components/server_banner';
import { Search } from 'flavours/glitch/features/compose/components/search';
import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
@@ -54,3 +55,25 @@ export const ComposePanel: React.FC = () => {
</div>
);
};
/**
* Redirect the user to the standalone compose page when the
* sidebar composer is hidden due to a change in viewport size
* while a post is being written.
*/
export const RedirectToMobileComposeIfNeeded: React.FC = () => {
const history = useAppHistory();
const shouldRedirect = useAppSelector((state) =>
state.compose.get('should_redirect_to_compose_page'),
);
useLayoutEffect(() => {
if (shouldRedirect) {
history.push('/publish');
}
}, [history, shouldRedirect]);
return null;
};

View File

@@ -21,6 +21,7 @@ export const ConfirmationModal: React.FC<
title: React.ReactNode;
message: React.ReactNode;
confirm: React.ReactNode;
cancel?: React.ReactNode;
secondary?: React.ReactNode;
onSecondary?: () => void;
onConfirm: () => void;
@@ -30,6 +31,7 @@ export const ConfirmationModal: React.FC<
title,
message,
confirm,
cancel,
onClose,
onConfirm,
secondary,
@@ -65,10 +67,12 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<button onClick={handleCancel} className='link-button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'
/>
{cancel ?? (
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'
/>
)}
</button>
{secondary && (

View File

@@ -0,0 +1,104 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { replyCompose } from 'flavours/glitch/actions/compose';
import { editStatus } from 'flavours/glitch/actions/statuses';
import type { Status } from 'flavours/glitch/models/status';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const editMessages = defineMessages({
title: {
id: 'confirmations.discard_draft.edit.title',
defaultMessage: 'Discard changes to your post?',
},
message: {
id: 'confirmations.discard_draft.edit.message',
defaultMessage:
'Continuing will discard any changes you have made to the post you are currently editing.',
},
cancel: {
id: 'confirmations.discard_draft.edit.cancel',
defaultMessage: 'Resume editing',
},
});
const postMessages = defineMessages({
title: {
id: 'confirmations.discard_draft.post.title',
defaultMessage: 'Discard your draft post?',
},
message: {
id: 'confirmations.discard_draft.post.message',
defaultMessage:
'Continuing will discard the post you are currently composing.',
},
cancel: {
id: 'confirmations.discard_draft.post.cancel',
defaultMessage: 'Resume draft',
},
});
const messages = defineMessages({
confirm: {
id: 'confirmations.discard_draft.confirm',
defaultMessage: 'Discard and continue',
},
});
const DiscardDraftConfirmationModal: React.FC<
{
onConfirm: () => void;
} & BaseConfirmationModalProps
> = ({ onConfirm, onClose }) => {
const intl = useIntl();
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
const contextualMessages = isEditing ? editMessages : postMessages;
return (
<ConfirmationModal
title={intl.formatMessage(contextualMessages.title)}
message={intl.formatMessage(contextualMessages.message)}
cancel={intl.formatMessage(contextualMessages.cancel)}
confirm={intl.formatMessage(messages.confirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};
export const ConfirmReplyModal: React.FC<
{
status: Status;
} & BaseConfirmationModalProps
> = ({ status, onClose }) => {
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(replyCompose(status));
}, [dispatch, status]);
return (
<DiscardDraftConfirmationModal onConfirm={onConfirm} onClose={onClose} />
);
};
export const ConfirmEditStatusModal: React.FC<
{
statusId: string;
} & BaseConfirmationModalProps
> = ({ statusId, onClose }) => {
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(editStatus(statusId));
}, [dispatch, statusId]);
return (
<DiscardDraftConfirmationModal onConfirm={onConfirm} onClose={onClose} />
);
};

View File

@@ -1,45 +0,0 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { editStatus } from 'flavours/glitch/actions/statuses';
import { useAppDispatch } from 'flavours/glitch/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
editTitle: {
id: 'confirmations.edit.title',
defaultMessage: 'Overwrite post?',
},
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: {
id: 'confirmations.edit.message',
defaultMessage:
'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?',
},
});
export const ConfirmEditStatusModal: React.FC<
{
statusId: string;
} & BaseConfirmationModalProps
> = ({ statusId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(editStatus(statusId));
}, [dispatch, statusId]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.editTitle)}
message={intl.formatMessage(messages.editMessage)}
confirm={intl.formatMessage(messages.editConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@@ -1,8 +1,10 @@
export { ConfirmationModal } from './confirmation_modal';
export { ConfirmDeleteStatusModal } from './delete_status';
export { ConfirmDeleteListModal } from './delete_list';
export { ConfirmReplyModal } from './reply';
export { ConfirmEditStatusModal } from './edit_status';
export {
ConfirmReplyModal,
ConfirmEditStatusModal,
} from './discard_draft_confirmation';
export { ConfirmUnfollowModal } from './unfollow';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';

View File

@@ -1,46 +0,0 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { replyCompose } from 'flavours/glitch/actions/compose';
import type { Status } from 'flavours/glitch/models/status';
import { useAppDispatch } from 'flavours/glitch/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
replyTitle: {
id: 'confirmations.reply.title',
defaultMessage: 'Overwrite post?',
},
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: {
id: 'confirmations.reply.message',
defaultMessage:
'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?',
},
});
export const ConfirmReplyModal: React.FC<
{
status: Status;
} & BaseConfirmationModalProps
> = ({ status, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(replyCompose(status));
}, [dispatch, status]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.replyTitle)}
message={intl.formatMessage(messages.replyMessage)}
confirm={intl.formatMessage(messages.replyConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@@ -38,7 +38,7 @@ class FilterModal extends ImmutablePureComponent {
handleSuccess = () => {
const { dispatch, statusId } = this.props;
dispatch(fetchStatus(statusId, true));
dispatch(fetchStatus(statusId, {forceFetch: true}));
this.setState({ isSubmitting: false, isSubmitted: true, step: 'submitted' });
};

View File

@@ -201,8 +201,6 @@ class MediaModal extends ImmutablePureComponent {
preview={image.get('preview_url')}
blurhash={image.get('blurhash')}
src={image.get('url')}
width={image.get('width')}
height={image.get('height')}
frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
startTime={currentTime || 0}
startPlaying={autoPlay || false}
@@ -218,8 +216,6 @@ class MediaModal extends ImmutablePureComponent {
return (
<GIFV
src={image.get('url')}
width={width}
height={height}
key={image.get('url')}
alt={description}
lang={lang}

View File

@@ -21,7 +21,6 @@ import {
IgnoreNotificationsModal,
AnnualReportModal,
} from 'flavours/glitch/features/ui/util/async-components';
import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar';
import BundleContainer from '../containers/bundle_container';
@@ -98,16 +97,6 @@ export default class ModalRoot extends PureComponent {
backgroundColor: null,
};
componentDidUpdate () {
if (this.props.type) {
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
} else {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = '0';
}
}
setBackgroundColor = color => {
this.setState({ backgroundColor: color });
};

View File

@@ -22,7 +22,7 @@ import { registrationsOpen, sso_redirect } from 'flavours/glitch/initial_state';
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
const messages = defineMessages({
export const messages = defineMessages({
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
search: { id: 'tabs_bar.search', defaultMessage: 'Search' },
publish: { id: 'tabs_bar.publish', defaultMessage: 'New Post' },

View File

@@ -51,7 +51,7 @@ const makeGetStatusIds = (pending = false) => createSelector([
}
const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index');
if (regex && !regex.test(searchIndex)) {
if (regex && regex.test(searchIndex)) {
return false;
}

View File

@@ -69,7 +69,6 @@ import {
DomainBlocks,
Mutes,
PinnedStatuses,
GettingStartedMisc,
Directory,
OnboardingProfile,
OnboardingFollows,
@@ -93,8 +92,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
layout: state.getIn(['meta', 'layout']),
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null,
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
isWide: state.getIn(['local_settings', 'stretch']),
fullWidthColumns: state.getIn(['local_settings', 'fullwidth_columns']),
@@ -152,13 +150,8 @@ class SwitchingColumnsArea extends PureComponent {
};
UNSAFE_componentWillMount () {
if (this.props.singleColumn) {
document.body.classList.toggle('layout-single-column', true);
document.body.classList.toggle('layout-multiple-columns', false);
} else {
document.body.classList.toggle('layout-single-column', false);
document.body.classList.toggle('layout-multiple-columns', true);
}
document.body.classList.toggle('layout-single-column', this.props.singleColumn);
document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn);
}
componentDidUpdate (prevProps) {
@@ -210,8 +203,8 @@ class SwitchingColumnsArea extends PureComponent {
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : 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}
{pathName === '/getting-started' ? <Redirect from='/getting-started' to={singleColumn ? '/home' : '/deck/getting-started'} exact /> : null}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
@@ -271,7 +264,6 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} />
<WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} />
<Route component={BundleColumnError} />
</WrappedSwitch>
@@ -291,8 +283,7 @@ class UI extends PureComponent {
fullWidthColumns: PropTypes.bool,
systemFontUi: PropTypes.bool,
isComposing: PropTypes.bool,
hasComposingText: PropTypes.bool,
hasMediaAttachments: PropTypes.bool,
hasComposingContents: PropTypes.bool,
canUploadMore: PropTypes.bool,
intl: PropTypes.object.isRequired,
unreadNotifications: PropTypes.number,
@@ -311,11 +302,11 @@ class UI extends PureComponent {
};
handleBeforeUnload = e => {
const { intl, dispatch, hasComposingText, hasMediaAttachments } = this.props;
const { intl, dispatch, hasComposingContents } = this.props;
dispatch(synchronouslySubmitMarkers());
if (hasComposingText || hasMediaAttachments) {
if (hasComposingContents) {
// Setting returnValue to any string causes confirmation dialog.
// Many browsers no longer display this text to users,
// but we set user-friendly message for other browsers, e.g. Edge.

View File

@@ -50,10 +50,6 @@ export function GettingStarted () {
return import('../../getting_started');
}
export function GettingStartedMisc () {
return import('../../getting_started_misc');
}
export function KeyboardShortcuts () {
return import('../../keyboard_shortcuts');
}

View File

@@ -8,13 +8,14 @@ import { openURL } from 'flavours/glitch/actions/search';
import { useAppDispatch } from 'flavours/glitch/store';
const isMentionClick = (element: HTMLAnchorElement) =>
element.classList.contains('mention');
element.classList.contains('mention') &&
!element.classList.contains('hashtag');
const isHashtagClick = (element: HTMLAnchorElement) =>
element.textContent?.[0] === '#' ||
element.previousSibling?.textContent?.endsWith('#');
export const useLinks = () => {
export const useLinks = (skipHashtags?: boolean) => {
const history = useHistory();
const dispatch = useAppDispatch();
@@ -61,12 +62,12 @@ export const useLinks = () => {
if (isMentionClick(target)) {
e.preventDefault();
void handleMentionClick(target);
} else if (isHashtagClick(target)) {
} else if (isHashtagClick(target) && !skipHashtags) {
e.preventDefault();
handleHashtagClick(target);
}
},
[handleMentionClick, handleHashtagClick],
[skipHashtags, handleMentionClick, handleHashtagClick],
);
return handleClick;

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "عرض الملف الشخصي كاملاً",
"boost_modal.missing_description": "هذا المنشور يحتوي على وسائط بلا وصف",
"column.favourited_by": "المفضلة من قبل",
"column.heading": "متنوعة",
"column.reblogged_by": "المرقى من قبل",
"column.subheading": "خيارات متنوعة",
"column_header.profile": "الملف الشخصي",
"column_subheading.lists": "القوائم",
"column_subheading.navigation": "التنقل",
"community.column_settings.allow_local_only": "إظهار المنشورات المحلية فقط",
"compose.attach.doodle": "الرسوم و التخمين",
"compose.change_federation": "تغيير اعدادات الفيديرالية",
@@ -36,8 +32,6 @@
"keyboard_shortcuts.secondary_toot": "لإرسال التبويق باستخدام إعدادات الخصوصية الثانوية",
"moved_to_warning": "عُلِّم هذا الحساب بأنه انتقل إلى {moved_to_link}، لذا قد لا يقبل متابعات جديدة.",
"navigation_bar.app_settings": "إعدادات التطبيق",
"navigation_bar.keyboard_shortcuts": "اختصارات لوحة المفاتيح",
"navigation_bar.misc": "متنوع",
"settings.always_show_spoilers_field": "تمكين دائما حقل تحذير المحتوى",
"settings.close": "إغلاق",
"settings.content_warnings": "Content warnings",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Zobrazit celý profil",
"boost_modal.missing_description": "Příspěvek obsahuje obrázky bez popisků",
"column.favourited_by": "Oblíbeno uživatelem",
"column.heading": "Různé",
"column.reblogged_by": "Boostnuto uživatelem",
"column.subheading": "Různé",
"column_header.profile": "Profil",
"column_subheading.lists": "Seznamy",
"column_subheading.navigation": "Navigace",
"community.column_settings.allow_local_only": "Zobrazit pouze místní tooty",
"compose.attach.doodle": "Nakreslete něco",
"compose.change_federation": "Změnit nastavení federace",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "pro odeslání příspěvku s sekundárním nastavením soukromí",
"moved_to_warning": "Tento účet je označen jako přesunut na {moved_to_link}, a proto nemusí přijímat nové sledování.",
"navigation_bar.app_settings": "Nastavení aplikace",
"navigation_bar.keyboard_shortcuts": "Klávesové zkratky",
"navigation_bar.misc": "Různé",
"notifications.column_settings.filter_bar.show_bar": "Zobrazit panel filtrů",
"settings.always_show_spoilers_field": "Vždy zobrazit pole pro varování o obsahu",
"settings.close": "Zavřít",

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