Compare commits

..

154 Commits

Author SHA1 Message Date
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
409 changed files with 9657 additions and 3584 deletions

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

@@ -2,6 +2,132 @@
All notable changes to this project will be documented in this file.
## [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

View File

@@ -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
@@ -458,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)
@@ -494,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)
@@ -642,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)
@@ -668,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
@@ -692,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)
@@ -725,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)
@@ -801,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)
@@ -869,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)
@@ -899,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)
@@ -932,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)
@@ -1096,6 +1096,7 @@ DEPENDENCIES
webauthn (~> 3.0)
webmock (~> 3.18)
webpush!
websocket-driver (~> 0.8)
xorcist (~> 1.1)
RUBY VERSION

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

@@ -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,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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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';
@@ -777,7 +778,6 @@ export const AccountHeader: React.FC<{
);
}
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

@@ -81,6 +81,7 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool,
lang: PropTypes.string,
maxChars: PropTypes.number,
redirectOnSuccess: PropTypes.bool,
};
static defaultProps = {
@@ -336,7 +337,7 @@ class ComposeForm extends ImmutablePureComponent {
>
{intl.formatMessage(
this.props.isEditing ?
messages.saveChanges :
messages.saveChanges :
(this.props.isInReply ? messages.reply : messages.publish)
)}
</Button>

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

@@ -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

@@ -466,6 +466,7 @@ export const CollapsibleNavigationPanel: React.FC = () => {
filterTaps: true,
bounds: isLtrDir ? { left: 0 } : { right: 0 },
rubberband: true,
enabled: openable,
},
);

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,10 @@ export const DetailedStatus: React.FC<{
{hashtagBar}
{status.get('quote') && (
<QuotedStatus quote={status.get('quote')} />
<QuotedStatus
quote={status.get('quote')}
parentQuotePostId={status.get('id')}
/>
)}
</>
)}

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

@@ -92,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']),
@@ -284,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,
@@ -304,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

@@ -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",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Dangos proffil cyfan",
"boost_modal.missing_description": "Mae'r tŵt yma'n cynnwys ychydig gyfryngau heb ddisgrifiad",
"column.favourited_by": "Wedi'i hoffi gan",
"column.heading": "Misg",
"column.reblogged_by": "Wedi'i bŵstio gan",
"column.subheading": "Opsiynnau arall",
"column_header.profile": "Proffil",
"column_subheading.lists": "Rhestri",
"column_subheading.navigation": "Llywio",
"community.column_settings.allow_local_only": "Dangos tŵtiau lleol yn unig",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",

View File

@@ -2,7 +2,6 @@
"home.column_settings.advanced": "Avanceret",
"home.column_settings.show_direct": "Vis private omtaler",
"navigation_bar.app_settings": "Appindstillinger",
"navigation_bar.misc": "Diverse",
"settings.always_show_spoilers_field": "Vis altid feltet til indholdsadvarsel",
"settings.close": "Luk",
"settings.content_warnings": "Indholdsadvarsler",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Vollständiges Profil anzeigen",
"boost_modal.missing_description": "Dieser Toot enthält Medien ohne Beschreibung",
"column.favourited_by": "Favorisiert von",
"column.heading": "Sonstiges",
"column.reblogged_by": "Geteilt von",
"column.subheading": "Sonstige Optionen",
"column_header.profile": "Profil",
"column_subheading.lists": "Listen",
"column_subheading.navigation": "Navigation",
"community.column_settings.allow_local_only": "Nur-lokale Toots anzeigen",
"compose.attach.doodle": "Male etwas",
"compose.change_federation": "Föderationseinstellungen ändern",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "Toot mit sekundärer Privatsphäreeinstellung absenden",
"moved_to_warning": "Dieses Konto ist als verschoben zu {moved_to_link} markiert und akzeptiert daher keine neuen Follower.",
"navigation_bar.app_settings": "App-Einstellungen",
"navigation_bar.keyboard_shortcuts": "Tastaturkürzel",
"navigation_bar.misc": "Sonstiges",
"notifications.column_settings.filter_bar.show_bar": "Filterleiste anzeigen",
"settings.always_show_spoilers_field": "Das Inhaltswarnungs-Feld immer aktivieren",
"settings.close": "Schließen",

View File

@@ -1,4 +1,92 @@
{
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
"about.fork_disclaimer": "Το Glitch-soc είναι ελεύθερο λογισμικό ανοιχτού κώδικα που είναι αντιγραφή από το Mastodon.",
"account.disclaimer_full": "Οι παρακάτω πληροφορίες μπορεί να αντικατοπτρίζουν το προφίλ του χρήστη ελλιπώς.",
"account.follows": "Ακολουθεί",
"account.suspended_disclaimer_full": "Αυτός ο χρήστης έχει ανασταλεί από έναν συντονιστή.",
"account.view_full_profile": "Προβολή πλήρους προφίλ",
"boost_modal.missing_description": "Αυτό το τουτ περιέχει κάποια μέσα χωρίς περιγραφή",
"column.favourited_by": "Αγαπήθηκε από",
"column.reblogged_by": "Ενισχύθηκε από",
"column_header.profile": "Προφίλ",
"community.column_settings.allow_local_only": "Εμφάνιση τοπικών τουτ",
"compose.attach.doodle": "Σχεδίασε κάτι",
"compose.change_federation": "Αλλαγή ρυθμίσεων ομοσπονδίας",
"compose.content-type.change": "Αλλαγή προηγμένων επιλογών μορφοποίησης",
"compose.content-type.html": "HTML",
"compose.content-type.html_meta": "Μορφοποίηση των αναρτήσεων σας με χρήση HTML",
"compose.content-type.markdown": "Markdown",
"compose.content-type.markdown_meta": "Μορφοποίηση των αναρτήσεων σας με χρήση Markdown",
"compose.content-type.plain": "Απλό κείμενο",
"compose.content-type.plain_meta": "Γράψιμο χωρίς προηγμένη μορφοποίηση",
"compose.disable_threaded_mode": "Απενεργοποίηση λειτουργίας νημάτων",
"compose.enable_threaded_mode": "Ενεργοποίηση λειτουργίας νημάτων",
"compose_form.sensitive.hide": "{count, plural, one {Επισήμανση πολυμέσου ως ευαίσθητο} other {Επισήμανση πολυμέσων ως ευαίσθητα}}",
"compose_form.sensitive.marked": "{count, plural, one {Το πολυμέσο έχει σημανθεί ως ευαίσθητο} other {Τα πολυμέσα έχουν σημανθεί ως ευαίσθητα}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Το πολυμέσο δεν έχει σημανθεί ως ευαίσθητο} other {Τα πολυμέσα δεν έχουν σημανθεί ως ευαίσθητα}}",
"confirmation_modal.do_not_ask_again": "Να μην ζητηθεί επιβεβαίωση ξανά",
"confirmations.deprecated_settings.confirm": "Χρήση προτιμήσεων του Mastodon",
"direct.group_by_conversations": "Ομαδοποίηση ανά συζήτηση",
"favourite_modal.favourite": "Αγάπησε την ανάρτηση;",
"federation.federated.long": "Επιτρέψτε σε αυτήν την ανάρτηση να φτάσει σε άλλους διακομιστές",
"federation.local_only.long": "Αποτρέψτε αυτήν την ανάρτηση να φτάσει σε άλλους διακομιστές",
"federation.local_only.short": "Τοπικά μόνο",
"firehose.column_settings.allow_local_only": "Εμφάνιση τοπικών αναρτήσεων σε \"Όλα\"",
"home.column_settings.advanced": "Για προχωρημένους",
"home.column_settings.filter_regex": "Φιλτράρισμα βάσει των κανονικών εκφράσεων",
"home.column_settings.show_direct": "Εμφάνιση ιδιωτικών επισημάνσεων",
"home.settings": "Ρυθμίσεις στήλης",
"keyboard_shortcuts.bookmark": "για να σελιδοποιήσει",
"keyboard_shortcuts.secondary_toot": "για αποστολή τουτ χρησιμοποιώντας δευτερεύουσα ρύθμιση απορρήτου",
"moved_to_warning": "Αυτός ο λογαριασμός έχει σημανθεί ως μετακινημένος στο {moved_to_link} και έτσι δεν μπορεί να δεχτεί νέους ακολούθους.",
"navigation_bar.app_settings": "Ρυθμίσεις εφαρμογής",
"notifications.column_settings.filter_bar.show_bar": "Εμφάνιση μπάρας φίλτρου",
"settings.always_show_spoilers_field": "Πάντα ενεργοποίηση του πεδίου Προειδοποίηση Περιεχομένου",
"settings.close": "Κλείσιμο",
"settings.compose_box_opts": "Πλαίσιο σύνθεσης",
"settings.content_warnings": "Προειδοποιήσεις περιεχομένου",
"settings.content_warnings.regexp": "Κανονική έκφραση (regex)",
"settings.content_warnings_filter": "Προειδοποιήσεις περιεχομένου να μην ξεδιπλώνονται αυτόματα:",
"settings.content_warnings_shared_state": "Εμφάνιση/απόκρυψη περιεχομένου όλων των αντιγράφων ταυτόχρονα",
"settings.content_warnings_unfold_opts": "Επιλογές αυτόματου ξεδιπλώματος",
"settings.deprecated_setting": "Αυτή η ρύθμιση τώρα ελέγχεται από τις {settings_page_link} του Mastodon",
"settings.enable_content_warnings_auto_unfold": "Αυτόματη ξεδίπλωμα προειδοποιήσεων περιεχομένου",
"settings.general": "Γενικά",
"settings.layout_opts": "Επιλογές διάταξης",
"settings.media": "Πολυμέσα",
"settings.notifications.tab_badge": "Σήμα μη αναγνωσμένων ειδοποιήσεων",
"settings.notifications.tab_badge.hint": "Εμφανίζει ένα σήμα για μη αναγνωσμένες ειδοποιήσεις στα εικονίδια της στήλης όταν η στήλη ειδοποιήσεων δεν είναι ανοιχτή",
"settings.notifications_opts": "Ρυθμίσεις ειδοποιήσεων",
"settings.pop_in_left": "Αριστερά",
"settings.pop_in_right": "Δεξιά",
"settings.preferences": "Προτιμήσεις χρήστη",
"settings.preselect_on_reply": "Προεπιλογή ονομάτων χρηστών στην απάντηση",
"settings.preselect_on_reply_hint": "Όταν απαντάτε σε μια συνομιλία με πολλαπλούς συμμετέχοντες, προεπιλέξτε τα ονόματα χρηστών μετά τον πρώτο",
"settings.rewrite_mentions": "Ξαναγράψε επισημάνσεις στις προβαλλόμενες καταστάσεις",
"settings.rewrite_mentions_acct": "Ξαναγράψε με όνομα χρήστη και τομέα (όταν ο λογαριασμός είναι απομακρυσμένος)",
"settings.rewrite_mentions_no": "Μην ξαναγράψεις επισημάνσεις",
"settings.rewrite_mentions_username": "Ξαναγράψε με όνομα χρήστη",
"settings.shared_settings_link": "προτιμήσεις χρήστη",
"settings.side_arm": "Δευτερεύον κουμπί τουτ:",
"settings.side_arm.none": "Κανένα",
"settings.side_arm_reply_mode": "Όταν απαντάτε σε ένα τουτ, το κουμπί δευτερεύοντος τουτ πρέπει να:",
"settings.side_arm_reply_mode.copy": "Αντιγράψει τη ρύθμιση απορρήτου του τουτ που απαντάται",
"settings.side_arm_reply_mode.keep": "Διατηρήσει το καθορισμένο απόρρητο",
"settings.side_arm_reply_mode.restrict": "Περιορίσει τη ρύθμιση απορρήτου σε εκείνη του τουτ που απαντάται",
"settings.status_icons": "Εικονίδια τουτ",
"settings.status_icons_language": "Ένδειξη γλώσσας",
"settings.status_icons_local_only": "Ένδειξη τοπικό μόνο",
"settings.status_icons_media": "Ενδείξεις μέσων και δημοσκοπήσεων",
"settings.status_icons_reply": "Ένδειξη απαντήσεων",
"settings.status_icons_visibility": "Ένδειξη απορρήτου τουτ",
"settings.tag_misleading_links": "Σήμανση παραπλανητικών συνδέσμων",
"settings.tag_misleading_links.hint": "Προσθήκη οπτικής ένδειξης με τον εξυπηρετητή στόχο του συνδέσμου σε κάθε σύνδεσμο που δεν αναφέρεται ρητά",
"status.filtered": "Φιλτραρισμένο",
"status.has_audio": "Διαθέτει συνημμένα αρχεία ήχου",
"status.has_pictures": "Διαθέτει συνημμένες εικόνες",
"status.has_preview_card": "Διαθέτει συνημμένη κάρτα προεπισκόπησης",
"status.has_video": "Διαθέτει συνημμένα βίντεο",
"status.hide": "Απόκρυψη ανάρτησης",
"status.in_reply_to": "Αυτό το τουτ είναι απάντηση",
"status.is_poll": "Αυτό το τουτ είναι δημοσκόπηση",
"status.show_filter_reason": "Εμφάνιση ούτως ή άλλως"
}

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Vidi plenan profilon",
"boost_modal.missing_description": "Ĉi tiu afiŝo enhavas plurmedion, ke ne havas priskribon",
"column.favourited_by": "Stelumita per",
"column.heading": "Diversaj aferoj",
"column.reblogged_by": "Diskonigita de",
"column.subheading": "Diversaj agordoj",
"column_header.profile": "Profilo",
"column_subheading.lists": "Listoj",
"column_subheading.navigation": "Navigado",
"community.column_settings.allow_local_only": "Montri nur-lokajn afiŝojn",
"compose.attach.doodle": "Skribu ion",
"compose.change_federation": "Ŝanĝi agordojn de federacio",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "sendi afiŝon per dua agordo de privateco",
"moved_to_warning": "Ĉi tiun konton sigelas, kiel movinta al {moved_to_link}, kaj pro ne permesos novajn sekvantojn.",
"navigation_bar.app_settings": "Agordoj de aplikaĵo",
"navigation_bar.keyboard_shortcuts": "Fulmoklavoj",
"navigation_bar.misc": "Aliaj",
"notifications.column_settings.filter_bar.show_bar": "Montri mezuron de filtrilo",
"settings.always_show_spoilers_field": "Ĉiam ŝaltiĝu la arealo de Enhava Averto",
"settings.close": "Fermi",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Ver perfil completo",
"boost_modal.missing_description": "Esta publicación contiene medios sin descripción",
"column.favourited_by": "Marcado como favorito por",
"column.heading": "Misc",
"column.reblogged_by": "Impulsado por",
"column.subheading": "Opciones misceláneas",
"column_header.profile": "Perfil",
"column_subheading.lists": "Listas",
"column_subheading.navigation": "Navegación",
"community.column_settings.allow_local_only": "Mostrar sólo toots locales",
"compose.attach.doodle": "Dibujá algo",
"compose.change_federation": "Cambiar configuración de la federación",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
"navigation_bar.app_settings": "Ajustes de aplicación",
"navigation_bar.keyboard_shortcuts": "Atajos de teclado",
"navigation_bar.misc": "Misc",
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
"settings.always_show_spoilers_field": "Siempre mostrar el campo de advertencia de contenido",
"settings.close": "Cerrar",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Ver perfil completo",
"boost_modal.missing_description": "Esta publicación contiene medios sin descripción",
"column.favourited_by": "Marcado como favorito por",
"column.heading": "Misc",
"column.reblogged_by": "Impulsado por",
"column.subheading": "Opciones misceláneas",
"column_header.profile": "Perfil",
"column_subheading.lists": "Listas",
"column_subheading.navigation": "Navegación",
"community.column_settings.allow_local_only": "Mostrar sólo toots locales",
"compose.attach.doodle": "Dibujar algo",
"compose.change_federation": "Cambiar configuración de la federación",
@@ -41,8 +37,6 @@
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
"navigation_bar.app_settings": "Ajustes de aplicación",
"navigation_bar.keyboard_shortcuts": "Atajos de teclado",
"navigation_bar.misc": "Misc",
"settings.always_show_spoilers_field": "Siempre mostrar el campo de advertencia de contenido",
"settings.close": "Cerrar",
"settings.compose_box_opts": "Cuadro de redacción",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Ver perfil completo",
"boost_modal.missing_description": "Esta publicación contiene medios sin descripción",
"column.favourited_by": "Marcado como favorito por",
"column.heading": "Misc",
"column.reblogged_by": "Impulsado por",
"column.subheading": "Opciones misceláneas",
"column_header.profile": "Perfil",
"column_subheading.lists": "Listas",
"column_subheading.navigation": "Navegación",
"community.column_settings.allow_local_only": "Mostrar toots solo-locales",
"compose.attach.doodle": "Dibujar algo",
"compose.change_federation": "Cambiar configuración de la federación",
@@ -41,8 +37,6 @@
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
"navigation_bar.app_settings": "Ajustes de la aplicación",
"navigation_bar.keyboard_shortcuts": "Atajos de teclado",
"navigation_bar.misc": "Misc",
"settings.always_show_spoilers_field": "Siempre mostrar el campo de advertencia de contenido",
"settings.close": "Cerrar",
"settings.compose_box_opts": "Cuadro de redacción",

View File

@@ -8,7 +8,6 @@
"compose.content-type.plain": "متن ساده",
"home.column_settings.advanced": "پیشرفته",
"navigation_bar.app_settings": "تنظیمات کاره",
"navigation_bar.keyboard_shortcuts": "میان‌برهای صفحه‌کلید",
"settings.close": "بستن",
"settings.content_warnings": "هشدارهای محتوا",
"settings.media": "رسانه",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Voir le profil complet",
"boost_modal.missing_description": "Ce post contient des médias sans description",
"column.favourited_by": "Ajouté en favori par",
"column.heading": "Divers",
"column.reblogged_by": "Partagé par",
"column.subheading": "Autres options",
"column_header.profile": "Profil",
"column_subheading.lists": "Listes",
"column_subheading.navigation": "Navigation",
"community.column_settings.allow_local_only": "Afficher seulement les posts locaux",
"compose.attach.doodle": "Dessinez quelque chose",
"compose.change_federation": "Changer les paramètres de fédération",
@@ -24,6 +20,9 @@
"compose.content-type.plain_meta": "Écrire sans formatage avancé",
"compose.disable_threaded_mode": "Désactiver le mode thread",
"compose.enable_threaded_mode": "Activer le mode thread",
"compose_form.sensitive.hide": "{count, plural, one {Marquer le média comme sensible} other {Marquer les médias comme sensibles}}",
"compose_form.sensitive.marked": "{count, plural, one {Le média est marqué comme sensible} other {Les médias sont marqués comme sensibles}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Le média nest pas marqué comme sensible} other {Les médias ne sont pas marqué comme sensible}}",
"confirmation_modal.do_not_ask_again": "Ne plus demander confirmation",
"confirmations.deprecated_settings.confirm": "Utiliser les préférences de Mastodon",
"confirmations.deprecated_settings.message": "Certaines {app_settings} de glitch-soc que vous utilisez ont été remplacées par les {preferences} de Mastodon et seront remplacées :",
@@ -42,8 +41,7 @@
"keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité",
"moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.",
"navigation_bar.app_settings": "Paramètres de l'application",
"navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
"navigation_bar.misc": "Autres",
"notifications.column_settings.filter_bar.show_bar": "Afficher la barre de filtre",
"settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu",
"settings.close": "Fermer",
"settings.compose_box_opts": "Zone de rédaction",
@@ -57,6 +55,8 @@
"settings.content_warnings_unfold_opts": "Options de dépliement automatique",
"settings.deprecated_setting": "Cette option est maintenant définie par les {settings_page_link} de Mastodon",
"settings.enable_content_warnings_auto_unfold": "Déplier automatiquement les avertissements de contenu",
"settings.fullwidth_view": "Étirer les colonnes sur toute la largeur (mode bureau uniquement)",
"settings.fullwidth_view_hint": "Étire les colonnes pour remplir tout l'espace disponible.",
"settings.general": "Général",
"settings.hicolor_privacy_icons": "Indicateurs de confidentialité en couleurs",
"settings.hicolor_privacy_icons.hint": "Affiche les indicateurs de confidentialité dans des couleurs facilement distinguables",
@@ -105,11 +105,14 @@
"settings.tag_misleading_links.hint": "Ajouter une indication visuelle avec l'hôte cible du lien à chaque lien ne le mentionnant pas explicitement",
"settings.wide_view": "Vue élargie (mode ordinateur uniquement)",
"settings.wide_view_hint": "Étire les colonnes pour mieux remplir l'espace disponible.",
"status.filtered": "Filtré",
"status.has_audio": "Contient des fichiers audio attachés",
"status.has_pictures": "Contient des images attachées",
"status.has_preview_card": "Contient une carte de prévisualisation attachée",
"status.has_video": "Contient des vidéos attachées",
"status.hide": "Masquer la publication",
"status.in_reply_to": "Ce post est une réponse",
"status.is_poll": "Ce post est un sondage",
"status.local_only": "Visible uniquement depuis votre instance"
"status.local_only": "Visible uniquement depuis votre instance",
"status.show_filter_reason": "Afficher quand même"
}

View File

@@ -2,16 +2,12 @@
"about.fork_disclaimer": "Glitch-soc est un logiciel gratuit et open source, fork de Mastodon.",
"account.disclaimer_full": "Les informations ci-dessous peuvent être incomplètes.",
"account.follows": "Abonnements",
"account.suspended_disclaimer_full": "Cet utilisateur a été suspendu par un modérateur.",
"account.suspended_disclaimer_full": "Ce compte a été suspendu par un modérateur.",
"account.view_full_profile": "Voir le profil complet",
"boost_modal.missing_description": "Ce post contient des médias sans description",
"column.favourited_by": "Ajouté en favori par",
"column.heading": "Divers",
"column.reblogged_by": "Partagé par",
"column.subheading": "Autres options",
"column_header.profile": "Profil",
"column_subheading.lists": "Listes",
"column_subheading.navigation": "Navigation",
"community.column_settings.allow_local_only": "Afficher seulement les posts locaux",
"compose.attach.doodle": "Dessinez quelque chose",
"compose.change_federation": "Changer les paramètres de fédération",
@@ -20,10 +16,13 @@
"compose.content-type.html_meta": "Formatez vos messages en HTML",
"compose.content-type.markdown": "Markdown",
"compose.content-type.markdown_meta": "Formatez vos messages en Markdown",
"compose.content-type.plain": "Text brut",
"compose.content-type.plain": "Texte brut",
"compose.content-type.plain_meta": "Écrire sans formatage avancé",
"compose.disable_threaded_mode": "Désactiver le mode thread",
"compose.enable_threaded_mode": "Activer le mode thread",
"compose_form.sensitive.hide": "{count, plural, one {Marquer le média comme sensible} other {Marquer les médias comme sensibles}}",
"compose_form.sensitive.marked": "{count, plural, one {Le média est marqué comme sensible} other {Les médias sont marqués comme sensibles}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Le média nest pas marqué comme sensible} other {Les médias ne sont pas marqué comme sensible}}",
"confirmation_modal.do_not_ask_again": "Ne plus demander confirmation",
"confirmations.deprecated_settings.confirm": "Utiliser les préférences de Mastodon",
"confirmations.deprecated_settings.message": "Certaines {app_settings} de glitch-soc que vous utilisez ont été remplacées par les {preferences} de Mastodon et seront remplacées :",
@@ -42,8 +41,7 @@
"keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité",
"moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.",
"navigation_bar.app_settings": "Paramètres de l'application",
"navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
"navigation_bar.misc": "Autres",
"notifications.column_settings.filter_bar.show_bar": "Afficher la barre de filtre",
"settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu",
"settings.close": "Fermer",
"settings.compose_box_opts": "Zone de rédaction",
@@ -57,6 +55,8 @@
"settings.content_warnings_unfold_opts": "Options de dépliement automatique",
"settings.deprecated_setting": "Cette option est maintenant définie par les {settings_page_link} de Mastodon",
"settings.enable_content_warnings_auto_unfold": "Déplier automatiquement les avertissements de contenu",
"settings.fullwidth_view": "Étirer les colonnes sur toute la largeur (mode bureau uniquement)",
"settings.fullwidth_view_hint": "Étire les colonnes pour remplir tout l'espace disponible.",
"settings.general": "Général",
"settings.hicolor_privacy_icons": "Indicateurs de confidentialité en couleurs",
"settings.hicolor_privacy_icons.hint": "Affiche les indicateurs de confidentialité dans des couleurs facilement distinguables",
@@ -76,7 +76,7 @@
"settings.pop_in_player": "Activer le lecteur pop-in",
"settings.pop_in_position": "Position du lecteur pop-in :",
"settings.pop_in_right": "Droite",
"settings.preferences": "Preferences",
"settings.preferences": "Préferences",
"settings.prepend_cw_re": "Préfixer les avertissements avec \"re: \" lors d'une réponse",
"settings.preselect_on_reply": "Présélectionner les noms dutilisateur·rices lors de la réponse",
"settings.preselect_on_reply_hint": "Présélectionner les noms d'utilisateurs après le premier lors d'une réponse à une conversation à plusieurs participants",
@@ -105,11 +105,14 @@
"settings.tag_misleading_links.hint": "Ajouter une indication visuelle avec l'hôte cible du lien à chaque lien ne le mentionnant pas explicitement",
"settings.wide_view": "Vue élargie (mode ordinateur uniquement)",
"settings.wide_view_hint": "Étire les colonnes pour mieux remplir l'espace disponible.",
"status.filtered": "Filtré",
"status.has_audio": "Contient des fichiers audio attachés",
"status.has_pictures": "Contient des images attachées",
"status.has_preview_card": "Contient une carte de prévisualisation attachée",
"status.has_video": "Contient des vidéos attachées",
"status.hide": "Masquer la publication",
"status.in_reply_to": "Ce post est une réponse",
"status.is_poll": "Ce post est un sondage",
"status.local_only": "Visible uniquement depuis votre instance"
"status.local_only": "Visible uniquement depuis votre instance",
"status.show_filter_reason": "Afficher quand même"
}

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Ver perfil completo",
"boost_modal.missing_description": "Esta publicación contén multimedia sen descrición",
"column.favourited_by": "Favorecida por",
"column.heading": "Varios",
"column.reblogged_by": "Promovida por",
"column.subheading": "Opcións diversas",
"column_header.profile": "Perfil",
"column_subheading.lists": "Listas",
"column_subheading.navigation": "Navegación",
"community.column_settings.allow_local_only": "Ver só mensaxes de ámbito local",
"compose.attach.doodle": "Debuxa algo",
"compose.change_federation": "Cambiar axustes de federación",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "para enviar publicación usando axuste secundario de privacidade",
"moved_to_warning": "Esta conta moveuse a {moved_to_link}, e non acepta novos seguimentos.",
"navigation_bar.app_settings": "Axustes da app",
"navigation_bar.keyboard_shortcuts": "Atallos de teclado",
"navigation_bar.misc": "Varios",
"notifications.column_settings.filter_bar.show_bar": "Amosar barra de filtros",
"settings.always_show_spoilers_field": "Activar sempre o campo de Aviso sobre o contido CW",
"settings.close": "Pechar",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Tampilkan profil lengkap",
"boost_modal.missing_description": "Toot ini berisi beberapa media tanpa deskripsi",
"column.favourited_by": "Disukai oleh",
"column.heading": "Lainnya",
"column.reblogged_by": "Dibagikan oleh",
"column.subheading": "Opsi lain-lain",
"column_header.profile": "Profil",
"column_subheading.lists": "Daftar",
"column_subheading.navigation": "Penelusuran",
"community.column_settings.allow_local_only": "Tampilkan toot lokal saja",
"compose.change_federation": "Ubah pengaturan federasi",
"compose.content-type.change": "Ubah opsi pemformatan lanjutan",
@@ -38,8 +34,6 @@
"keyboard_shortcuts.secondary_toot": "untuk mengirim toot menggunakan pengaturan privasi sekunder",
"moved_to_warning": "Akun ini ditandai sebagai dipindahkan ke {moved_to_link}, dan karenanya tidak menerima pengikut baru.",
"navigation_bar.app_settings": "Pengaturan aplikasi",
"navigation_bar.keyboard_shortcuts": "Pintasan keyboard",
"navigation_bar.misc": "Lainnya",
"settings.always_show_spoilers_field": "Selalu aktifkan bidang Peringatan Konten",
"settings.close": "Tutup",
"settings.compose_box_opts": "Kotak tulis",
@@ -49,6 +43,7 @@
"settings.content_warnings.regexp": "Ekspresi reguler",
"settings.content_warnings_filter": "Peringatan konten yang tidak akan dibuka secara otomatis:",
"settings.content_warnings_shared_state": "Tampilkan/sembunyikan konten semua salinan sekaligus",
"settings.pop_in_right": "Kanan",
"settings.preferences": "Preferences",
"settings.status_icons_reply": "Indikator balasan",
"settings.status_icons_visibility": "Indikator privasi toot",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "正確な情報を見る",
"boost_modal.missing_description": "このトゥートには少なくとも1つの画像に説明が付与されていません",
"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.content-type.html": "HTML",
"compose.content-type.html_meta": "投稿に HTML を使用する",
@@ -33,8 +29,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.compose_box_opts": "コンポーズボックス設定",

View File

@@ -1,6 +1,5 @@
{
"compose.attach.doodle": "Ssuneɣ-d kra",
"navigation_bar.keyboard_shortcuts": "Inezgumen n unasiw",
"settings.close": "Mdel",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"

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": "연합 설정 변경",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "보조 프라이버시 설정으로 글 보내기",
"moved_to_warning": "이 계정은 {moved_to_link}로 이동한 것으로 표시되었고, 새 팔로우를 받지 않는 것 같습니다.",
"navigation_bar.app_settings": "앱 설정",
"navigation_bar.keyboard_shortcuts": "키보드 단축기",
"navigation_bar.misc": "다양한 옵션들",
"notifications.column_settings.filter_bar.show_bar": "필터 막대 표시",
"settings.always_show_spoilers_field": "열람주의 항목을 언제나 활성화",
"settings.close": "닫기",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Peržiūrėti visą profilį",
"boost_modal.missing_description": "Šis įrašas turi šiek tiek medijų be aprašų.",
"column.favourited_by": "Pamėgo",
"column.heading": "Įvairūs",
"column.reblogged_by": "Pasidalino",
"column.subheading": "Įvairios parinktys",
"column_header.profile": "Profilis",
"column_subheading.lists": "Sąrašai",
"column_subheading.navigation": "Naršymas",
"community.column_settings.allow_local_only": "Rodyti tik vietinius įrašus",
"compose.attach.doodle": "Nupiešti kažką",
"compose.change_federation": "Keisti federacijos nustatymus",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "siųsti įrašą naudojant antrinį privatumo nustatymą",
"moved_to_warning": "Ši paskyra pažymėta kaip perkelta į {moved_to_link}, todėl negali priimti naujų sekimų.",
"navigation_bar.app_settings": "Programėlės nustatymai",
"navigation_bar.keyboard_shortcuts": "Spartieji klavišai",
"navigation_bar.misc": "Įvairūs",
"notifications.column_settings.filter_bar.show_bar": "Rodyti filtro juostą",
"settings.always_show_spoilers_field": "Visada įjungti turinio įspėjimo lauką",
"settings.close": "Užverti",

View File

@@ -3,12 +3,8 @@
"account.view_full_profile": "Volledig profiel weergeven",
"boost_modal.missing_description": "Deze toot bevat media zonder beschrijving",
"column.favourited_by": "Favoriet door",
"column.heading": "Overige",
"column.reblogged_by": "Geboost door",
"column.subheading": "Diverse opties",
"column_header.profile": "Profiel",
"column_subheading.lists": "Lijsten",
"column_subheading.navigation": "Navigatie",
"community.column_settings.allow_local_only": "Toon alleen lokale toots",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Pokaż pełny profil",
"boost_modal.missing_description": "Ten wpis zawiera multimedialne załączniki bez opisu",
"column.favourited_by": "Polubiony przez",
"column.heading": "Różne",
"column.reblogged_by": "Podbity przez",
"column.subheading": "Różne opcje",
"column_header.profile": "Profil",
"column_subheading.lists": "Listy",
"column_subheading.navigation": "Nawigacja",
"community.column_settings.allow_local_only": "Pokazuj wyłącznie wpisy lokalne",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
@@ -28,8 +24,6 @@
"keyboard_shortcuts.secondary_toot": "aby opublikować wpis używając dodatkowych ustawień prywatności",
"moved_to_warning": "To konto oznaczone jest jako przeniesione do {moved_to_link} i może z tego powodu nie akceptować nowych obserwujących.",
"navigation_bar.app_settings": "Ustawienia aplikacji",
"navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
"navigation_bar.misc": "Różne",
"settings.always_show_spoilers_field": "Zawsze pokazuj pole ostrzeżenia o zawartości",
"settings.close": "Zamknij",
"settings.compose_box_opts": "Pole edycji",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "Ver o perfil completo",
"boost_modal.missing_description": "Este toot contém algumas mídias sem descrição",
"column.favourited_by": "Favoritado por",
"column.heading": "Diversos",
"column.reblogged_by": "Inpulsionado por",
"column.subheading": "Opções diversas",
"column_header.profile": "Perfil",
"column_subheading.lists": "Listas",
"column_subheading.navigation": "Navegação",
"community.column_settings.allow_local_only": "Mostrar os toots apenas locais",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
@@ -28,8 +24,6 @@
"keyboard_shortcuts.secondary_toot": "para enviar toot usando a configuração de privacidade secundária",
"moved_to_warning": "Esta conta foi como movida para {moved_to_link} e, portanto, pode não aceitar novos seguidores.",
"navigation_bar.app_settings": "Configurações do aplicativo",
"navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
"navigation_bar.misc": "Diversos",
"settings.always_show_spoilers_field": "Sempre ativar o campo Aviso de Conteúdo",
"settings.close": "Fechar",
"settings.compose_box_opts": "Caixa de composição",

View File

@@ -6,7 +6,6 @@
"account.view_full_profile": "Ver o perfil completo",
"boost_modal.missing_description": "Este post contém alguns media sem descrição",
"column.favourited_by": "Adicionado aos favoritos de",
"column.heading": "Diversos",
"moved_to_warning": "Esta conta mudou-se para {moved_to_link} e, portanto, pode não aceitar novos seguidores.",
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
"settings.always_show_spoilers_field": "Mostrar sempre o campo Aviso de Conteúdo",

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": "Изменить настройки федерации",
@@ -39,8 +35,6 @@
"keyboard_shortcuts.bookmark": "добавить закладку",
"moved_to_warning": "Этот аккаунт переехал на {moved_to_link}, и скорее всего не принимает новых подписчиков.",
"navigation_bar.app_settings": "Настройки приложения",
"navigation_bar.keyboard_shortcuts": "Сочетания клавиш",
"navigation_bar.misc": "Прочее",
"notifications.column_settings.filter_bar.show_bar": "Показать панель фильтров",
"settings.always_show_spoilers_field": "Всегда ставить предупреждение о содержании",
"settings.close": "Закрыть",

View File

@@ -6,12 +6,8 @@
"account.view_full_profile": "View full profile",
"boost_modal.missing_description": "This post contains some media withoot description",
"column.favourited_by": "Favouritit by",
"column.heading": "Misc",
"column.reblogged_by": "Boosted by",
"column.subheading": "Miscellaneous options",
"column_header.profile": "Profile",
"column_subheading.lists": "Lists",
"column_subheading.navigation": "Navigation",
"community.column_settings.allow_local_only": "Show local-only posts",
"compose.attach.doodle": "Draw something",
"compose.change_federation": "Change federation settins",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "tae send post using secondary privacy settin",
"moved_to_warning": "This account is marked as moved tae {moved_to_link}, an' may not accept new follows.",
"navigation_bar.app_settings": "App settins",
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
"navigation_bar.misc": "Misc",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
"settings.always_show_spoilers_field": "Always enable the Content Warning field",
"settings.close": "Close",

View File

@@ -4,12 +4,8 @@
"account.view_full_profile": "Visa full profil",
"boost_modal.missing_description": "Denna toot innehåller viss media utan beskrivning",
"column.favourited_by": "Favoritmarkerad av",
"column.heading": "Övrigt",
"column.reblogged_by": "Boostad av",
"column.subheading": "Övriga val",
"column_header.profile": "Profil",
"column_subheading.lists": "Listor",
"column_subheading.navigation": "Navigering",
"community.column_settings.allow_local_only": "Visa endast lokala toots",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",

View File

@@ -1,8 +1,5 @@
{
"about.fork_disclaimer": ".",
"account.disclaimer_full": ".",
"account.follows": "",
"community.column_settings.allow_local_only": "",
"account.follows": "ติดตาม",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View File

@@ -5,12 +5,8 @@
"account.view_full_profile": "Tam görünüm",
"boost_modal.missing_description": "Bu gönderi açıklaması olmayan medya içerir",
"column.favourited_by": "Tarafından favorilere eklendi",
"column.heading": "Diğer",
"column.reblogged_by": "Tarafından yükseltildi",
"column.subheading": "Diğer seçenekler",
"column_header.profile": "Profil",
"column_subheading.lists": "Listeler",
"column_subheading.navigation": "Gezinme",
"community.column_settings.allow_local_only": "Sadece yerel gönderileri göster",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown modu",

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.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
@@ -29,8 +25,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.compose_box_opts": "Compose box",

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": "更改联动设置",
@@ -45,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "使用另一隐私设置发送嘟文",
"moved_to_warning": "此账户已被标记为移至 {moved_to_link},并且似乎没有收到新粉丝。",
"navigation_bar.app_settings": "应用设置",
"navigation_bar.keyboard_shortcuts": "键盘快捷键",
"navigation_bar.misc": "杂项",
"notifications.column_settings.filter_bar.show_bar": "显示筛选栏",
"settings.always_show_spoilers_field": "始终显示内容警告框",
"settings.close": "关闭",

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": "變更聯邦設定",
@@ -24,6 +20,9 @@
"compose.content-type.plain_meta": "不使用進階格式撰寫",
"compose.disable_threaded_mode": "停用貼文串模式",
"compose.enable_threaded_mode": "啟用貼文串模式",
"compose_form.sensitive.hide": "{count, plural, other {將媒體標記為敏感內容}}",
"compose_form.sensitive.marked": "{count, plural, other {媒體已被標記為敏感內容}}",
"compose_form.sensitive.unmarked": "{count, plural, other {媒體未被標記為敏感內容}}",
"confirmation_modal.do_not_ask_again": "不要再顯示確認訊息",
"confirmations.deprecated_settings.confirm": "使用 Mastodon 偏好",
"confirmations.deprecated_settings.message": "您正在使用的某些特定於 glitch-soc 設備的 {app_settings} 已被 Mastodon {preferences} 所取代,並將被覆蓋:",
@@ -42,8 +41,6 @@
"keyboard_shortcuts.secondary_toot": "使用次要隱私設定來發布貼文",
"moved_to_warning": "此帳戶已標記為移至 {moved_to_link},因此可能不接受新的追隨者。",
"navigation_bar.app_settings": "應用程式設定",
"navigation_bar.keyboard_shortcuts": "鍵盤快速鍵",
"navigation_bar.misc": "雜項",
"notifications.column_settings.filter_bar.show_bar": "顯示過濾器",
"settings.always_show_spoilers_field": "永遠啟用內容警告欄位",
"settings.close": "關閉",
@@ -58,6 +55,8 @@
"settings.content_warnings_unfold_opts": "自動展開選項",
"settings.deprecated_setting": "此設定現在已由 Mastodon 的 {settings_page_link} 控制。",
"settings.enable_content_warnings_auto_unfold": "自動展開內容警告",
"settings.fullwidth_view": "將欄位延展至全寬(僅限桌面模式)",
"settings.fullwidth_view_hint": "將欄位延展以填滿所有可用空間。",
"settings.general": "一般設定",
"settings.hicolor_privacy_icons": "隱私圖示使用對比色",
"settings.hicolor_privacy_icons.hint": "用明亮且易於區分的顏色顯示隱私圖示",

View File

@@ -126,6 +126,9 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
? accountJSON.username
: accountJSON.display_name;
const accountNote =
accountJSON.note && accountJSON.note !== '<p></p>' ? accountJSON.note : '';
return AccountFactory({
...accountJSON,
moved: moved?.id,
@@ -142,8 +145,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
escapeTextContentForBrowser(displayName),
emojiMap,
),
note_emojified: emojify(accountJSON.note, emojiMap),
note_plain: unescapeHTML(accountJSON.note),
note_emojified: emojify(accountNote, emojiMap),
note_plain: unescapeHTML(accountNote),
url:
accountJSON.url.startsWith('http://') ||
accountJSON.url.startsWith('https://')

View File

@@ -619,8 +619,13 @@ export const composeReducer = (state = initialState, action) => {
}
if (action.status.get('poll')) {
let options = ImmutableList(action.status.get('poll').options.map(x => x.title));
if (options.size < action.maxOptions) {
options = options.push('');
}
map.set('poll', ImmutableMap({
options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
options: options,
multiple: action.status.get('poll').multiple,
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
}));
@@ -650,8 +655,13 @@ export const composeReducer = (state = initialState, action) => {
}
if (action.status.get('poll')) {
let options = ImmutableList(action.status.get('poll').options.map(x => x.title));
if (options.size < action.maxOptions) {
options = options.push('');
}
map.set('poll', ImmutableMap({
options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
options: options,
multiple: action.status.get('poll').multiple,
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
}));

View File

@@ -10,7 +10,7 @@ const initialState = ImmutableMap({
stretch : true,
side_arm : 'none',
side_arm_reply_mode : 'keep',
show_reply_count : false,
show_reply_count : true,
always_show_spoilers_field: false,
confirm_boost_missing_media_description: false,
confirm_before_clearing_draft: true,

View File

@@ -73,8 +73,15 @@ export default function statuses(state = initialState, action) {
switch(action.type) {
case STATUS_FETCH_REQUEST:
return state.setIn([action.id, 'isLoading'], true);
case STATUS_FETCH_FAIL:
return state.delete(action.id);
case STATUS_FETCH_FAIL: {
if (action.parentQuotePostId && action.error.status === 404) {
return state
.delete(action.id)
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
} else {
return state.delete(action.id);
}
}
case STATUS_IMPORT:
return importStatus(state, action.status);
case STATUSES_IMPORT:

View File

@@ -1893,7 +1893,7 @@ a.sparkline {
font-size: 15px;
line-height: 22px;
li {
> li {
counter-increment: step 1;
padding-inline-start: 2.5rem;
padding-bottom: 8px;

View File

@@ -992,6 +992,7 @@ body > [data-popper-placement] {
line-height: 20px;
letter-spacing: 0.1px;
color: $highlight-text-color;
background-color: var(--input-background-color);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -1922,7 +1923,10 @@ body > [data-popper-placement] {
}
.status__quote {
--quote-margin: 36px;
// --status-gutter-width is currently only set inside of
// .notification-ungrouped, so everywhere else this will fall back
// to the pixel values
--quote-margin: var(--status-gutter-width, 36px);
position: relative;
margin-block-start: 16px;
@@ -1933,7 +1937,7 @@ body > [data-popper-placement] {
border: var(--nested-card-border);
@container (width > 460px) {
--quote-margin: 56px;
--quote-margin: var(--status-gutter-width, 56px);
}
}
@@ -2394,6 +2398,7 @@ a .account__avatar {
.detailed-status__display-name,
.detailed-status__datetime,
.detailed-status__application,
.detailed-status__link,
.account__display-name {
text-decoration: none;
}
@@ -2426,7 +2431,8 @@ a.account__display-name {
}
.detailed-status__application,
.detailed-status__datetime {
.detailed-status__datetime,
.detailed-status__link {
color: inherit;
}
@@ -2612,8 +2618,9 @@ a.account__display-name {
}
.status__relative-time,
.detailed-status__datetime {
&:hover {
.detailed-status__datetime,
.detailed-status__link {
&:is(a):hover {
text-decoration: underline;
}
}
@@ -2913,7 +2920,6 @@ a.account__display-name {
&__pane {
height: 100%;
overflow: hidden;
pointer-events: none;
display: flex;
justify-content: flex-end;
min-width: 285px;
@@ -2925,7 +2931,6 @@ a.account__display-name {
&__inner {
position: fixed;
width: 285px;
pointer-events: auto;
height: 100%;
}
}
@@ -3939,16 +3944,18 @@ a.account__display-name {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 12px;
font-size: 16px;
font-weight: 400;
padding: 12px;
text-decoration: none;
overflow: hidden;
white-space: nowrap;
border: 0;
background: transparent;
color: $secondary-text-color;
background: transparent;
border: 0;
border-left: 4px solid transparent;
box-sizing: border-box;
&:hover,
&:focus,
@@ -11130,21 +11137,23 @@ noscript {
}
}
.status {
.status:not(.status--is-quote) {
border: 0;
padding: 0;
&__avatar {
width: 40px;
height: 40px;
}
}
.status__wrapper-direct {
background: transparent;
}
$icon-margin: 48px; // 40px avatar + 8px gap
.status {
// 40px avatar + 8px gap
--status-gutter-width: 48px;
}
.status--is-quote {
--status-gutter-width: 0;
}
.status__content,
.status__action-bar,
@@ -11158,16 +11167,16 @@ noscript {
.hashtag-bar,
.content-warning,
.filter-warning {
margin-inline-start: $icon-margin;
width: calc(100% - $icon-margin);
margin-inline-start: var(--status-gutter-width);
width: calc(100% - var(--status-gutter-width));
}
.more-from-author {
width: calc(100% - $icon-margin + 2px);
width: calc(100% - var(--status-gutter-width) + 2px);
}
.status__content__read-more-button {
margin-inline-start: $icon-margin;
margin-inline-start: var(--status-gutter-width);
}
.notification__report {

View File

@@ -589,11 +589,14 @@ code {
}
.stacked-actions {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 30px;
margin-bottom: 15px;
}
button:not(.button, .link-button, .help-button) {
.btn {
display: block;
width: 100%;
border: 0;
@@ -610,8 +613,6 @@ code {
cursor: pointer;
font-weight: 500;
outline: 0;
margin-bottom: 10px;
margin-inline-end: 10px;
&:last-child {
margin-inline-end: 0;

View File

@@ -107,7 +107,8 @@
cursor: pointer;
}
&.editable {
&.editable,
&.disabled {
align-items: center;
overflow: visible;
}
@@ -165,7 +166,8 @@
}
}
&__option.editable &__input {
&__option.editable &__input,
&__option.disabled &__input {
&:active,
&:focus,
&:hover {

View File

@@ -96,12 +96,17 @@ export const ensureComposeIsVisible = (getState) => {
};
export function setComposeToStatus(status, text, spoiler_text) {
return{
type: COMPOSE_SET_STATUS,
status,
text,
spoiler_text,
};
return (dispatch, getState) => {
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
dispatch({
type: COMPOSE_SET_STATUS,
status,
text,
spoiler_text,
maxOptions,
});
}
}
export function changeCompose(text) {
@@ -183,7 +188,7 @@ export function directCompose(account) {
};
}
export function submitCompose() {
export function submitCompose(successCallback) {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
@@ -239,6 +244,9 @@ export function submitCompose() {
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) {
const normalStatus = { ...status };
@@ -72,7 +81,7 @@ export function normalizeStatus(status, normalOldStatus) {
} else {
// If the status has a CW but no contents, treat the CW as if it were the
// status' contents, to avoid having a CW toggle with seemingly no effect.
if (normalStatus.spoiler_text && !normalStatus.content) {
if (normalStatus.spoiler_text && !normalStatus.content && !normalStatus.quote) {
normalStatus.content = normalStatus.spoiler_text;
normalStatus.spoiler_text = '';
}
@@ -86,6 +95,11 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
// 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;
}
@@ -125,6 +139,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 'mastodon/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,21 +89,27 @@ 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) {
return {
type: REDRAFT,
status,
raw_text,
return (dispatch, getState) => {
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
dispatch({
type: REDRAFT,
status,
raw_text,
maxOptions,
});
};
}

View File

@@ -1,10 +1,11 @@
import { apiRequestPost } from 'mastodon/api';
import type { Status, StatusVisibility } from 'mastodon/models/status';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
import type { StatusVisibility } from 'mastodon/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

@@ -37,7 +37,7 @@ export interface BaseApiAccountJSON {
roles?: ApiAccountJSON[];
statuses_count: number;
uri: string;
url: string;
url?: string;
username: string;
moved?: ApiAccountJSON;
suspended?: boolean;

View File

@@ -1,12 +1,30 @@
import { useCallback } from 'react';
import { useLinks } from 'mastodon/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

@@ -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 'mastodon/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 '@/mastodon/actions/timelines';
import { Icon } from '@/mastodon/components/icon';
import { IconButton } from '@/mastodon/components/icon_button';
import StatusContainer from '@/mastodon/containers/status_container';
import { StatusQuoteManager } from '@/mastodon/components/status_quoted';
import { usePrevious } from '@/mastodon/hooks/usePrevious';
import { useAppDispatch, useAppSelector } from '@/mastodon/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

@@ -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;
@@ -45,7 +44,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
activeStyle,
onClick,
onKeyDown,
onKeyPress,
onMouseDown,
active = false,
disabled = false,
@@ -85,16 +83,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) => {
@@ -161,7 +149,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

@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -11,13 +11,16 @@ import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { Icon } from 'mastodon/components/icon';
import StatusContainer from 'mastodon/containers/status_container';
import { domain } from 'mastodon/initial_state';
import type { Status } from 'mastodon/models/status';
import type { RootState } from 'mastodon/store';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import QuoteIcon from '../../images/quote.svg?react';
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'
@@ -208,7 +262,11 @@ export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
if (quote) {
return (
<StatusContainer {...props}>
<QuotedStatus quote={quote} contextType={props.contextType} />
<QuotedStatus
quote={quote}
parentQuotePostId={status?.get('id') as string}
contextType={props.contextType}
/>
</StatusContainer>
);
}

View File

@@ -6,6 +6,7 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
import { AccountBio } from '@/mastodon/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';
@@ -773,7 +774,6 @@ export const AccountHeader: React.FC<{
);
}
const content = { __html: account.note_emojified };
const displayNameHtml = { __html: account.display_name_html };
const fields = account.fields;
const isLocal = !account.acct.includes('@');
@@ -897,12 +897,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

@@ -73,6 +73,7 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool,
lang: PropTypes.string,
maxChars: PropTypes.number,
redirectOnSuccess: PropTypes.bool,
};
static defaultProps = {
@@ -310,7 +311,7 @@ class ComposeForm extends ImmutablePureComponent {
>
{intl.formatMessage(
this.props.isEditing ?
messages.saveChanges :
messages.saveChanges :
(this.props.isInReply ? messages.reply : messages.publish)
)}
</Button>

View File

@@ -34,7 +34,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));
@@ -47,7 +47,11 @@ const mapDispatchToProps = (dispatch) => ({
modalProps: {},
}));
} else {
dispatch(submitCompose());
dispatch(submitCompose((status) => {
if (props.redirectOnSuccess) {
window.location.assign(status.url);
}
}));
}
},

View File

@@ -50,16 +50,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',
@@ -77,7 +83,7 @@ export const MoreLink: React.FC = () => {
href: '/settings/export',
text: intl.formatMessage(messages.importExport),
},
);
];
if (canManageReports(permissions)) {
arr.push(null, {
@@ -106,7 +112,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

@@ -431,6 +431,7 @@ export const CollapsibleNavigationPanel: React.FC = () => {
filterTaps: true,
bounds: isLtrDir ? { left: 0 } : { right: 0 },
rubberband: true,
enabled: openable,
},
);

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

@@ -21,6 +21,7 @@ import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button';
import { useIdentity } from 'mastodon/identity_context';
import { me } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import type { Status } from 'mastodon/models/status';
import { makeGetStatus } from 'mastodon/selectors';
import type { RootState } from 'mastodon/store';
@@ -66,10 +67,7 @@ export const Footer: React.FC<{
const dispatch = useAppDispatch();
const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector;
const status = useAppSelector((state) => getStatus(state, { id: statusId }));
const accountId = status?.get('account') as string | undefined;
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
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 'mastodon/features/ui/containers/modal_container';
const Compose = () => (
<>
<ComposeFormContainer autoFocus withoutNavigation />
<ComposeFormContainer autoFocus withoutNavigation redirectOnSuccess />
<AlertsController />
<ModalContainer />
<LoadingBarContainer className='loading-bar' />

View File

@@ -32,7 +32,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

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

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'])}
aspectRatio={`${image.getIn(['meta', 'original', 'width'])} / ${image.getIn(['meta', 'original', 'height'])}`}
startTime={currentTime || 0}
@@ -219,8 +217,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

@@ -90,8 +90,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
layout: state.getIn(['meta', 'layout']),
isComposing: state.getIn(['compose', 'is_composing']),
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 < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']),
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
newAccount: !state.getIn(['accounts', me, 'note']) && !state.getIn(['accounts', me, 'bot']) && state.getIn(['accounts', me, 'following_count'], 0) === 0 && state.getIn(['accounts', me, 'statuses_count'], 0) === 0,
@@ -272,8 +271,7 @@ class UI extends PureComponent {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
isComposing: PropTypes.bool,
hasComposingText: PropTypes.bool,
hasMediaAttachments: PropTypes.bool,
hasComposingContents: PropTypes.bool,
canUploadMore: PropTypes.bool,
intl: PropTypes.object.isRequired,
layout: PropTypes.string.isRequired,
@@ -288,11 +286,11 @@ class UI extends PureComponent {
};
handleBeforeUnload = e => {
const { intl, dispatch, isComposing, hasComposingText, hasMediaAttachments } = this.props;
const { intl, dispatch, isComposing, hasComposingContents } = this.props;
dispatch(synchronouslySubmitMarkers());
if (isComposing && (hasComposingText || hasMediaAttachments)) {
if (isComposing && hasComposingContents) {
e.preventDefault();
// Setting returnValue to any string causes confirmation dialog.
// Many browsers no longer display this text to users,

View File

@@ -8,13 +8,14 @@ import { openURL } from 'mastodon/actions/search';
import { useAppDispatch } from 'mastodon/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

@@ -1,6 +1,7 @@
{
"about.blocks": "Gemodereerde bedieners",
"about.contact": "Kontak:",
"about.default_locale": "Verstek",
"about.disclaimer": "Mastodon is gratis oopbronsagteware en n handelsmerk van Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Rede nie beskikbaar nie",
"about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.",

View File

@@ -1,7 +1,7 @@
{
"about.blocks": "خوادم تحت الإشراف",
"about.contact": "للاتصال:",
"about.default_locale": "افتراضيالافتراضية",
"about.default_locale": "افتراضي",
"about.disclaimer": "ماستدون برنامج حر ومفتوح المصدر وعلامة تجارية لـ Mastodon GmbH.",
"about.domain_blocks.no_reason_available": "السبب غير متوفر",
"about.domain_blocks.preamble": "يتيح مَستُدون عمومًا لمستخدميه مطالعة المحتوى من المستخدمين من الخواديم الأخرى في الفدرالية والتفاعل معهم. وهذه هي الاستثناءات التي وضعت على هذا الخادوم.",
@@ -110,12 +110,12 @@
"announcement.announcement": "إعلان",
"annual_report.summary.archetype.booster": "The cool-hunter",
"annual_report.summary.archetype.lurker": "المتصفح الصامت",
"annual_report.summary.archetype.oracle": "حكيم",
"annual_report.summary.archetype.oracle": "الحكيم",
"annual_report.summary.archetype.pollster": "مستطلع للرأي",
"annual_report.summary.archetype.replier": "الفراشة الاجتماعية",
"annual_report.summary.followers.followers": "المُتابِعُون",
"annual_report.summary.followers.total": "{count} في المجمل",
"annual_report.summary.here_it_is": "هذا ملخص الخص بك لسنة {year}:",
"annual_report.summary.here_it_is": "فيما يلي ملخصك لسنة {year}:",
"annual_report.summary.highlighted_post.by_favourites": "المنشور ذو أعلى عدد تفضيلات",
"annual_report.summary.highlighted_post.by_reblogs": "أكثر منشور مُعاد نشره",
"annual_report.summary.highlighted_post.by_replies": "المنشور بأعلى عدد تعليقات",
@@ -873,6 +873,7 @@
"status.open": "وسّع هذا المنشور",
"status.pin": "دبّسه على الصفحة التعريفية",
"status.quote_error.filtered": "مُخفي بسبب إحدى إعدادات التصفية خاصتك",
"status.quote_error.limited_account_hint.action": "إظهاره على أي حال",
"status.quote_error.not_found": "لا يمكن عرض هذا المنشور.",
"status.quote_error.pending_approval": "هذا المنشور ينتظر موافقة صاحب المنشور الأصلي.",
"status.quote_error.rejected": "لا يمكن عرض هذا المنشور لأن صاحب المنشور الأصلي لا يسمح له بأن يكون مقتبس.",

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