Compare commits

...

228 commits

Author SHA1 Message Date
065aa1a32c Merge branch 'main' into bark-prod
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-26 13:39:14 +02:00
61c71f9c5b Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-26 13:26:00 +02:00
github-actions[bot]
5201882a23
New Crowdin Translations (automated) (#30077)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-26 10:05:18 +00:00
Claire
4ef0b48b95
Add in-app notifications for moderation actions/warnings (#30065) 2024-04-25 17:26:05 +00:00
Eugen Rochko
0ec061aa8f
Change design of people tab on explore in web UI (#30059) 2024-04-25 16:25:33 +00:00
github-actions[bot]
85fdbd0ad5
New Crowdin Translations (automated) (#30062)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-25 08:50:54 +00:00
Matt Jankowski
d9eee9bf9a
Remove column defaults for status_pins timestamp columns (#29261) 2024-04-24 14:56:54 +00:00
Tim Rogers
b128474625
Fixed rendering of excess whitespace in status card titles (#30017) 2024-04-24 09:09:21 +00:00
Matt Jankowski
f4a53f3fb4
Extract constants for column size length validation limits (#30045) 2024-04-24 08:56:28 +00:00
github-actions[bot]
ebcf9840f4
New Crowdin Translations (automated) (#30050)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-24 08:45:24 +00:00
Eugen Rochko
74012831f6
Change mute options to be in dropdown on muted users list in web UI (#30049) 2024-04-24 08:45:12 +00:00
Matt Jankowski
b903e6909e
Disable Style/HashAsLastArrayItem cop (#30041) 2024-04-24 08:32:18 +00:00
Renaud Chaput
0e585b9a52
Update to Ruby 3.2.4 (#30036) 2024-04-24 08:21:05 +00:00
Matt Jankowski
3f6887557b
Move JS source from packs to entrypoints (#30037) 2024-04-23 16:45:12 +00:00
Matt Jankowski
32ead51e5a
Add material design icons to admin/settings views (#27780)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-04-23 16:43:49 +00:00
Claire
a2399046ca
Fix string interpolation for software updates admin mailer (#30035) 2024-04-23 12:54:52 +00:00
12ef898447 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-23 13:55:48 +02:00
Emelia Smith
049b159beb
Add read:me OAuth 2.0 scope, allowing more limited access to user data (#29087) 2024-04-23 11:47:00 +00:00
github-actions[bot]
d754b15afb
New Crowdin Translations (automated) (#30034)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-23 09:07:05 +00:00
renovate[bot]
91c7406b59
Update dependency postcss-preset-env to v9.5.9 (#30029)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-23 08:41:50 +00:00
renovate[bot]
1471c0d4e0
Update dependency rubocop to v1.63.3 (#30031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-23 08:41:33 +00:00
renovate[bot]
483fabf48a
Update dependency http to '~> 5.2.0' (#30027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-04-22 14:02:47 +00:00
Claire
2ef098d01c
Revert "Rely on dotenv autoload instead of explicit call (#30007)" (#30028) 2024-04-22 14:02:24 +00:00
Matt Jankowski
33e829763d
Use shared form partial for admin/domain_blocks views (#29609) 2024-04-22 12:22:16 +00:00
Matt Jankowski
ffbbf74c50
Limit http gem version to 5.1.x series (#30010) 2024-04-22 09:01:36 +00:00
Matt Jankowski
56b095edeb
Update Gemfile.lock ruby and bundler versions (#30011) 2024-04-22 09:01:24 +00:00
Tim Rogers
1ca6ff8ca5
Fixed crash when supplying FFMPEG_BINARY environment variable (#30022) 2024-04-22 09:00:24 +00:00
Tim Rogers
75163d9daf
Fixed rendering error on /start when not logged in (#30023) 2024-04-22 08:53:08 +00:00
github-actions[bot]
3655fb6a22
New Crowdin Translations (automated) (#30014)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-22 08:42:35 +00:00
Matt Jankowski
18737aad49
Rely on dotenv autoload instead of explicit call (#30007) 2024-04-22 08:31:20 +00:00
Matt Jankowski
a15139bc02
Fix intermittent order based failure in UpdateStatusService spec (#30008) 2024-04-22 08:30:38 +00:00
renovate[bot]
24e67c4394
Update dependency postcss-preset-env to v9.5.8 (#30018)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 08:24:08 +00:00
renovate[bot]
3e21af3e4a
Update dependency @types/react to v18.2.79 (#30024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 08:14:09 +00:00
renovate[bot]
88f946890d
Update peter-evans/create-pull-request action to v6.0.4 (#30025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 08:13:28 +00:00
renovate[bot]
223936c2e8
Update eslint (non-major) to v7.7.0 (#30026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 08:13:04 +00:00
Matt Jankowski
2ec9bff36e
Fix Rubocop Rails/UniqueValidationWithoutIndex cop (#27461)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-04-22 08:04:05 +00:00
3b11f173b3 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-22 09:59:07 +02:00
Matt Jankowski
35b517c207
Remove remnants of capistrano (#30009) 2024-04-19 20:53:01 +00:00
Matt Jankowski
369b2ef0ed
Fix Style/TrailingCommaInHashLiteral cop (#30004) 2024-04-19 20:52:01 +00:00
Matt Jankowski
c7384adc00
Fix Style/TrailingCommaInArguments cop (#30003) 2024-04-19 20:37:18 +00:00
Matt Jankowski
b6f04aed35
Silence warning about requiring rubocop-rspec_rails (#30002) 2024-04-19 20:35:00 +00:00
Matt Jankowski
933189887b
Fix Style/StringLiterals cop (#30005) 2024-04-19 20:33:00 +00:00
Matt Jankowski
8d47ba893a
Fix Style/PercentLiteralDelimiters cop (#30006) 2024-04-19 20:32:26 +00:00
renovate[bot]
d24462c81a
Update dependency test-prof to v1.3.3 (#30000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 19:46:13 +00:00
Matt Jankowski
fa5e154ee3
Indirect deps gem version bumps (#29998) 2024-04-19 19:45:41 +00:00
Matt Jankowski
f5d341382e
Add any_args to have_enqueued_sidekiq_job call (quiets deprecation) (#29999) 2024-04-19 19:44:59 +00:00
Matt Jankowski
f386eb6c63
Replace deprecated dotenv-rails gem with dotenv gem (#29173) 2024-04-19 14:25:14 +00:00
github-actions[bot]
ec71c02c4b
New Crowdin Translations (automated) (#29994)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-19 13:57:43 +00:00
Matt Jankowski
4837bfcc6a
Use shared form partial for admin/announcements views (#29608) 2024-04-19 13:57:32 +00:00
renovate[bot]
e5d5bd7ff1
Update dependency postcss-preset-env to v9.5.6 (#29983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-18 16:24:55 +00:00
Matt Jankowski
75f9c652e2
Fix Bundler/OrderedGems cop (#28400) 2024-04-18 16:24:22 +00:00
github-actions[bot]
443186ff40
New Crowdin Translations (automated) (#29980)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-18 11:18:39 +00:00
Matt Jankowski
11e0049b08
Use enum-generated scopes/queries for BulkImport (#29975) 2024-04-18 10:13:35 +00:00
renovate[bot]
630572323f
Update dependency ioredis to v5.4.1 (#29977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 21:52:26 +00:00
61cd0f81b5 max fields
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-17 16:55:18 +02:00
renovate[bot]
1ad119941f
Update dependency rspec-sidekiq to v4.2.0 (#29964)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 11:14:09 +00:00
Claire
8bece467f8
Change have_enqueued_sidekiq_job usage to always make argument expectations explicit (#29974) 2024-04-17 11:13:52 +00:00
Matt Jankowski
013671f29f
Rename JS testing section in GH Actions config (#29931) 2024-04-17 10:23:07 +00:00
Matt Jankowski
650c548c31
Add not_featured_by scope to Tag (#28815) 2024-04-17 10:05:38 +00:00
Matt Jankowski
1d3ecd3fba
Add API::Pagination concern (#28826) 2024-04-17 09:22:45 +00:00
Matt Jankowski
828299e71c
Enable AR Encryption (#29831) 2024-04-17 09:19:02 +00:00
renovate[bot]
a390299744
Update dependency ioredis to v5.4.0 (#29969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 09:08:19 +00:00
renovate[bot]
ee8f999a7b
Update dependency core-js to v3.37.0 (#29968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 09:08:08 +00:00
github-actions[bot]
c35042b7eb
New Crowdin Translations (automated) (#29972)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-17 09:07:13 +00:00
Claire
fc89ecc6ca
Change /api/v1/announcements to return regular Status entities (#26736) 2024-04-17 09:06:23 +00:00
Matt Jankowski
9ae2594726
Add reusable duplicate ID finder methods in maintenance CLI (#28910) 2024-04-17 09:00:08 +00:00
renovate[bot]
03abff3b30
Update dependency aws-sdk-s3 to v1.147.0 (#29967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 08:30:58 +00:00
Matt Jankowski
9ce2db4136
Combine double subject runs and DRY up change check in bulk import service spec (#29402) 2024-04-17 08:23:25 +00:00
Matt Jankowski
6fed108703
Use Rails upsert to generate update_count! query in Counters concern (#28738)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-04-17 08:16:51 +00:00
Claire
5915bd7f45
Fix development environment admin account not being auto-approved (#29958) 2024-04-16 17:30:32 +00:00
Matt Jankowski
caad1e2628
Add scope Status.distributable_visibility (#29950) 2024-04-16 13:16:54 +00:00
renovate[bot]
0622107449
Update dependency @testing-library/react to v15 (#29893)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 09:44:02 +00:00
Matt Jankowski
6b33d3f81b
Add CustomFilter.unexpired scope (#29896) 2024-04-16 09:29:34 +00:00
Claire
66ee0d4a1f
Fix incorrect label for filtered notifications badge (#29922) 2024-04-16 09:25:23 +00:00
Matt Jankowski
3159c0a547
Add scope Status.list_eligible_visibility (#29951) 2024-04-16 09:17:03 +00:00
renovate[bot]
e6927db2fe
Update dependency rubocop to v1.63.2 (#29959)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 08:58:04 +00:00
renovate[bot]
6ee1b034b6
Update dependency prom-client to v15.1.2 (#29957)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 08:45:20 +00:00
github-actions[bot]
285d4123b5
New Crowdin Translations (automated) (#29955)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-16 08:36:21 +00:00
Matt Jankowski
7fed4a9740
Pull out repeated setup to shared setup in statusus/show view spec (#29927) 2024-04-15 15:24:31 +00:00
Claire
4117c8f6b8
Fix unfollow button being out of frame on small screens on old browsers (#29923) 2024-04-15 11:56:48 +00:00
Matt Jankowski
1549e6a9dc
Drop support for Ruby 3.0 (reaching EOL) (#29702) 2024-04-15 10:19:23 +00:00
github-actions[bot]
4e78cb9988
New Crowdin Translations (automated) (#29939)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-15 09:29:39 +00:00
Matt Jankowski
0d9ad96d3f
Rename PremailerWebpackStrategy -> PremailerBundledAssetStrategy (#29934) 2024-04-15 09:16:59 +00:00
Matt Jankowski
bf5d948237
Fix Style/SingleArgumentDig cop in webpacker/manifest_extensions (#29929) 2024-04-15 09:15:32 +00:00
Renaud Chaput
67dd1763bb
Fix PostCSS config (#29926) 2024-04-15 09:06:06 +00:00
Renaud Chaput
ee4ea83a87
Remove image_pack_tag usage (#29925) 2024-04-15 09:05:19 +00:00
renovate[bot]
34e826f373
Update dependency typescript to v5.4.5 (#29945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-15 07:28:29 +00:00
renovate[bot]
1906330d13
Update dependency react-redux to v9.1.1 (#29943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-15 07:27:56 +00:00
renovate[bot]
80edd7a317
Update DefinitelyTyped types (non-major) (#29944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-15 07:27:33 +00:00
renovate[bot]
e3dd60cce1
Update peter-evans/create-pull-request action to v6.0.3 (#29946)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-15 07:26:57 +00:00
renovate[bot]
86c53e175d
Update dependency @testing-library/react to v14.3.1 (#29947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-15 07:26:41 +00:00
renovate[bot]
c4d6e10115
Update dependency rubocop to v1.63.1 (#29878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-12 10:57:37 +00:00
renovate[bot]
67a37c7279
Update dependency sass to v1.75.0 (#29919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-12 10:56:49 +00:00
Matt Jankowski
61d108f415
Extract header_tags method in statuses/show view spec (#29907) 2024-04-12 09:50:46 +00:00
renovate[bot]
8986e3b088
Update dependency webpack-bundle-analyzer to v4.10.2 (#29909)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-12 09:48:13 +00:00
Claire
c386c36866
Add / keyboard shortcut to focus the search field (#29921) 2024-04-12 09:42:12 +00:00
renovate[bot]
3f821e0d5e
Update dependency postcss-preset-env to v9.5.5 (#29912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-12 09:41:44 +00:00
Matt Jankowski
da6b9238f5
Expand coverage for admin/metrics/measure/* classes (#29914) 2024-04-12 09:38:24 +00:00
Matt Jankowski
ec5a0e0f5e
Expand coverage for admin/metrics/dimension/* classes (#29913) 2024-04-12 09:18:35 +00:00
Matt Jankowski
13bbde2246
Use existing DEFAULT_FIELDS_SIZE constant to limit Account#fields (#29911) 2024-04-12 09:04:23 +00:00
github-actions[bot]
5992df0762
New Crowdin Translations (automated) (#29920)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-12 08:19:48 +00:00
Matt Jankowski
449f99e168
Fix repeated concat output buffer duplicating layout markup (#29918) 2024-04-11 23:37:07 +00:00
github-actions[bot]
20b1e55f24
New Crowdin Translations (automated) (#29903)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-11 10:30:31 +00:00
renovate[bot]
4826c1da8a
Update dependency devise to v4.9.4 (#29890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-11 09:00:15 +00:00
Matt Jankowski
576554b19b
Use fabrication sequence in domain values (#29895) 2024-04-11 08:59:01 +00:00
renovate[bot]
96bdeeed0e
Update dependency nokogiri to v1.16.4 (#29900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-11 08:50:34 +00:00
github-actions[bot]
db5a5636d9
New Crowdin Translations (automated) (#29888)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-10 13:03:40 +00:00
Matt Jankowski
4565015615
Fix Style/MapIntoArray cop in cli progress helper (#29884) 2024-04-10 12:46:43 +00:00
Matt Jankowski
b57ee5cf5b
Fix Style/MapIntoArray cop in context helper (#29885) 2024-04-10 12:46:39 +00:00
Matt Jankowski
4948a063d2
Use tt extension for form scaffold template (#29676) 2024-04-10 09:20:21 +00:00
renovate[bot]
b8dca8d22a
Update dependency typescript to v5.4.4 (#29875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-08 14:08:32 +00:00
Matt Jankowski
285f63c02e
Use composable query in User.active scope (#29775) 2024-04-08 13:53:49 +00:00
Claire
babbf6017d
Remove caching in cache_collection (#29862) 2024-04-08 13:46:13 +00:00
Claire
f3430eebbb
Fix hashtag string interpolation in welcome email (#29879) 2024-04-08 13:45:25 +00:00
github-actions[bot]
13faf26315
New Crowdin Translations (automated) (#29859)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-08 13:44:54 +00:00
Renaud Chaput
730e2127e1
Fix webpack warnings due to unhandled extensions (LICENCE and README.md) (#29869) 2024-04-08 08:17:51 +00:00
renovate[bot]
a1277a9b2b
Update eslint (non-major) (#29877)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-08 08:17:23 +00:00
renovate[bot]
2441fe6fd4
Update dependency stylelint-config-standard-scss to v13.1.0 (#29876)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-08 08:16:57 +00:00
renovate[bot]
b06510d579
Update DefinitelyTyped types (non-major) (#29874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-08 08:16:36 +00:00
renovate[bot]
499b184fcd
Update dependency pino to v8.20.0 (#29865)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-08 08:16:20 +00:00
renovate[bot]
79bbb2023d
Update dependency react-intl to v6.6.5 (#29864)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-08 08:15:46 +00:00
Claire
e73cf356d2
Move OAuth flow tests from feature tests to system tests (#29837) 2024-04-05 16:52:05 +00:00
Matt Jankowski
b61ae28f8d
Separate methods for theme style and meta color tags (#29802) 2024-04-05 09:52:43 +00:00
ache
52ab8a59c6
Forward 3035 port (#29710) 2024-04-05 09:19:43 +00:00
Matt Jankowski
c0fe8a9f13
Extract shared callback behaviour to CustomFilterCache concern (#29695) 2024-04-05 09:17:58 +00:00
renovate[bot]
285a87a77f
Update dependency scenic to v1.8.0 (#29794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-05 09:04:10 +00:00
Matt Jankowski
59da591d13
Improve spec for dimension/languages admin metric (#29842) 2024-04-05 08:54:11 +00:00
Renaud Chaput
6ac90d4c5d
Add tests for our number formatting function (#29852) 2024-04-05 08:06:31 +00:00
Renaud Chaput
906a399634
Fix wrong extension for a test file (#29853) 2024-04-05 07:57:44 +00:00
renovate[bot]
c7378218ba
Update dependency rubocop-rspec to v2.29.1 (#29858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-05 07:57:20 +00:00
Claire
38b9d31f63
Improve email address validation (#29838) 2024-04-05 07:48:45 +00:00
Claire
1f11aa5f04
Add stub for trending tags in user mailer spec (#29850) 2024-04-05 07:48:07 +00:00
Matt Jankowski
601834d746
Use partial collection render for welcome mailer features (#29843) 2024-04-04 16:13:10 +00:00
Matt Jankowski
191bf5876e
Add coverage for sanitize failure path in api/web/embeds spec (#29851) 2024-04-04 16:07:16 +00:00
Michael Stanclift
1c87cb8019
Add purple border to active compose field search inputs (#29839) 2024-04-04 11:51:06 +00:00
Matt Jankowski
966d7f5bf9
Add missing snowflake range correction (#29841) 2024-04-04 11:33:17 +00:00
Jeong Arm
4045c069f8
Use public_visibility (#29847) 2024-04-04 07:31:30 +00:00
renovate[bot]
91d3b3fb25
Update dependency sass to v1.74.1 (#29846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-04 07:25:30 +00:00
renovate[bot]
58dfc12af2
Update babel monorepo to v7.24.4 (#29840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-04 07:23:53 +00:00
nicolas
c6da3ee828
Makes the star icon rotate around its actual centre axis (#29844) 2024-04-03 21:10:02 +00:00
Matt Jankowski
cde3206478
Simplify feature loop in welcome mailer (#29760) 2024-04-03 20:10:59 +00:00
renovate[bot]
37d984b8bf
Update eslint (non-major) (#29820)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2024-04-03 18:05:39 +00:00
Michael Stanclift
e284417349
Fix blue border on emoji/language search in Safari & Chrome (#29832) 2024-04-03 15:19:10 +00:00
Michael Stanclift
5d67247061
Fix language and emoji search field background colors on light theme (#29828) 2024-04-03 14:22:50 +00:00
github-actions[bot]
56d13069cd
New Crowdin Translations (automated) (#29836)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-03 09:04:29 +00:00
renovate[bot]
4233ee1f59
Update dependency pg to v8.11.5 (#29833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 21:33:43 +00:00
renovate[bot]
54c7d1ad14
Update dependency pg-connection-string to v2.6.4 (#29814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 21:22:22 +00:00
Matt Jankowski
a7b637768e
Regenerate rubocop todo with version 1.62.1 (#29830) 2024-04-02 19:56:54 +00:00
renovate[bot]
88c3664889
Update dependency rubocop-performance to v1.21.0 (#29804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 17:00:07 +00:00
Matt Jankowski
d27eb181f6
Reduce LineLength from 320 to 300 (#29636) 2024-04-02 15:50:57 +00:00
renovate[bot]
f8b03c3925
Update dependency faker to v3.3.1 (#29829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 15:48:27 +00:00
Matt Jankowski
edde54e991
Update stoplight to version 4.1.0 (#28366) 2024-04-02 15:47:40 +00:00
Matt Jankowski
921c4c1273
Match comment style of FeedManager list/tags checks (#29639)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2024-04-02 14:07:31 +00:00
Claire
0b9d4103cb
Fix contrast in notification request badge (#29826) 2024-04-02 14:05:46 +00:00
Matt Jankowski
f87959ab50
Fix RSpec/LetSetup cop in api/v1/timelines/public spec (#28972) 2024-04-02 14:05:02 +00:00
Claire
54119570e6
Remove dependency on posix-spawn (#18559) 2024-04-02 14:02:07 +00:00
Matt Jankowski
34489591ec
Add max_pinned_statuses to instances serializer and api response (#29441) 2024-04-02 13:54:11 +00:00
Matt Jankowski
f56309f5f0
Add by_latest_used scope, move admin area recent IPs to partial (#29497) 2024-04-02 13:51:34 +00:00
renovate[bot]
c70c39cad0
Update docker/dockerfile Docker tag to v1.7 (#27993)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 13:47:15 +00:00
renovate[bot]
b0692d994f
Update dependency letter_opener to v1.10.0 (#29189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 13:31:03 +00:00
renovate[bot]
695dded7ed
Update dependency aws-sdk-s3 to v1.146.1 (#28961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 13:29:46 +00:00
renovate[bot]
f6e24bbd79
Update dependency postcss-preset-env to v9.5.4 (#29825)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 12:13:43 +00:00
Renaud Chaput
b4d991adaa
Use integers and not numbers in notification policy API counters (#29810) 2024-04-02 10:06:26 +00:00
renovate[bot]
d05f62391d
Update dependency public_suffix to v5.0.5 (#29824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 10:04:32 +00:00
Renaud Chaput
e47a3d00fe
Add API types for status and related objects (#29792) 2024-04-02 10:03:33 +00:00
Renaud Chaput
07635228e2
Fix Redux Middleware types (#29800) 2024-04-02 09:56:03 +00:00
Matt Jankowski
a3fe82e359
Rename cop RSpec/Rails/HttpStatus to RSpecRails/HttpStatus (#29806) 2024-04-02 09:34:44 +00:00
github-actions[bot]
c717747603
New Crowdin Translations (automated) (#29812)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-04-02 09:26:06 +00:00
Michael Stanclift
fa9574086d
Fix search box color on light theme (#29808) 2024-04-02 09:15:31 +00:00
renovate[bot]
143d9553fa
Update dependency fastimage to v2.3.1 (#29822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 09:15:24 +00:00
renovate[bot]
a4158be4d7
Update devDependencies (non-major) (#29819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 08:59:52 +00:00
renovate[bot]
2f7a2d4df7
Update DefinitelyTyped types (non-major) (#29818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 08:59:26 +00:00
renovate[bot]
b58666e12e
Update dependency @reduxjs/toolkit to v2.2.3 (#29817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 08:59:05 +00:00
renovate[bot]
9611023380
Update dependency postcss-preset-env to v9.5.3 (#29816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 08:58:00 +00:00
renovate[bot]
173adb04e2
Update dependency pg to v8.11.4 (#29813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 08:57:14 +00:00
renovate[bot]
589e34d00c
Update dependency selenium-webdriver to v4.19.0 (#29776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-04-02 08:53:44 +00:00
Michael Stanclift
90eb4a5d01
Fix light theme header on mobile (#29809) 2024-03-29 21:03:30 +00:00
Michael Stanclift
430da03160
Fix background tint in single column light theme (#29803) 2024-03-29 17:16:51 +00:00
Renaud Chaput
69e5771881
Handle createAppAsyncThunk rejected actions in the errors middleware (#29791) 2024-03-29 13:57:39 +00:00
github-actions[bot]
f96648d41c
New Crowdin Translations (automated) (#29796)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-03-29 13:35:50 +00:00
Renaud Chaput
672c9f5f05
Change the theme-color value automatically when using a built-in theme (#29795) 2024-03-29 13:32:07 +00:00
renovate[bot]
671167f6da
Update dependency glob to v10.3.12 (#29790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-29 13:31:13 +00:00
renovate[bot]
cd9d11dda6
Update dependency debug to v1.9.2 (#29799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-29 13:30:52 +00:00
Renaud Chaput
67442f9039
Remove global boosts state and convert boosts modal to Typescript (#29774) 2024-03-28 15:33:15 +00:00
github-actions[bot]
8a498f4e65
New Crowdin Translations (automated) (#29785)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-03-28 14:48:09 +00:00
Claire
4f068d4fcc
Fix logo pushing header buttons out of view on certain conditions in mobile layout (#29787) 2024-03-28 14:00:57 +00:00
Matt Jankowski
e85f24174e
Simplify checklist step loop in welcome mailer (#29761) 2024-03-28 10:56:33 +00:00
renovate[bot]
d44e7a8578
Update dependency node to 20.12 (#29765)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-28 10:54:16 +00:00
Renaud Chaput
961bb84e4c
Fix <RelativeTimestamp> types (#29781) 2024-03-28 10:07:01 +00:00
Renaud Chaput
d088964761
Fix props for <Button> (#29780) 2024-03-28 10:06:25 +00:00
Renaud Chaput
f2fd1da23f
Fix PropTypes for some record objects (#29786) 2024-03-28 10:05:16 +00:00
github-actions[bot]
1025fff6b9
New Crowdin Translations (automated) (#29772)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-03-27 21:19:03 +00:00
Claire
c913e2f3e5
Fix language picker and privacy picker not having a backdrop filter (#29779) 2024-03-27 16:32:00 +00:00
Renaud Chaput
b9982ce578
Fix notifications marker fetch (#29777) 2024-03-27 15:49:02 +00:00
Renaud Chaput
9fbe8d3a0c
Rewrite PIP state in Typescript (#27645)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-03-27 15:19:33 +00:00
Matt Jankowski
b016f03637
Pull out constant from AccountWarning.recent scope (#29767) 2024-03-27 14:08:04 +00:00
Renaud Chaput
27d014a7fa
Rewrite markers reducer in Typescript (#27644) 2024-03-27 12:47:09 +00:00
renovate[bot]
d49343ed11
Update dependency react-intl to v6.6.4 (#29771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-27 09:59:54 +00:00
renovate[bot]
86f999c1f2
Update dependency json-schema to v4.3.0 (#29769)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-27 09:59:50 +00:00
Matt Jankowski
1d0a43f6a3
Use composable query in Status.not_domain_blocked_by_account scope (#29766) 2024-03-27 09:59:45 +00:00
renovate[bot]
c5692d2f2f
Update dependency prom-client to v15.1.1 (#29764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-27 09:59:42 +00:00
Claire
b2d841ce9a
Fix column borders disappearing in advanced interface on low width viewports (#29763) 2024-03-26 23:14:11 +00:00
Matt Jankowski
c4feba4347
Use existing MascotHelper#instance_presenter instead of local var in welcome email template (#29759) 2024-03-26 15:58:48 +00:00
Claire
de740dfb9c
Use upsert_all and insert_all to reduce back-and-forth in costly migrations (#29752) 2024-03-26 15:57:08 +00:00
Claire
9c24f2d6b1
Undo notification permissions on individual and domain blocks (#29570) 2024-03-26 14:46:38 +00:00
Claire
7508472d84
Fix admin interface repeating rule title instead of showing hint text (#29758) 2024-03-26 14:46:05 +00:00
Claire
cfea9cc172
Add list of pending releases directly in mail notifications for version updates (#29436) 2024-03-26 14:45:19 +00:00
Matt Jankowski
32938dadd7
Add not_allowed scope for PreviewCardTrend (#29599) 2024-03-26 13:21:20 +00:00
Matt Jankowski
cf76380c91
Add AccountStat.by_recent_status, use in Account (#29704) 2024-03-26 13:12:09 +00:00
renovate[bot]
b34c089591
Update dependency csv to v3.3.0 (#29715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 13:09:35 +00:00
github-actions[bot]
b3d970bdb8
New Crowdin Translations (automated) (#29756)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-03-26 09:49:19 +00:00
Matt Jankowski
c3e3c60069
Remove extraneous Lint/UselessAccessModifier config (#29749) 2024-03-26 09:33:07 +00:00
Matt Jankowski
5e6a600b64
Bundler version update and misc gem version bumps (#29717) 2024-03-26 09:31:24 +00:00
Emelia Smith
eb926b7e60
Ensure case-insensitive fields are converted to lowercase in user imports (#29740) 2024-03-26 09:30:10 +00:00
Emelia Smith
a3e8b78250
Ensure case-insensitive fields are converted to lowercase in Admin Imports (#29739) 2024-03-26 09:30:07 +00:00
renovate[bot]
06fc2b3fde
Update dependency faker to v3.3.0 (#29755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 09:29:38 +00:00
renovate[bot]
572a8ef7f9
Update dependency rubocop-rails to v2.24.1 (#29745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 09:27:41 +00:00
renovate[bot]
3002a1e89b
Update dependency express to v4.19.2 (#29750)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 09:27:23 +00:00
renovate[bot]
36bc57de95
Update dependency cssnano to v6.1.2 (#29754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 09:27:05 +00:00
Renaud Chaput
02ea161506
Support "system" theme setting (light/dark theme depending on user system preference) (#29748)
Co-authored-by: Nishiki Liu <hello@nshki.com>
2024-03-26 09:25:49 +00:00
Eugen Rochko
0cea7a623b
Fix background and icon on notification requests in web UI (#29706) 2024-03-25 13:39:06 +00:00
Eugen Rochko
29f9dc742e
Change design of notification about lost connections in web UI (#29731) 2024-03-25 13:27:38 +00:00
Eugen Rochko
dd061291b1
Change out-of-band hashtags design in web UI (#29732) 2024-03-25 12:45:00 +00:00
renovate[bot]
766c1fea20
Update devDependencies (non-major) (#29746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-25 12:14:46 +00:00
renovate[bot]
55e2c827bd
Update DefinitelyTyped types (non-major) (#29743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-25 11:09:05 +00:00
renovate[bot]
45f8364cd1
Update dependency typescript to v5.4.3 (#29744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-25 11:09:00 +00:00
renovate[bot]
bbf36836b6
Update formatjs monorepo (#29733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-25 11:07:47 +00:00
github-actions[bot]
799e3be9bd
New Crowdin Translations (automated) (#29726)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-03-25 11:07:38 +00:00
484 changed files with 6645 additions and 3176 deletions

4
.env.development Normal file
View file

@ -0,0 +1,4 @@
# Required by ActiveRecord encryption feature
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr

View file

@ -3,3 +3,8 @@ NODE_ENV=production
# Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true
# Required by ActiveRecord encryption feature
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr

View file

@ -363,6 +363,7 @@ module.exports = defineConfig({
"message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
}
],
"@typescript-eslint/restrict-template-expressions": ['warn', { allowNumber: true }],
'jsdoc/require-jsdoc': 'off',
// Those rules set stricter rules for TS files

View file

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

View file

@ -38,5 +38,5 @@ jobs:
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Jest testing
- name: JavaScript testing
run: yarn jest --reporters github-actions summary

View file

@ -28,6 +28,9 @@ jobs:
env:
RAILS_ENV: ${{ matrix.mode }}
BUNDLE_WITH: ${{ matrix.mode }}
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: precompile_placeholder
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: precompile_placeholder
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: precompile_placeholder
OTP_SECRET: precompile_placeholder
SECRET_KEY_BASE: precompile_placeholder
@ -111,7 +114,6 @@ jobs:
fail-fast: false
matrix:
ruby-version:
- '3.0'
- '3.1'
- '.ruby-version'
- '3.3'
@ -187,7 +189,6 @@ jobs:
fail-fast: false
matrix:
ruby-version:
- '3.0'
- '3.1'
- '.ruby-version'
- '3.3'
@ -287,7 +288,6 @@ jobs:
fail-fast: false
matrix:
ruby-version:
- '3.0'
- '3.1'
- '.ruby-version'
- '3.3'

1
.gitignore vendored
View file

@ -24,7 +24,6 @@
/public/packs-test
.env
.env.production
.env.development
/node_modules/
/build/

View file

@ -1,6 +1,5 @@
exclude:
- 'vendor/**/*'
- lib/templates/haml/scaffold/_form.html.haml
require:
- ./lib/linter/haml_middle_dot.rb
@ -11,6 +10,6 @@ linters:
MiddleDot:
enabled: true
LineLength:
max: 320
max: 300
ViewLength:
max: 200 # Override default value of 100 inherited from rubocop

2
.nvmrc
View file

@ -1 +1 @@
20.11
20.12

View file

@ -9,12 +9,13 @@ inherit_mode:
require:
- rubocop-rails
- rubocop-rspec
- rubocop-rspec_rails
- rubocop-performance
- rubocop-capybara
- ./lib/linter/rubocop_middle_dot
AllCops:
TargetRubyVersion: 3.0 # Set to minimum supported version of CI
TargetRubyVersion: 3.1 # Set to minimum supported version of CI
DisplayCopNames: true
DisplayStyleGuide: true
ExtraDetails: true
@ -39,13 +40,7 @@ Layout/FirstHashElementIndentation:
# Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
Layout/LineLength:
Max: 320 # Default of 120 causes a duplicate entry in generated todo file
# Reason:
# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier
Lint/UselessAccessModifier:
ContextCreatingMethods:
- class_methods
Max: 300 # Default of 120 causes a duplicate entry in generated todo file
## Disable most Metrics/*Length cops
# Reason: those are often triggered and force significant refactors when this happend
@ -86,6 +81,11 @@ Metrics/CyclomaticComplexity:
Metrics/ParameterLists:
CountKeywordArgs: false
# Reason: Prefer seeing a variable name
# https://docs.rubocop.org/rubocop/cops_naming.html#namingblockforwarding
Naming/BlockForwarding:
EnforcedStyle: explicit
# Reason: Prevailing style is argument file paths
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
Rails/FilePath:
@ -148,11 +148,6 @@ RSpec/NamedSubject:
RSpec/NotToNot:
EnforcedStyle: to_not
# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus
# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus
RSpec/Rails/HttpStatus:
EnforcedStyle: numeric
# Reason: Match overrides from Rspec/FilePath rule above
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat
RSpec/SpecFilePathFormat:
@ -163,6 +158,11 @@ RSpec/SpecFilePathFormat:
OEmbedController: oembed_controller
OStatus: ostatus
# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus
# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus
RSpecRails/HttpStatus:
EnforcedStyle: numeric
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren
Style/ClassAndModuleChildren:
@ -182,10 +182,16 @@ Style/FormatStringToken:
AllowedMethods:
- redirect_with_vary
# Reason: Prevailing style choice
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashaslastarrayitem
Style/HashAsLastArrayItem:
Enabled: false
# Reason: Enforce modern Ruby style
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax
Style/HashSyntax:
EnforcedStyle: ruby19_no_mixed_keys
EnforcedShorthandSyntax: either
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals

View file

@ -1,18 +1,11 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.60.2.
# using RuboCop version 1.62.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
# Include: **/*.gemfile, **/Gemfile, **/gems.rb
Bundler/OrderedGems:
Exclude:
- 'Gemfile'
Lint/NonLocalExitFromIterator:
Exclude:
- 'app/helpers/jsonld_helper.rb'
@ -36,7 +29,7 @@ Metrics/PerceivedComplexity:
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 20 # Override default of 5
Max: 18
RSpec/MultipleExpectations:
Max: 7
@ -61,15 +54,6 @@ Rails/OutputSafety:
Exclude:
- 'config/initializers/simple_form.rb'
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/UniqueValidationWithoutIndex:
Exclude:
- 'app/models/account_alias.rb'
- 'app/models/custom_filter_status.rb'
- 'app/models/identity.rb'
- 'app/models/webauthn_credential.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedMethods, AllowedPatterns.
# AllowedMethods: ==, equal?, eql?
@ -88,7 +72,6 @@ Style/FetchEnvVar:
Exclude:
- 'app/lib/redis_configuration.rb'
- 'app/lib/translation_service.rb'
- 'config/environments/development.rb'
- 'config/environments/production.rb'
- 'config/initializers/2_limited_federation_mode.rb'
- 'config/initializers/3_omniauth.rb'
@ -98,7 +81,6 @@ Style/FetchEnvVar:
- 'config/initializers/paperclip.rb'
- 'config/initializers/vapid.rb'
- 'lib/mastodon/redis_config.rb'
- 'lib/premailer_webpack_strategy.rb'
- 'lib/tasks/repo.rake'
- 'spec/features/profile_spec.rb'
@ -144,22 +126,8 @@ Style/GuardClause:
- 'lib/mastodon/cli/accounts.rb'
- 'lib/mastodon/cli/maintenance.rb'
- 'lib/mastodon/cli/media.rb'
- 'lib/paperclip/attachment_extensions.rb'
- 'lib/tasks/repo.rake'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: braces, no_braces
Style/HashAsLastArrayItem:
Exclude:
- 'app/controllers/admin/statuses_controller.rb'
- 'app/controllers/api/v1/statuses_controller.rb'
- 'app/models/concerns/account/counters.rb'
- 'app/models/concerns/status/threading_concern.rb'
- 'app/models/status.rb'
- 'app/services/batched_remove_status_service.rb'
- 'app/services/notify_service.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/HashTransformValues:
Exclude:
@ -207,13 +175,6 @@ Style/OptionalBooleanParameter:
- 'app/workers/unfollow_follow_worker.rb'
- 'lib/mastodon/redis_config.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'config/deploy.rb'
- 'config/initializers/doorkeeper.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: short, verbose
@ -252,44 +213,12 @@ Style/SignalException:
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/SingleArgumentDig:
Exclude:
- 'lib/webpacker/manifest_extensions.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Mode.
Style/StringConcatenation:
Exclude:
- 'config/initializers/paperclip.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiterals:
Exclude:
- 'config/environments/production.rb'
- 'config/initializers/backtrace_silencers.rb'
- 'config/initializers/http_client_proxy.rb'
- 'config/initializers/rack_attack.rb'
- 'config/initializers/webauthn.rb'
- 'config/routes.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArguments:
Exclude:
- 'config/initializers/paperclip.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInHashLiteral:
Exclude:
- 'config/environments/production.rb'
- 'config/environments/test.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: WordRegex.
# SupportedStyles: percent, brackets

View file

@ -1 +1 @@
3.2.3
3.2.4

View file

@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.4
# syntax=docker/dockerfile:1.7
# Please see https://docs.docker.com/engine/reference/builder for information about
# the extended buildx capabilities used in this file.
@ -7,15 +7,15 @@
ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM}
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.3"]
ARG RUBY_VERSION="3.2.3"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.4"]
ARG RUBY_VERSION="3.2.4"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
# Ruby image to use for base image based on combined variables (ex: 3.2.3-slim-bookworm)
# Ruby image to use for base image based on combined variables (ex: 3.2.4-slim-bookworm)
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA="prod"
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
ARG RAILS_SERVE_STATIC_FILES="true"
# Allow to use YJIT compiler
# See: https://github.com/ruby/ruby/blob/v3_2_3/doc/yjit/yjit.md
# See: https://github.com/ruby/ruby/blob/v3_2_4/doc/yjit/yjit.md
ARG RUBY_YJIT_ENABLE="1"
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC"
@ -205,7 +205,12 @@ ARG TARGETPLATFORM
RUN \
# Use Ruby on Rails to create Mastodon assets
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=precompile_placeholder \
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=precompile_placeholder \
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=precompile_placeholder \
OTP_SECRET=precompile_placeholder \
SECRET_KEY_BASE=precompile_placeholder \
bundle exec rails assets:precompile; \
# Cleanup temporary files
rm -fr /opt/mastodon/tmp;
@ -257,4 +262,4 @@ USER mastodon
# Expose default Puma ports
EXPOSE 3000
# Set container tini as default entry point
ENTRYPOINT ["/usr/bin/tini", "--"]
ENTRYPOINT ["/usr/bin/tini", "--"]

39
Gemfile
View file

@ -1,28 +1,28 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '>= 3.0.0'
ruby '>= 3.1.0'
gem 'puma', '~> 6.3'
gem 'rails', '~> 7.1.1'
gem 'propshaft'
gem 'thor', '~> 1.2'
gem 'puma', '~> 6.3'
gem 'rack', '~> 2.2.7'
gem 'rails', '~> 7.1.1'
gem 'thor', '~> 1.2'
# For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182
gem 'irb', '~> 1.8'
gem 'dotenv'
gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.5'
gem 'pghero'
gem 'dotenv-rails', '~> 2.8'
gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'blurhash', '~> 0.1'
gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 1.0', require: false
gem 'kt-paperclip', '~> 7.2'
gem 'md-paperclip-azure', '~> 2.2', require: false
gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
@ -39,11 +39,11 @@ end
gem 'net-ldap', '~> 0.18'
gem 'omniauth-cas', '~> 3.0.0.beta.1'
gem 'omniauth-saml', '~> 2.0'
gem 'omniauth_openid_connect', '~> 0.6.1'
gem 'omniauth', '~> 2.0'
gem 'omniauth-cas', '~> 3.0.0.beta.1'
gem 'omniauth_openid_connect', '~> 0.6.1'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-saml', '~> 2.0'
gem 'color_diff', '~> 0.1'
gem 'csv', '~> 3.2'
@ -53,9 +53,8 @@ gem 'ed25519', '~> 1.3'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.10'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 5.1'
gem 'http', '~> 5.2.0'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.6.2'
gem 'i18n', '1.14.1' # TODO: Remove version when resolved: https://github.com/glebm/i18n-tasks/issues/552 / https://github.com/ruby-i18n/i18n/pull/688
@ -63,40 +62,40 @@ gem 'idn-ruby', require: 'idn'
gem 'inline_svg'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15'
gem 'nsa'
gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14'
gem 'parslet'
gem 'posix-spawn'
gem 'premailer-rails'
gem 'public_suffix', '~> 5.0'
gem 'pundit', '~> 2.3'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
gem 'rails-i18n', '~> 7.0'
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'redis-namespace', '~> 1.10'
gem 'rqrcode', '~> 2.2'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.7'
gem 'sidekiq', '~> 6.5'
gem 'sidekiq-bulk', '~> 0.2.0'
gem 'sidekiq-scheduler', '~> 5.0'
gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'sidekiq-bulk', '~> 0.2.0'
gem 'simple-navigation', '~> 4.4'
gem 'simple_form', '~> 5.2'
gem 'stoplight', '~> 3.0.1'
gem 'simple-navigation', '~> 4.4'
gem 'stoplight', '~> 4.1'
gem 'strong_migrations', '1.8.0'
gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023'
gem 'webauthn', '~> 3.0'
gem 'webpacker', '~> 5.4'
gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9'
gem 'webauthn', '~> 3.0'
gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2'
@ -198,12 +197,14 @@ group :production do
gem 'lograge', '~> 0.12'
end
gem 'cocoon', '~> 1.2'
gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
gem 'cocoon', '~> 1.2'
gem 'net-http', '~> 0.4.0'
gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1'
gem 'mail', '~> 2.8'

View file

@ -99,20 +99,20 @@ GEM
ast (2.4.2)
attr_encrypted (4.0.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
attr_required (1.0.2)
awrence (1.2.1)
aws-eventstream (1.3.0)
aws-partitions (1.873.0)
aws-sdk-core (3.190.1)
aws-partitions (1.916.0)
aws-sdk-core (3.192.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.75.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.142.0)
aws-sdk-core (~> 3, >= 3.189.0)
aws-sdk-s3 (1.147.0)
aws-sdk-core (~> 3, >= 3.192.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
@ -132,7 +132,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
better_html (2.0.2)
better_html (2.1.1)
actionview (>= 6.0)
activesupport (>= 6.0)
ast (~> 2.0)
@ -140,9 +140,9 @@ GEM
parser (>= 2.4)
smart_properties
bigdecimal (3.1.7)
bindata (2.4.15)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
bindata (2.5.0)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.7)
bootsnap (1.18.3)
msgpack (~> 1.2)
@ -167,7 +167,7 @@ GEM
xpath (~> 3.2)
case_transform (0.2)
activesupport
cbor (0.5.9.6)
cbor (0.5.9.8)
charlock_holmes (0.7.7)
chewy (7.5.1)
activesupport (>= 5.2)
@ -182,23 +182,23 @@ GEM
cose (1.3.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
crack (0.4.6)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
css_parser (1.14.0)
css_parser (1.17.1)
addressable
csv (3.2.8)
csv (3.3.0)
database_cleaner-active_record (2.1.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
debug (1.9.1)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
debug_inspector (1.1.0)
devise (4.9.3)
debug_inspector (1.2.0)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@ -217,14 +217,10 @@ GEM
discard (1.3.0)
activerecord (>= 4.2, < 8)
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
domain_name (0.6.20240107)
doorkeeper (5.6.9)
railties (>= 5)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
dotenv (3.1.0)
drb (2.2.1)
ed25519 (1.3.0)
elasticsearch (7.13.3)
@ -242,11 +238,11 @@ GEM
mail (~> 2.7)
encryptor (3.0.0)
erubi (1.12.0)
et-orbi (1.2.7)
et-orbi (1.2.11)
tzinfo
excon (0.109.0)
excon (0.110.0)
fabrication (2.31.0)
faker (3.2.3)
faker (3.3.1)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
@ -274,10 +270,10 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fast_blank (1.0.1)
fastimage (2.3.0)
ffi (1.15.5)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
fastimage (2.3.1)
ffi (1.16.3)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
fog-core (2.4.0)
builder
@ -291,7 +287,7 @@ GEM
fog-core (~> 2.1)
fog-json (>= 1.0)
formatador (1.1.0)
fugit (1.8.1)
fugit (1.10.1)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
fuubar (2.5.1)
@ -318,15 +314,16 @@ GEM
hashie (5.0.0)
hcaptcha (7.1.0)
json
highline (2.1.0)
highline (3.0.1)
hiredis (0.6.3)
hkdf (0.3.0)
htmlentities (4.3.4)
http (5.1.1)
http (5.2.0)
addressable (~> 2.8)
base64 (~> 0.1)
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.4.0)
llhttp-ffi (~> 0.5.0)
http-cookie (1.0.5)
domain_name (~> 0.5)
http-form_data (2.3.0)
@ -357,7 +354,7 @@ GEM
rdoc
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.7.1)
json (2.7.2)
json-canonicalization (1.0.0)
json-jwt (1.15.3.1)
activesupport (>= 4.2)
@ -374,7 +371,7 @@ GEM
json-ld-preloaded (3.3.0)
json-ld (~> 3.3)
rdf (~> 3.3)
json-schema (4.2.0)
json-schema (4.3.0)
addressable (>= 2.8)
jsonapi-renderer (0.2.2)
jwt (2.7.1)
@ -399,15 +396,15 @@ GEM
language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (2.0.0)
actionmailer (>= 5.2)
letter_opener (~> 1.7)
railties (>= 5.2)
rexml
link_header (0.0.8)
llhttp-ffi (0.4.0)
llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.14.0)
@ -423,7 +420,7 @@ GEM
net-imap
net-pop
net-smtp
marcel (1.0.2)
marcel (1.0.4)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
matrix (0.4.2)
@ -434,13 +431,13 @@ GEM
memory_profiler (1.0.1)
mime-types (3.5.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.1205)
mime-types-data (3.2024.0305)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
mini_portile2 (2.8.6)
minitest (5.22.3)
msgpack (1.7.2)
multi_json (1.15.0)
multipart-post (2.3.0)
multipart-post (2.4.0)
mutex_m (0.2.0)
net-http (0.4.1)
uri
@ -454,10 +451,10 @@ GEM
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.4.0.1)
net-smtp (0.5.0)
net-protocol
nio4r (2.5.9)
nokogiri (1.16.3)
nio4r (2.7.1)
nokogiri (1.16.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nsa (0.3.0)
@ -510,8 +507,7 @@ GEM
pg (1.5.6)
pghero (3.4.1)
activerecord (>= 6)
posix-spawn (0.3.15)
premailer (1.21.0)
premailer (1.23.0)
addressable
css_parser (>= 1.12.0)
htmlentities (>= 4.0.0)
@ -527,7 +523,7 @@ GEM
railties (>= 7.0.0)
psych (5.1.2)
stringio
public_suffix (5.0.4)
public_suffix (5.0.5)
puma (6.4.2)
nio4r (~> 2.0)
pundit (2.3.1)
@ -548,7 +544,7 @@ GEM
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack-proxy (0.7.7)
rack
rack-session (1.0.2)
rack (< 3)
@ -594,7 +590,7 @@ GEM
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.1.0)
rake (13.2.1)
rdf (3.3.1)
bcp47_spec (~> 0.2)
link_header (~> 0.0, >= 0.0.8)
@ -609,16 +605,16 @@ GEM
redlock (1.3.2)
redis (>= 3.0.0, < 6.0)
regexp_parser (2.9.0)
reline (0.4.3)
reline (0.5.2)
io-console (~> 0.5)
request_store (1.5.1)
request_store (1.6.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.2.6)
rotp (6.3.0)
rouge (4.1.2)
rouge (4.2.1)
rpam2 (4.0.2)
rqrcode (2.2.0)
chunky_png (~> 1.0)
@ -642,13 +638,13 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-sidekiq (4.1.0)
rspec-sidekiq (4.2.0)
rspec-core (~> 3.0)
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.13.1)
rubocop (1.62.1)
rubocop (1.63.3)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@ -665,21 +661,24 @@ GEM
rubocop (~> 1.41)
rubocop-factory_bot (2.25.1)
rubocop (~> 1.41)
rubocop-performance (1.20.2)
rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rails (2.24.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.24.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.27.1)
rubocop-rspec (2.29.1)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
rubocop-rspec_rails (~> 2.28)
rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40)
ruby-prof (1.7.0)
ruby-progressbar (1.13.0)
ruby-saml (1.15.0)
ruby-saml (1.16.0)
nokogiri (>= 1.13.10)
rexml
ruby2_keywords (0.0.5)
@ -691,10 +690,10 @@ GEM
sanitize (6.1.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
scenic (1.7.0)
scenic (1.8.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
selenium-webdriver (4.18.1)
selenium-webdriver (4.19.0)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
@ -731,7 +730,7 @@ GEM
smart_properties (1.17.0)
stackprof (0.2.26)
statsd-ruby (1.5.0)
stoplight (3.0.2)
stoplight (4.1.0)
redlock (~> 1.0)
stringio (3.1.0)
strong_migrations (1.8.0)
@ -746,7 +745,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1)
climate_control
test-prof (1.3.2)
test-prof (1.3.3)
thor (1.3.1)
tilt (2.3.0)
timeout (0.4.1)
@ -763,7 +762,7 @@ GEM
tty-cursor (~> 0.7)
tty-screen (~> 0.8)
wisper (~> 2.0)
tty-screen (0.8.1)
tty-screen (0.8.2)
twitter-text (3.1.0)
idn-ruby
unf (~> 0.1.0)
@ -773,9 +772,9 @@ GEM
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unf_ext (0.0.9.1)
unicode-display_width (2.5.0)
uri (0.12.2)
uri (0.13.0)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
@ -796,7 +795,7 @@ GEM
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
webmock (3.22.0)
webmock (3.23.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -847,7 +846,7 @@ DEPENDENCIES
devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.2)
doorkeeper (~> 5.6)
dotenv-rails (~> 2.8)
dotenv
ed25519 (~> 1.3)
email_spec
fabrication (~> 2.30)
@ -862,7 +861,7 @@ DEPENDENCIES
hcaptcha (~> 7.1)
hiredis (~> 0.6)
htmlentities (~> 4.3)
http (~> 5.1)
http (~> 5.2.0)
http_accept_language (~> 2.1)
httplog (~> 1.6.2)
i18n (= 1.14.1)
@ -879,6 +878,7 @@ DEPENDENCIES
letter_opener_web (~> 2.0)
link_header (~> 0.0)
lograge (~> 0.12)
mail (~> 2.8)
mario-redis-lock (~> 1.2)
md-paperclip-azure (~> 2.2)
memory_profiler
@ -897,7 +897,6 @@ DEPENDENCIES
parslet
pg (~> 1.5)
pghero
posix-spawn
premailer-rails
private_address_check (~> 0.5)
propshaft
@ -939,7 +938,7 @@ DEPENDENCIES
simplecov (~> 0.22)
simplecov-lcov (~> 0.8)
stackprof
stoplight (~> 3.0.1)
stoplight (~> 4.1)
strong_migrations (= 1.8.0)
test-prof
thor (~> 1.2)
@ -953,7 +952,7 @@ DEPENDENCIES
xorcist (~> 1.1)
RUBY VERSION
ruby 3.2.2p53
ruby 3.2.3p157
BUNDLED WITH
2.5.4
2.5.9

View file

@ -69,7 +69,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
- **PostgreSQL** 12+
- **Redis** 4+
- **Ruby** 3.0+
- **Ruby** 3.1+
- **Node.js** 16+
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.

1
Vagrantfile vendored
View file

@ -173,6 +173,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080
config.vm.network :forwarded_port, guest: 3000, host: 3000
config.vm.network :forwarded_port, guest: 3035, host: 3035
config.vm.network :forwarded_port, guest: 4000, host: 4000
config.vm.network :forwarded_port, guest: 8080, host: 8080
config.vm.network :forwarded_port, guest: 9200, host: 9200

View file

@ -46,7 +46,7 @@ class AccountsController < ApplicationController
end
def default_statuses
@account.statuses.where(visibility: [:public, :unlisted])
@account.statuses.distributable_visibility
end
def only_media_scope

View file

@ -31,7 +31,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
def set_replies
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.distributable_visibility.where(in_reply_to_id: @status.id)
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end

View file

@ -9,6 +9,7 @@ class Api::BaseController < ApplicationController
include Api::CachingConcern
include Api::ContentSecurityPolicy
include Api::ErrorHandling
include Api::Pagination
skip_before_action :require_functional!, unless: :limited_federation_mode?
@ -29,21 +30,6 @@ class Api::BaseController < ApplicationController
protected
def pagination_max_id
pagination_collection.last.id
end
def pagination_since_id
pagination_collection.first.id
end
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path
links << [prev_path, [%w(rel prev)]] if prev_path
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
end
def limit_param(default_limit)
return default_limit unless params[:limit]
@ -72,10 +58,6 @@ class Api::BaseController < ApplicationController
render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.unavailable?
end
def require_valid_pagination_options!
render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid?
end
def require_user!
if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422
@ -104,14 +86,6 @@ class Api::BaseController < ApplicationController
private
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_options_invalid?
params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?)
end
def respond_with_error(code)
render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Api::V1::Accounts::CredentialsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:update]
before_action -> { doorkeeper_authorize! :read, :'read:accounts', :'read:me' }, except: [:update]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update]
before_action :require_user!

View file

@ -12,10 +12,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
private
def set_recently_used_tags
@recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10)
end
def featured_tag_ids
current_account.featured_tags.pluck(:tag_id)
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
end
end

View file

@ -23,7 +23,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base
end
def paginated_statuses
Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted]).paginate_by_max_id(
Status.where(reblog_of_id: @status.id).distributable_visibility.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Api::Pagination
extend ActiveSupport::Concern
protected
def pagination_max_id
pagination_collection.last.id
end
def pagination_since_id
pagination_collection.first.id
end
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path
links << [prev_path, [%w(rel prev)]] if prev_path
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
end
def require_valid_pagination_options!
render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid?
end
private
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_options_invalid?
params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?)
end
end

View file

@ -46,27 +46,19 @@ module CacheConcern
end
end
# TODO: Rename this method, as it does not perform any caching anymore.
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)
return raw unless klass.respond_to?(:preload_cacheable_associations)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
return [] if raw.empty?
records = raw.to_a
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
klass.preload_cacheable_associations(records)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
Rails.cache.write_multi(uncached.values.to_h { |i| [i, i] })
end
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
records
end
# TODO: Rename this method, as it does not perform any caching anymore.
def cache_collection_paginated_by_id(raw, klass, limit, options)
cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass
cache_collection raw.to_a_paginated_by_id(limit, options), klass
end
end

View file

@ -66,7 +66,7 @@ module SignatureVerification
compare_signed_string = build_signed_string(include_query_string: false)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
@ -226,10 +226,10 @@ module SignatureVerification
end
if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
account ||= stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e
@ -238,12 +238,11 @@ module SignatureVerification
raise SignatureVerificationError, e.message
end
def stoplight_wrap_request(&block)
Stoplight("source:#{request.remote_ip}", &block)
def stoplight_wrapper
Stoplight("source:#{request.remote_ip}")
.with_threshold(1)
.with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
.run
end
def actor_refresh_key!(actor)

View file

@ -38,7 +38,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
end
def set_recently_used_tags
@recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
end
def featured_tag_params

View file

@ -31,7 +31,7 @@ class Settings::ImportsController < Settings::BaseController
def show; end
def failures
@bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id])
@bulk_import = current_account.bulk_imports.state_finished.find(params[:id])
respond_to do |format|
format.csv do
@ -92,7 +92,7 @@ class Settings::ImportsController < Settings::BaseController
end
def set_bulk_import
@bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id])
@bulk_import = current_account.bulk_imports.state_unconfirmed.find(params[:id])
end
def set_recent_imports

View file

@ -113,6 +113,14 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
def material_symbol(icon, attributes = {})
inline_svg_tag(
"400-24px/#{icon}.svg",
class: %w(icon).concat(attributes[:class].to_s.split),
role: :img
)
end
def check_icon
inline_svg_tag 'check.svg'
end

View file

@ -19,6 +19,6 @@ module BrandingHelper
end
def render_logo
image_pack_tag('logo.svg', alt: 'Mastodon', class: 'logo logo--icon')
image_tag(frontend_asset_path('images/logo.svg'), alt: 'Mastodon', class: 'logo logo--icon')
end
end

View file

@ -48,13 +48,11 @@ module ContextHelper
end
def serialized_context(named_contexts_map, context_extensions_map)
context_array = []
named_contexts = named_contexts_map.keys
context_extensions = context_extensions_map.keys
named_contexts.each do |key|
context_array << NAMED_CONTEXT_MAP[key]
context_array = named_contexts.map do |key|
NAMED_CONTEXT_MAP[key]
end
extensions = context_extensions.each_with_object({}) do |key, h|

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
module ThemeHelper
def theme_style_tags(theme)
if theme == 'system'
stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') +
stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
else
stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous'
end
end
def theme_color_tags(theme)
if theme == 'system'
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') +
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
else
tag.meta name: 'theme-color', content: theme_color_for(theme)
end
end
private
def theme_color_for(theme)
theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark]
end
end

View file

@ -363,6 +363,6 @@ ready(() => {
document.querySelectorAll('[data-admin-component]').forEach((element) => {
void mountReactComponent(element);
});
}).catch((reason) => {
}).catch((reason: unknown) => {
throw reason;
});

View file

@ -69,7 +69,7 @@ window.addEventListener('message', (e) => {
},
'*',
);
}).catch((e) => {
}).catch((e: unknown) => {
console.error('Error in setHeightMessage postMessage', e);
});
});
@ -206,7 +206,7 @@ function loaded() {
return true;
})
.catch((error) => {
.catch((error: unknown) => {
console.error(error);
});
}
@ -448,7 +448,7 @@ Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
});
function main() {
ready(loaded).catch((error) => {
ready(loaded).catch((error: unknown) => {
console.error(error);
});
}
@ -457,6 +457,6 @@ loadPolyfills()
.then(loadLocale)
.then(main)
.then(loadKeyboardExtensions)
.catch((error) => {
.catch((error: unknown) => {
console.error(error);
});

View file

@ -1,32 +0,0 @@
import { openModal } from './modal';
export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
export function initBoostModal(props) {
return (dispatch, getState) => {
const default_privacy = getState().getIn(['compose', 'default_privacy']);
const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
dispatch({
type: BOOSTS_INIT_MODAL,
privacy,
});
dispatch(openModal({
modalType: 'BOOST',
modalProps: props,
}));
};
}
export function changeBoostPrivacy(privacy) {
return dispatch => {
dispatch({
type: BOOSTS_CHANGE_PRIVACY,
privacy,
});
};
}

View file

@ -1,152 +0,0 @@
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import api from '../api';
import { compareId } from '../compare_id';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL';
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if (window.fetch && 'keepalive' in new Request('')) {
fetch('/api/v1/markers', {
keepalive: true,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
} else if (navigator && navigator.sendBeacon) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) {
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon('/api/v1/markers', formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}
};
const _buildParams = (state) => {
const params = {};
const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
api(getState).post('/api/v1/markers', params).then(() => {
dispatch(submitMarkersSuccess(params));
}).catch(() => {});
}, 300000, { leading: true, trailing: true });
export function submitMarkersSuccess({ home, notifications }) {
return {
type: MARKERS_SUBMIT_SUCCESS,
home: (home || {}).last_read_id,
notifications: (notifications || {}).last_read_id,
};
}
export function submitMarkers(params = {}) {
const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
if (params.immediate === true) {
debouncedSubmitMarkers.flush();
}
return result;
}
export const fetchMarkers = () => (dispatch, getState) => {
const params = { timeline: ['notifications'] };
dispatch(fetchMarkersRequest());
api(getState).get('/api/v1/markers', { params }).then(response => {
dispatch(fetchMarkersSuccess(response.data));
}).catch(error => {
dispatch(fetchMarkersFail(error));
});
};
export function fetchMarkersRequest() {
return {
type: MARKERS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchMarkersSuccess(markers) {
return {
type: MARKERS_FETCH_SUCCESS,
markers,
skipLoading: true,
};
}
export function fetchMarkersFail(error) {
return {
type: MARKERS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
};
}

View file

@ -0,0 +1,165 @@
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import type { MarkerJSON } from 'mastodon/api_types/markers';
import type { RootState } from 'mastodon/store';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
import api, { authorizationTokenFromState } from '../api';
import { compareId } from '../compare_id';
export const synchronouslySubmitMarkers = createAppAsyncThunk(
'markers/submit',
async (_args, { getState }) => {
const accessToken = authorizationTokenFromState(getState);
const params = buildPostMarkersParams(getState());
if (Object.keys(params).length === 0 || !accessToken) {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if ('fetch' in window && 'keepalive' in new Request('')) {
await fetch('/api/v1/markers', {
keepalive: true,
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if ('navigator' && 'sendBeacon' in navigator) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) {
if (value.last_read_id)
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon('/api/v1/markers', formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}
},
);
interface MarkerParam {
last_read_id?: string;
}
function getLastHomeId(state: RootState): string | undefined {
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
return (
state
// @ts-expect-error state.timelines is not yet typed
.getIn(['timelines', 'home', 'items'], ImmutableList())
// @ts-expect-error state.timelines is not yet typed
.find((item) => item !== null)
);
}
function getLastNotificationId(state: RootState): string | undefined {
// @ts-expect-error state.notifications is not yet typed
return state.getIn(['notifications', 'lastReadId']);
}
const buildPostMarkersParams = (state: RootState) => {
const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
const lastHomeId = getLastHomeId(state);
const lastNotificationId = getLastNotificationId(state);
if (lastHomeId && compareId(lastHomeId, state.markers.home) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
if (
lastNotificationId &&
compareId(lastNotificationId, state.markers.notifications) > 0
) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
export const submitMarkersAction = createAppAsyncThunk<{
home: string | undefined;
notifications: string | undefined;
}>('markers/submitAction', async (_args, { getState }) => {
const accessToken = authorizationTokenFromState(getState);
const params = buildPostMarkersParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return { home: undefined, notifications: undefined };
}
await api(getState).post<MarkerJSON>('/api/v1/markers', params);
return {
home: params.home?.last_read_id,
notifications: params.notifications?.last_read_id,
};
});
const debouncedSubmitMarkers = debounce(
(dispatch) => {
dispatch(submitMarkersAction());
},
300000,
{
leading: true,
trailing: true,
},
);
export const submitMarkers = createAppAsyncThunk(
'markers/submit',
(params: { immediate?: boolean }, { dispatch }) => {
debouncedSubmitMarkers(dispatch);
if (params.immediate) {
debouncedSubmitMarkers.flush();
}
},
);
export const fetchMarkers = createAppAsyncThunk(
'markers/fetch',
async (_args, { getState }) => {
const response = await api(getState).get<Record<string, MarkerJSON>>(
`/api/v1/markers`,
{ params: { timeline: ['notifications'] } },
);
return { markers: response.data };
},
);

View file

@ -1,46 +0,0 @@
// @ts-check
export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY';
export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
/**
* @typedef MediaProps
* @property {string} src
* @property {boolean} muted
* @property {number} volume
* @property {number} currentTime
* @property {string} poster
* @property {string} backgroundColor
* @property {string} foregroundColor
* @property {string} accentColor
*/
/**
* @param {string} statusId
* @param {string} accountId
* @param {string} playerType
* @param {MediaProps} props
* @returns {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
// @ts-expect-error
return (dispatch, getState) => {
// Do not open a player for a toot that does not exist
if (getState().hasIn(['statuses', statusId])) {
dispatch({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
}
};
};
/*
* @return {object}
*/
export const removePictureInPicture = () => ({
type: PICTURE_IN_PICTURE_REMOVE,
});

View file

@ -0,0 +1,31 @@
import { createAction } from '@reduxjs/toolkit';
import type { PIPMediaProps } from 'mastodon/reducers/picture_in_picture';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
interface DeployParams {
statusId: string;
accountId: string;
playerType: 'audio' | 'video';
props: PIPMediaProps;
}
export const removePictureInPicture = createAction('pip/remove');
export const deployPictureInPictureAction =
createAction<DeployParams>('pip/deploy');
export const deployPictureInPicture = createAppAsyncThunk(
'pip/deploy',
(args: DeployParams, { dispatch, getState }) => {
const { statusId } = args;
// Do not open a player for a toot that does not exist
// @ts-expect-error state.statuses is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (getState().hasIn(['statuses', statusId])) {
dispatch(deployPictureInPictureAction(args));
}
},
);

View file

@ -29,9 +29,14 @@ const setCSRFHeader = () => {
void ready(setCSRFHeader);
export const authorizationTokenFromState = (getState?: GetState) => {
return (
getState && (getState().meta.get('access_token', '') as string | false)
);
};
const authorizationHeaderFromState = (getState?: GetState) => {
const accessToken =
getState && (getState().meta.get('access_token', '') as string);
const accessToken = authorizationTokenFromState(getState);
if (!accessToken) {
return {};

View file

@ -0,0 +1,7 @@
// See app/serializers/rest/account_serializer.rb
export interface MarkerJSON {
last_read_id: string;
version: string;
updated_at: string;
}

View file

@ -0,0 +1,22 @@
// See app/serializers/rest/media_attachment_serializer.rb
export type MediaAttachmentType =
| 'image'
| 'gifv'
| 'video'
| 'unknown'
| 'audio';
export interface ApiMediaAttachmentJSON {
id: string;
type: MediaAttachmentType;
url: string;
preview_url: string;
remoteUrl: string;
preview_remote_url: string;
text_url: string;
// TODO: how to define this?
meta: unknown;
description?: string;
blurhash: string;
}

View file

@ -0,0 +1,23 @@
import type { ApiCustomEmojiJSON } from './custom_emoji';
// See app/serializers/rest/poll_serializer.rb
export interface ApiPollOptionJSON {
title: string;
votes_count: number;
}
export interface ApiPollJSON {
id: string;
expires_at: string;
expired: boolean;
multiple: boolean;
votes_count: number;
voters_count: number;
options: ApiPollOptionJSON[];
emojis: ApiCustomEmojiJSON[];
voted: boolean;
own_votes: number[];
}

View file

@ -0,0 +1,91 @@
// See app/serializers/rest/status_serializer.rb
import type { ApiAccountJSON } from './accounts';
import type { ApiCustomEmojiJSON } from './custom_emoji';
import type { ApiMediaAttachmentJSON } from './media_attachments';
import type { ApiPollJSON } from './polls';
// See app/modals/status.rb
export type StatusVisibility =
| 'public'
| 'unlisted'
| 'private'
// | 'limited' // This is never exposed to the API (they become `private`)
| 'direct';
export interface ApiStatusApplicationJSON {
name: string;
website: string;
}
export interface ApiTagJSON {
name: string;
url: string;
}
export interface ApiMentionJSON {
id: string;
username: string;
url: string;
acct: string;
}
export interface ApiPreviewCardJSON {
url: string;
title: string;
description: string;
language: string;
type: string;
author_name: string;
author_url: string;
provider_name: string;
provider_url: string;
html: string;
width: number;
height: number;
image: string;
image_description: string;
embed_url: string;
blurhash: string;
published_at: string;
}
export interface ApiStatusJSON {
id: string;
created_at: string;
in_reply_to_id?: string;
in_reply_to_account_id?: string;
sensitive: boolean;
spoiler_text?: string;
visibility: StatusVisibility;
language: string;
uri: string;
url: string;
replies_count: number;
reblogs_count: number;
favorites_count: number;
edited_at?: string;
favorited?: boolean;
reblogged?: boolean;
muted?: boolean;
bookmarked?: boolean;
pinned?: boolean;
// filtered: FilterResult[]
filtered: unknown; // TODO
content?: string;
text?: string;
reblog?: ApiStatusJSON;
application?: ApiStatusApplicationJSON;
account: ApiAccountJSON;
media_attachments: ApiMediaAttachmentJSON[];
mentions: ApiMentionJSON[];
tags: ApiTagJSON[];
emojis: ApiCustomEmojiJSON[];
card?: ApiPreviewCardJSON;
poll?: ApiPollJSON;
}

View file

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

View file

@ -1,17 +1,19 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { EmptyAccount } from 'mastodon/components/empty_account';
import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { me } from '../initial_state';
import { Avatar } from './avatar';
@ -30,151 +32,151 @@ const messages = defineMessages({
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
more: { id: 'status.more', defaultMessage: 'More' },
});
class Account extends ImmutablePureComponent {
const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, minimal, defaultAction, withBio }) => {
const intl = useIntl();
static propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.record,
onFollow: PropTypes.func,
onBlock: PropTypes.func,
onMute: PropTypes.func,
onMuteNotifications: PropTypes.func,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
minimal: PropTypes.bool,
defaultAction: PropTypes.string,
withBio: PropTypes.bool,
};
const handleFollow = useCallback(() => {
onFollow(account);
}, [onFollow, account]);
static defaultProps = {
size: 46,
};
const handleBlock = useCallback(() => {
onBlock(account);
}, [onBlock, account]);
handleFollow = () => {
this.props.onFollow(this.props.account);
};
const handleMute = useCallback(() => {
onMute(account);
}, [onMute, account]);
handleBlock = () => {
this.props.onBlock(this.props.account);
};
const handleMuteNotifications = useCallback(() => {
onMuteNotifications(account, true);
}, [onMuteNotifications, account]);
handleMute = () => {
this.props.onMute(this.props.account);
};
const handleUnmuteNotifications = useCallback(() => {
onMuteNotifications(account, false);
}, [onMuteNotifications, account]);
handleMuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, true);
};
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
};
render () {
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
}
if (hidden) {
return (
<>
{account.get('display_name')}
{account.get('username')}
</>
);
}
let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
} else if (blocking) {
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) {
hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
}
buttons = (
<>
<Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
{hidingNotificationsButton}
</>
);
} else if (defaultAction === 'mute') {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
} else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
} else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
}
}
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
}
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
}
if (hidden) {
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'>
<Avatar account={account} size={size} />
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
</div>
)}
</div>
</Link>
{!minimal && (
<div className='account__relationship'>
{buttons}
</div>
)}
</div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div>
<>
{account.get('display_name')}
{account.get('username')}
</>
);
}
}
let buttons;
export default injectIntl(Account);
if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={handleFollow} />;
} else if (blocking) {
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={handleBlock} />;
} else if (muting) {
let menu;
if (account.getIn(['relationship', 'muting_notifications'])) {
menu = [{ text: intl.formatMessage(messages.unmute_notifications), action: handleUnmuteNotifications }];
} else {
menu = [{ text: intl.formatMessage(messages.mute_notifications), action: handleMuteNotifications }];
}
buttons = (
<>
<DropdownMenuContainer
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
<Button text={intl.formatMessage(messages.unmute)} onClick={handleMute} />
</>
);
} else if (defaultAction === 'mute') {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={handleMute} />;
} else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
} else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={handleFollow} />;
}
}
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'>
<Avatar account={account} size={size} />
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
</div>
)}
</div>
</Link>
{!minimal && (
<div className='account__relationship'>
{buttons}
</div>
)}
</div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div>
);
};
Account.propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.record,
onFollow: PropTypes.func,
onBlock: PropTypes.func,
onMute: PropTypes.func,
onMuteNotifications: PropTypes.func,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
minimal: PropTypes.bool,
defaultAction: PropTypes.string,
withBio: PropTypes.bool,
};
export default Account;

View file

@ -1,26 +1,26 @@
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react';
import classNames from 'classnames';
interface BaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
interface BaseProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
block?: boolean;
secondary?: boolean;
text?: JSX.Element;
}
interface PropsWithChildren extends BaseProps {
text?: never;
interface PropsChildren extends PropsWithChildren<BaseProps> {
text?: undefined;
}
interface PropsWithText extends BaseProps {
text: JSX.Element;
children: never;
text: JSX.Element | string;
children?: undefined;
}
type Props = PropsWithText | PropsWithChildren;
type Props = PropsWithText | PropsChildren;
export const Button: React.FC<Props> = ({
text,
type = 'button',
onClick,
disabled,
@ -28,6 +28,7 @@ export const Button: React.FC<Props> = ({
secondary,
className,
title,
text,
children,
...props
}) => {

View file

@ -24,7 +24,7 @@ export type StatusLike = Record<{
function normalizeHashtag(hashtag: string) {
return (
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
!!hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
).normalize('NFKC');
}

View file

@ -191,7 +191,7 @@ const timeRemainingString = (
interface Props {
intl: IntlShape;
timestamp: string;
year: number;
year?: number;
futureDate?: boolean;
short?: boolean;
}
@ -203,11 +203,6 @@ class RelativeTimestamp extends Component<Props, States> {
now: Date.now(),
};
static defaultProps = {
year: new Date().getFullYear(),
short: true,
};
_timer: number | undefined;
shouldComponentUpdate(nextProps: Props, nextState: States) {
@ -257,7 +252,13 @@ class RelativeTimestamp extends Component<Props, States> {
}
render() {
const { timestamp, intl, year, futureDate, short } = this.props;
const {
timestamp,
intl,
futureDate,
year = new Date().getFullYear(),
short = true,
} = this.props;
const timeGiven = timestamp.includes('T');
const date = new Date(timestamp);

View file

@ -81,7 +81,7 @@ class StatusActionBar extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
relationship: ImmutablePropTypes.record,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,

View file

@ -4,11 +4,10 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import type { StatusVisibility } from 'mastodon/models/status';
import { Icon } from './icon';
type Visibility = 'public' | 'unlisted' | 'private' | 'direct';
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: {
@ -25,7 +24,7 @@ const messages = defineMessages({
},
});
export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
export const VisibilityIcon: React.FC<{ visibility: StatusVisibility }> = ({
visibility,
}) => {
const intl = useIntl();

View file

@ -8,7 +8,6 @@ import {
} from '../actions/accounts';
import { showAlertForError } from '../actions/alerts';
import { initBlockModal } from '../actions/blocks';
import { initBoostModal } from '../actions/boosts';
import {
replyCompose,
mentionCompose,
@ -107,7 +106,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
if ((e && e.shiftKey) || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
},
@ -262,7 +261,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
},
deployPictureInPicture (status, type, mediaProps) {
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps}));
},
onInteractionModal (type, status) {

View file

@ -22,23 +22,23 @@ describe('emoji', () => {
it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">');
'<picture><img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg"></picture>');
expect(emojify('👨‍👩‍👧‍👧')).toEqual(
'<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">');
expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg">');
'<picture><img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg"></picture>');
expect(emojify('👩‍👩‍👦')).toEqual('<picture><img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg"></picture>');
expect(emojify('\u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
});
it('does multiple unicode', () => {
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture><picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> bar');
'foo <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> bar');
});
it('ignores unicode inside of tags', () => {
@ -46,16 +46,16 @@ describe('emoji', () => {
});
it('does multiple emoji properly (issue 5188)', () => {
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
expect(emojify('👌🌈💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture><picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
expect(emojify('👌 🌈 💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture> <picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture> <picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
});
it('does an emoji that has no shortcode', () => {
expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg">');
expect(emojify('👁‍🗨')).toEqual('<picture><img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg"></picture>');
});
it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg">');
expect(emojify('↙️')).toEqual('<picture><img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg"></picture>');
});
it('avoid emojifying on invisible text', () => {
@ -67,11 +67,11 @@ describe('emoji', () => {
it('avoid emojifying on invisible text with nested tags', () => {
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
.toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
.toEqual('<span class="invisible">😄<br>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
});
it('does not emojify emojis with textual presentation VS15 character', () => {
@ -79,19 +79,19 @@ describe('emoji', () => {
.toEqual('✴︎');
});
it('does an simple emoji properly', () => {
it('does a simple emoji properly', () => {
expect(emojify('♀♂'))
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg">');
.toEqual('<picture><img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"></picture><picture><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg"></picture>');
});
it('does an emoji containing ZWJ properly', () => {
expect(emojify('💂‍♀️💂‍♂️'))
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
.toEqual('<picture><img draggable="false" class="emojione" alt="💂\u200D♀" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"></picture><picture><img draggable="false" class="emojione" alt="💂\u200D♂" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg"></picture>');
});
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'))
.toEqual('<p><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>');
.toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>');
});
});
});

View file

@ -17,8 +17,13 @@ const emojiFilenames = (emojis) => {
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺', '📱', '📲', '🚲', '🪮', '🐦‍⬛']);
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️', '🪽', '🪿']);
const emojiFilename = (filename) => {
const borderedEmoji = (document.body && document.body.classList.contains('theme-mastodon-light')) ? lightEmoji : darkEmoji;
/**
* @param {string} filename
* @param {"light" | "dark" } colorScheme
* @returns {string}
*/
const emojiFilename = (filename, colorScheme) => {
const borderedEmoji = colorScheme === "light" ? lightEmoji : darkEmoji;
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
};
@ -92,12 +97,30 @@ const emojifyTextNode = (node, customEmojis) => {
const { filename, shortCode } = unicodeMapping[unicode_emoji];
const title = shortCode ? `:${shortCode}:` : '';
replacement = document.createElement('img');
replacement.setAttribute('draggable', 'false');
replacement.setAttribute('class', 'emojione');
replacement.setAttribute('alt', unicode_emoji);
replacement.setAttribute('title', title);
replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`);
replacement = document.createElement('picture');
const isSystemTheme = !!document.body?.classList.contains('theme-system');
if(isSystemTheme) {
let source = document.createElement('source');
source.setAttribute('media', '(prefers-color-scheme: dark)');
source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`);
replacement.appendChild(source);
}
let img = document.createElement('img');
img.setAttribute('draggable', 'false');
img.setAttribute('class', 'emojione');
img.setAttribute('alt', unicode_emoji);
img.setAttribute('title', title);
let theme = "light";
if(!isSystemTheme && !document.body?.classList.contains('theme-mastodon-light'))
theme = "dark";
img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`);
replacement.appendChild(img);
}
// Add the processed-up-to-now string and the emoji replacement

View file

@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
import { dismissSuggestion } from 'mastodon/actions/suggestions';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import { domain } from 'mastodon/initial_state';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" },
});
export const Card = ({ id, source }) => {
const intl = useIntl();
const account = useSelector(state => state.getIn(['accounts', id]));
const relationship = useSelector(state => state.getIn(['relationships', id]));
const dispatch = useDispatch();
const following = relationship?.get('following') ?? relationship?.get('requested');
const handleFollow = useCallback(() => {
if (following) {
dispatch(unfollowAccount(id));
} else {
dispatch(followAccount(id));
}
}, [id, following, dispatch]);
const handleDismiss = useCallback(() => {
dispatch(dismissSuggestion(id));
}, [id, dispatch]);
let label;
switch (source) {
case 'friends_of_friends':
label = <FormattedMessage id='follow_suggestions.friends_of_friends_longer' defaultMessage='Popular among people you follow' />;
break;
case 'similar_to_recently_followed':
label = <FormattedMessage id='follow_suggestions.similar_to_recently_followed_longer' defaultMessage='Similar to profiles you recently followed' />;
break;
case 'featured':
label = <FormattedMessage id='follow_suggestions.featured_longer' defaultMessage='Hand-picked by the {domain} team' values={{ domain }} />;
break;
case 'most_followed':
label = <FormattedMessage id='follow_suggestions.popular_suggestion_longer' defaultMessage='Popular on {domain}' values={{ domain }} />;
break;
case 'most_interactions':
label = <FormattedMessage id='follow_suggestions.popular_suggestion_longer' defaultMessage='Popular on {domain}' values={{ domain }} />;
break;
}
return (
<div className='explore__suggestions__card'>
<div className='explore__suggestions__card__source'>
{label}
</div>
<div className='explore__suggestions__card__body'>
<Link to={`/@${account.get('acct')}`}><Avatar account={account} size={48} /></Link>
<div className='explore__suggestions__card__body__main'>
<div className='explore__suggestions__card__body__main__name-button'>
<Link className='explore__suggestions__card__body__main__name-button__name' to={`/@${account.get('acct')}`}><DisplayName account={account} /></Link>
<IconButton iconComponent={CloseIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} secondary={following} onClick={handleFollow} />
</div>
</div>
</div>
</div>
);
};
Card.propTypes = {
id: PropTypes.string.isRequired,
source: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']),
};

View file

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import AccountCard from 'mastodon/features/directory/components/account_card';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Card } from './components/card';
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
@ -54,7 +55,11 @@ class Suggestions extends PureComponent {
return (
<div className='explore__suggestions scrollable' data-nosnippet>
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
<Card
key={suggestion.get('account')}
id={suggestion.get('account')}
source={suggestion.getIn(['sources', 0])}
/>
))}
</div>
);

View file

@ -82,7 +82,7 @@ class GettingStarted extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.map,
myAccount: ImmutablePropTypes.record,
multiColumn: PropTypes.bool,
fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number,

View file

@ -107,7 +107,7 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
</tr>
<tr>
<td><kbd>s</kbd></td>
<td><kbd>s</kbd>, <kbd>/</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
</tr>
<tr>

View file

@ -27,7 +27,7 @@ export const FilteredNotificationsBanner = () => {
};
}, [dispatch]);
if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) * 1 === 0) {
if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) === 0) {
return null;
}
@ -41,7 +41,8 @@ export const FilteredNotificationsBanner = () => {
</div>
<div className='filtered-notifications-banner__badge'>
{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}
<div className='filtered-notifications-banner__badge__badge'>{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}</div>
<FormattedMessage id='filtered_notifications_banner.mentions' defaultMessage='{count, plural, one {mention} other {mentions}}' values={{ count: policy.getIn(['summary', 'pending_notifications_count']) }} />
</div>
</Link>
);

View file

@ -0,0 +1,78 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import WarningIcon from '@/material-icons/400-24px/warning-fill.svg?react';
import { Icon } from 'mastodon/components/icon';
// This needs to be kept in sync with app/models/account_warning.rb
const messages = defineMessages({
none: {
id: 'notification.moderation_warning.action_none',
defaultMessage: 'Your account has received a moderation warning.',
},
disable: {
id: 'notification.moderation_warning.action_disable',
defaultMessage: 'Your account has been disabled.',
},
mark_statuses_as_sensitive: {
id: 'notification.moderation_warning.action_mark_statuses_as_sensitive',
defaultMessage: 'Some of your posts have been marked as sensitive.',
},
delete_statuses: {
id: 'notification.moderation_warning.action_delete_statuses',
defaultMessage: 'Some of your posts have been removed.',
},
sensitive: {
id: 'notification.moderation_warning.action_sensitive',
defaultMessage: 'Your posts will be marked as sensitive from now on.',
},
silence: {
id: 'notification.moderation_warning.action_silence',
defaultMessage: 'Your account has been limited.',
},
suspend: {
id: 'notification.moderation_warning.action_suspend',
defaultMessage: 'Your account has been suspended.',
},
});
interface Props {
action:
| 'none'
| 'disable'
| 'mark_statuses_as_sensitive'
| 'delete_statuses'
| 'sensitive'
| 'silence'
| 'suspend';
id: string;
hidden: boolean;
}
export const ModerationWarning: React.FC<Props> = ({ action, id, hidden }) => {
const intl = useIntl();
if (hidden) {
return null;
}
return (
<a
href={`/disputes/strikes/${id}`}
target='_blank'
rel='noopener noreferrer'
className='notification__moderation-warning'
>
<Icon id='warning' icon={WarningIcon} />
<div className='notification__moderation-warning__content'>
<p>{intl.formatMessage(messages[action])}</p>
<span className='link-button'>
<FormattedMessage
id='notification.moderation-warning.learn_more'
defaultMessage='Learn more'
/>
</span>
</div>
</a>
);
};

View file

@ -12,7 +12,6 @@ import { HotKeys } from 'react-hotkeys';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react';
@ -27,7 +26,8 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import FollowRequestContainer from '../containers/follow_request_container';
import RelationshipsSeveranceEvent from './relationships_severance_event';
import { ModerationWarning } from './moderation_warning';
import { RelationshipsSeveranceEvent } from './relationships_severance_event';
import Report from './report';
const messages = defineMessages({
@ -40,6 +40,8 @@ const messages = defineMessages({
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
relationshipsSevered: { id: 'notification.relationships_severance_event', defaultMessage: 'Lost connections with {name}' },
moderationWarning: { id: 'notification.moderation_warning', defaultMessage: 'Your have received a moderation warning' },
});
const notificationForScreenReader = (intl, message, timestamp) => {
@ -361,24 +363,44 @@ class Notification extends ImmutablePureComponent {
}
renderRelationshipsSevered (notification) {
const { intl, unread } = this.props;
const { intl, unread, hidden } = this.props;
const event = notification.get('event');
if (!notification.get('event')) {
if (!event) {
return null;
}
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-severed-relationships focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: notification.getIn(['event', 'target_name']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='heart_broken' icon={HeartBrokenIcon} />
<div className={classNames('notification notification-severed-relationships focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.relationshipsSevered, { name: notification.getIn(['event', 'target_name']) }), notification.get('created_at'))}>
<RelationshipsSeveranceEvent
type={event.get('type')}
target={event.get('target_name')}
followersCount={event.get('followers_count')}
followingCount={event.get('following_count')}
hidden={hidden}
/>
</div>
</HotKeys>
);
}
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.severed_relationships' defaultMessage='Relationships with {name} severed' values={{ name: notification.getIn(['event', 'target_name']) }} />
</span>
</div>
renderModerationWarning (notification) {
const { intl, unread, hidden } = this.props;
const warning = notification.get('moderation_warning');
<RelationshipsSeveranceEvent event={notification.get('event')} />
if (!warning) {
return null;
}
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-moderation-warning focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.moderationWarning), notification.get('created_at'))}>
<ModerationWarning
action={warning.get('action')}
id={warning.get('id')}
hidden={hidden}
/>
</div>
</HotKeys>
);
@ -457,6 +479,8 @@ class Notification extends ImmutablePureComponent {
return this.renderPoll(notification, account);
case 'severed_relationships':
return this.renderRelationshipsSevered(notification);
case 'moderation_warning':
return this.renderModerationWarning(notification);
case 'admin.sign_up':
return this.renderAdminSignUp(notification, account, link);
case 'admin.report':

View file

@ -7,8 +7,8 @@ import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
import { Avatar } from 'mastodon/components/avatar';
import { IconButton } from 'mastodon/components/icon_button';
@ -51,7 +51,7 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
</Link>
<div className='notification-request__actions'>
<IconButton iconComponent={VolumeOffIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<IconButton iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
</div>
</div>

View file

@ -2,60 +2,44 @@ import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
import { Icon } from 'mastodon/components/icon';
import { domain } from 'mastodon/initial_state';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
// This needs to be kept in sync with app/models/relationship_severance_event.rb
// This needs to be kept in sync with app/models/relationships_severance_event.rb
const messages = defineMessages({
account_suspension: { id: 'relationship_severance_notification.types.account_suspension', defaultMessage: 'Account has been suspended' },
domain_block: { id: 'relationship_severance_notification.types.domain_block', defaultMessage: 'Domain has been suspended' },
user_domain_block: { id: 'relationship_severance_notification.types.user_domain_block', defaultMessage: 'You blocked this domain' },
account_suspension: { id: 'notification.relationships_severance_event.account_suspension', defaultMessage: 'An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.' },
domain_block: { id: 'notification.relationships_severance_event.domain_block', defaultMessage: 'An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
user_domain_block: { id: 'notification.relationships_severance_event.user_domain_block', defaultMessage: 'You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
});
const RelationshipsSeveranceEvent = ({ event, hidden }) => {
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden }) => {
const intl = useIntl();
if (hidden || !event) {
if (hidden) {
return null;
}
return (
<div className='notification__report'>
<div className='notification__report__details'>
<div>
<RelativeTimestamp timestamp={event.get('created_at')} short={false} />
{' · '}
{ event.get('purged') ? (
<FormattedMessage
id='relationship_severance_notification.purged_data'
defaultMessage='purged by administrators'
/>
) : (
<FormattedMessage
id='relationship_severance_notification.relationships'
defaultMessage='{count, plural, one {# relationship} other {# relationships}}'
values={{ count: event.get('followers_count', 0) + event.get('following_count', 0) }}
/>
)}
<br />
<strong>{intl.formatMessage(messages[event.get('type')])}</strong>
</div>
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='notification__relationships-severance-event'>
<Icon id='heart_broken' icon={HeartBrokenIcon} />
<div className='notification__report__actions'>
<a href='/severed_relationships' className='button' target='_blank' rel='noopener noreferrer'>
<FormattedMessage id='relationship_severance_notification.view' defaultMessage='View' />
</a>
</div>
<div className='notification__relationships-severance-event__content'>
<p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p>
<span className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></span>
</div>
</div>
</a>
);
};
RelationshipsSeveranceEvent.propTypes = {
event: ImmutablePropTypes.map.isRequired,
type: PropTypes.oneOf([
'account_suspension',
'domain_block',
'user_domain_block',
]).isRequired,
target: PropTypes.string.isRequired,
followersCount: PropTypes.number.isRequired,
followingCount: PropTypes.number.isRequired,
hidden: PropTypes.bool,
};
export default RelationshipsSeveranceEvent;

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux';
import { initBoostModal } from '../../../actions/boosts';
import { mentionCompose } from '../../../actions/compose';
import {
reblog,
@ -8,6 +7,7 @@ import {
unreblog,
unfavourite,
} from '../../../actions/interactions';
import { openModal } from '../../../actions/modal';
import {
hideStatus,
revealStatus,
@ -49,7 +49,7 @@ const mapDispatchToProps = dispatch => ({
if (e.shiftKey || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
}
},

View file

@ -16,6 +16,7 @@ import EditNoteIcon from '@/material-icons/400-24px/edit_note.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import { focusCompose } from 'mastodon/actions/compose';
import { Icon } from 'mastodon/components/icon';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import Column from 'mastodon/features/ui/components/column';
import { me } from 'mastodon/initial_state';
import { useAppSelector } from 'mastodon/store';
@ -42,42 +43,44 @@ const Onboarding = () => {
return (
<Column>
<Switch>
<Route path='/start' exact>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<img src={illustration} alt='' className='onboarding__illustration' />
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
{account ? (
<Switch>
<Route path='/start' exact>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<img src={illustration} alt='' className='onboarding__illustration' />
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
</div>
<div className='onboarding__steps'>
<Step to='/start/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
<Step to='/start/follows' completed={(account.get('following_count') * 1) >= 1} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
<Step onClick={handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
<Step to='/start/share' icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
<div className='onboarding__links'>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
</div>
</Route>
<div className='onboarding__steps'>
<Step to='/start/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
<Step to='/start/follows' completed={(account.get('following_count') * 1) >= 1} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
<Step onClick={handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
<Step to='/start/share' icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
<div className='onboarding__links'>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
</div>
</Route>
<Route path='/start/profile' component={Profile} />
<Route path='/start/follows' component={Follows} />
<Route path='/start/share' component={Share} />
</Switch>
<Route path='/start/profile' component={Profile} />
<Route path='/start/follows' component={Follows} />
<Route path='/start/share' component={Share} />
</Switch>
) : <NotSignedInIndicator />}
<Helmet>
<meta name='robots' content='noindex' />

View file

@ -14,7 +14,6 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { initBoostModal } from 'mastodon/actions/boosts';
import { replyCompose } from 'mastodon/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal';
@ -140,7 +139,7 @@ class Footer extends ImmutablePureComponent {
} else if ((e && e.shiftKey) || !boostModal) {
this._performReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this._performReblog } }));
}
} else {
dispatch(openModal({
@ -210,4 +209,4 @@ class Footer extends ImmutablePureComponent {
}
export default withRouter(connect(makeMapStateToProps)(injectIntl(Footer)));
export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer)));

View file

@ -1,51 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const mapStateToProps = (state, { accountId }) => ({
account: state.getIn(['accounts', accountId]),
});
class Header extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
statusId: PropTypes.string.isRequired,
account: ImmutablePropTypes.record.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { account, statusId, onClose, intl } = this.props;
return (
<div className='picture-in-picture__header'>
<Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'>
<Avatar account={account} size={36} />
<DisplayName account={account} />
</Link>
<IconButton icon='times' iconComponent={CloseIcon} onClick={onClose} title={intl.formatMessage(messages.close)} />
</div>
);
}
}
export default connect(mapStateToProps)(injectIntl(Header));

View file

@ -0,0 +1,46 @@
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import { useAppSelector } from 'mastodon/store';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
interface Props {
accountId: string;
statusId: string;
onClose: () => void;
}
export const Header: React.FC<Props> = ({ accountId, statusId, onClose }) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
const intl = useIntl();
if (!account) return null;
return (
<div className='picture-in-picture__header'>
<Link
to={`/@${account.get('acct')}/${statusId}`}
className='picture-in-picture__header__account'
>
<Avatar account={account} size={36} />
<DisplayName account={account} />
</Link>
<IconButton
icon='times'
iconComponent={CloseIcon}
onClick={onClose}
title={intl.formatMessage(messages.close)}
/>
</div>
);
};

View file

@ -1,89 +0,0 @@
import PropTypes from 'prop-types';
import { Component } from 'react';
import { connect } from 'react-redux';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import Audio from 'mastodon/features/audio';
import Video from 'mastodon/features/video';
import Footer from './components/footer';
import Header from './components/header';
const mapStateToProps = state => ({
...state.get('picture_in_picture'),
});
class PictureInPicture extends Component {
static propTypes = {
statusId: PropTypes.string,
accountId: PropTypes.string,
type: PropTypes.string,
src: PropTypes.string,
muted: PropTypes.bool,
volume: PropTypes.number,
currentTime: PropTypes.number,
poster: PropTypes.string,
backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string,
accentColor: PropTypes.string,
dispatch: PropTypes.func.isRequired,
};
handleClose = () => {
const { dispatch } = this.props;
dispatch(removePictureInPicture());
};
render () {
const { type, src, currentTime, accountId, statusId } = this.props;
if (!currentTime) {
return null;
}
let player;
if (type === 'video') {
player = (
<Video
src={src}
currentTime={this.props.currentTime}
volume={this.props.volume}
muted={this.props.muted}
autoPlay
inline
alwaysVisible
/>
);
} else if (type === 'audio') {
player = (
<Audio
src={src}
currentTime={this.props.currentTime}
volume={this.props.volume}
muted={this.props.muted}
poster={this.props.poster}
backgroundColor={this.props.backgroundColor}
foregroundColor={this.props.foregroundColor}
accentColor={this.props.accentColor}
autoPlay
/>
);
}
return (
<div className='picture-in-picture'>
<Header accountId={accountId} statusId={statusId} onClose={this.handleClose} />
{player}
<Footer statusId={statusId} />
</div>
);
}
}
export default connect(mapStateToProps)(PictureInPicture);

View file

@ -0,0 +1,79 @@
import { useCallback } from 'react';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import Audio from 'mastodon/features/audio';
import Video from 'mastodon/features/video';
import { useAppDispatch, useAppSelector } from 'mastodon/store/typed_functions';
import Footer from './components/footer';
import { Header } from './components/header';
export const PictureInPicture: React.FC = () => {
const dispatch = useAppDispatch();
const handleClose = useCallback(() => {
dispatch(removePictureInPicture());
}, [dispatch]);
const pipState = useAppSelector((s) => s.picture_in_picture);
if (pipState.type === null) {
return null;
}
const {
type,
src,
currentTime,
accountId,
statusId,
volume,
muted,
poster,
backgroundColor,
foregroundColor,
accentColor,
} = pipState;
let player;
switch (type) {
case 'video':
player = (
<Video
src={src}
currentTime={currentTime}
volume={volume}
muted={muted}
autoPlay
inline
alwaysVisible
/>
);
break;
case 'audio':
player = (
<Audio
src={src}
currentTime={currentTime}
volume={volume}
muted={muted}
poster={poster}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
accentColor={accentColor}
autoPlay
/>
);
}
return (
<div className='picture-in-picture'>
<Header accountId={accountId} statusId={statusId} onClose={handleClose} />
{player}
<Footer statusId={statusId} />
</div>
);
};

View file

@ -74,7 +74,7 @@ class ActionBar extends PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
relationship: ImmutablePropTypes.record,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,

View file

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { showAlertForError } from '../../../actions/alerts';
import { initBlockModal } from '../../../actions/blocks';
import { initBoostModal } from '../../../actions/boosts';
import {
replyCompose,
mentionCompose,
@ -85,7 +84,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (e.shiftKey || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
}
}
},

View file

@ -27,7 +27,6 @@ import {
unmuteAccount,
} from '../../actions/accounts';
import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts';
import {
replyCompose,
mentionCompose,
@ -317,7 +316,7 @@ class Status extends ImmutablePureComponent {
if ((e && e.shiftKey) || !boostModal) {
this.handleModalReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.handleModalReblog } }));
}
}
} else {

View file

@ -1,125 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { changeBoostPrivacy } from 'mastodon/actions/boosts';
import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon';
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Avatar } from '../../../components/avatar';
import { Button } from '../../../components/button';
import { DisplayName } from '../../../components/display_name';
import { RelativeTimestamp } from '../../../components/relative_timestamp';
import StatusContent from '../../../components/status_content';
const messages = defineMessages({
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
});
const mapStateToProps = state => {
return {
privacy: state.getIn(['boosts', 'new', 'privacy']),
};
};
const mapDispatchToProps = dispatch => {
return {
onChangeBoostPrivacy(value) {
dispatch(changeBoostPrivacy(value));
},
};
};
class BoostModal extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
onReblog: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onChangeBoostPrivacy: PropTypes.func.isRequired,
privacy: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
...WithRouterPropTypes,
};
handleReblog = () => {
this.props.onReblog(this.props.status, this.props.privacy);
this.props.onClose();
};
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.onClose();
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
}
};
_findContainer = () => {
return document.getElementsByClassName('modal-root__container')[0];
};
render () {
const { status, privacy, intl } = this.props;
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
return (
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
<div className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />
</a>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name'>
<div className='status__avatar'>
<Avatar account={status.get('account')} size={48} />
</div>
<DisplayName account={status.get('account')} />
</a>
</div>
<StatusContent status={status} />
{status.get('media_attachments').size > 0 && (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
)}
</div>
</div>
<div className='boost-modal__action-bar'>
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' icon={RepeatIcon} /></span> }} /></div>
{status.get('visibility') !== 'private' && !status.get('reblogged') && (
<PrivacyDropdown
noDirect
value={privacy}
container={this._findContainer}
onChange={this.props.onChangeBoostPrivacy}
/>
)}
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} autoFocus />
</div>
</div>
);
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal)));

View file

@ -0,0 +1,162 @@
import type { MouseEventHandler } from 'react';
import { useCallback, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router';
import type Immutable from 'immutable';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon';
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
import type { Account } from 'mastodon/models/account';
import type { Status, StatusVisibility } from 'mastodon/models/status';
import { useAppSelector } from 'mastodon/store';
import { Avatar } from '../../../components/avatar';
import { Button } from '../../../components/button';
import { DisplayName } from '../../../components/display_name';
import { RelativeTimestamp } from '../../../components/relative_timestamp';
import StatusContent from '../../../components/status_content';
const messages = defineMessages({
cancel_reblog: {
id: 'status.cancel_reblog_private',
defaultMessage: 'Unboost',
},
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
});
export const BoostModal: React.FC<{
status: Status;
onClose: () => void;
onReblog: (status: Status, privacy: StatusVisibility) => void;
}> = ({ status, onReblog, onClose }) => {
const intl = useIntl();
const history = useHistory();
const default_privacy = useAppSelector(
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
(state) => state.compose.get('default_privacy') as StatusVisibility,
);
const account = status.get('account') as Account;
const statusVisibility = status.get('visibility') as StatusVisibility;
const [privacy, setPrivacy] = useState<StatusVisibility>(
statusVisibility === 'private' ? 'private' : default_privacy,
);
const onPrivacyChange = useCallback((value: StatusVisibility) => {
setPrivacy(value);
}, []);
const handleReblog = useCallback(() => {
onReblog(status, privacy);
onClose();
}, [onClose, onReblog, status, privacy]);
const handleAccountClick = useCallback<MouseEventHandler>(
(e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
onClose();
history.push(`/@${account.acct}`);
}
},
[history, onClose, account],
);
const buttonText = status.get('reblogged')
? messages.cancel_reblog
: messages.reblog;
const findContainer = useCallback(
() => document.getElementsByClassName('modal-root__container')[0],
[],
);
return (
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div
className={classNames(
'status',
`status-${statusVisibility}`,
'light',
)}
>
<div className='status__info'>
<a
href={`/@${account.acct}/${status.get('id') as string}`}
className='status__relative-time'
target='_blank'
rel='noopener noreferrer'
>
<span className='status__visibility-icon'>
<VisibilityIcon visibility={statusVisibility} />
</span>
<RelativeTimestamp
timestamp={status.get('created_at') as string}
/>
</a>
<a
onClick={handleAccountClick}
href={`/@${account.acct}`}
className='status__display-name'
>
<div className='status__avatar'>
<Avatar account={account} size={48} />
</div>
<DisplayName account={account} />
</a>
</div>
{/* @ts-expect-error Expected until StatusContent is typed */}
<StatusContent status={status} />
{(status.get('media_attachments') as Immutable.List<unknown>).size >
0 && (
<AttachmentList compact media={status.get('media_attachments')} />
)}
</div>
</div>
<div className='boost-modal__action-bar'>
<div>
<FormattedMessage
id='boost_modal.combo'
defaultMessage='You can press {combo} to skip this next time'
values={{
combo: (
<span>
Shift + <Icon id='retweet' icon={RepeatIcon} />
</span>
),
}}
/>
</div>
{statusVisibility !== 'private' && !status.get('reblogged') && (
<PrivacyDropdown
noDirect
value={privacy}
container={findContainer}
onChange={onPrivacyChange}
/>
)}
<Button
text={intl.formatMessage(buttonText)}
onClick={handleReblog}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
/>
</div>
</div>
);
};

View file

@ -24,7 +24,7 @@ import BundleContainer from '../containers/bundle_container';
import ActionsModal from './actions_modal';
import AudioModal from './audio_modal';
import BoostModal from './boost_modal';
import { BoostModal } from './boost_modal';
import BundleModalError from './bundle_modal_error';
import ConfirmationModal from './confirmation_modal';
import FocalPointModal from './focal_point_modal';

View file

@ -14,7 +14,7 @@ import { HotKeys } from 'react-hotkeys';
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import PictureInPicture from 'mastodon/features/picture_in_picture';
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -89,7 +89,7 @@ const mapStateToProps = state => ({
const keyMap = {
help: '?',
new: 'n',
search: 's',
search: ['s', '/'],
forceNew: 'option+n',
toggleComposeSpoilers: 'option+x',
focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],

View file

@ -89,6 +89,14 @@
"announcement.announcement": "إعلان",
"attachments_list.unprocessed": "(غير معالَج)",
"audio.hide": "إخفاء المقطع الصوتي",
"block_modal.remote_users_caveat": "Do ti kërkojmë shërbyesit {domain} të respektojë vendimin tuaj. Por, pajtimi sështë i garantuar, ngaqë disa shërbyes mund ti trajtojnë ndryshe bllokimet. Psotimet publike mundet të jenë ende të dukshme për përdorues pa bërë hyrje në llogari.",
"block_modal.show_less": "اعرض أقلّ",
"block_modal.show_more": "أظهر المزيد",
"block_modal.they_cant_mention": "لن يستطيع ذِكرك أو متابعتك.",
"block_modal.they_cant_see_posts": "لن يستطيع رؤية منشوراتك ولن ترى منشوراته.",
"block_modal.they_will_know": "يمكنه أن يرى أنه قد تم حجبه.",
"block_modal.title": "أتريد حظر المستخدم؟",
"block_modal.you_wont_see_mentions": "لن تر المنشورات التي يُشار فيهم إليه.",
"boost_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة",
"bundle_column_error.copy_stacktrace": "انسخ تقرير الخطأ",
"bundle_column_error.error.body": "لا يمكن تقديم الصفحة المطلوبة. قد يكون بسبب خطأ في التعليمات البرمجية، أو مشكلة توافق المتصفح.",
@ -169,6 +177,7 @@
"confirmations.delete_list.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَذفَ هذِهِ القائمة بشكلٍ دائم؟",
"confirmations.discard_edit_media.confirm": "تجاهل",
"confirmations.discard_edit_media.message": "لديك تغييرات غير محفوظة لوصف الوسائط أو معاينتها، أتريد تجاهلها على أي حال؟",
"confirmations.domain_block.confirm": "حظر الخادم",
"confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.",
"confirmations.edit.confirm": "تعديل",
"confirmations.edit.message": "التعديل في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد تحريرها. متأكد من أنك تريد المواصلة؟",
@ -200,6 +209,23 @@
"dismissable_banner.explore_statuses": "هذه هي المنشورات الرائجة على الشبكات الاجتماعيّة اليوم. تظهر المنشورات المعاد نشرها والحائزة على مفضّلات أكثر في مرتبة عليا.",
"dismissable_banner.explore_tags": "هذه هي الوسوم تكتسب جذب الاهتمام حاليًا على الويب الاجتماعي. الوسوم التي يستخدمها مختلف الناس تحتل مرتبة عليا.",
"dismissable_banner.public_timeline": "هذه هي أحدث المنشورات العامة من الناس على الشبكة الاجتماعية التي يتبعها الناس على {domain}.",
"domain_block_modal.block": "حظر الخادم",
"domain_block_modal.block_account_instead": "أحجب @{name} بدلاً من ذلك",
"domain_block_modal.they_can_interact_with_old_posts": "يمكن للأشخاص من هذا الخادم التفاعل مع منشوراتك القديمة.",
"domain_block_modal.they_cant_follow": "لا أحد من هذا الخادم يمكنه متابعتك.",
"domain_block_modal.they_wont_know": "لن يَعرف أنه قد تم حظره.",
"domain_block_modal.title": "أتريد حظر النطاق؟",
"domain_block_modal.you_will_lose_followers": "سيتم إزالة جميع متابعيك من هذا الخادم.",
"domain_block_modal.you_wont_see_posts": "لن ترى منشورات أو إشعارات من المستخدمين على هذا الخادم.",
"domain_pill.activitypub_lets_connect": "يتيح لك التواصل والتفاعل مع الناس ليس فقط على ماستدون، ولكن عبر تطبيقات اجتماعية مختلفة أيضا.",
"domain_pill.activitypub_like_language": "إنّ ActivityPub مثل لغة ماستدون التي يتحدث بها مع شبكات اجتماعية أخرى.",
"domain_pill.server": "الخادِم",
"domain_pill.their_handle": "مُعرِّفُه:",
"domain_pill.their_server": "بيتهم الرقمي، حيث تُستضاف كافة منشوراتهم.",
"domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.",
"domain_pill.username": "اسم المستخدم",
"domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟",
"domain_pill.your_handle": "عنوانك الكامل:",
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
"embed.preview": "إليك ما سيبدو عليه:",
"emoji_button.activity": "الأنشطة",
@ -266,6 +292,7 @@
"filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة",
"filter_modal.select_filter.title": "تصفية هذا المنشور",
"filter_modal.title.status": "تصفية منشور",
"filtered_notifications_banner.title": "الإشعارات المصفاة",
"firehose.all": "الكل",
"firehose.local": "هذا الخادم",
"firehose.remote": "خوادم أخرى",
@ -394,6 +421,13 @@
"loading_indicator.label": "جاري التحميل…",
"media_gallery.toggle_visible": "{number, plural, zero {} one {اخف الصورة} two {اخف الصورتين} few {اخف الصور} many {اخف الصور} other {اخف الصور}}",
"moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.",
"mute_modal.hide_options": "إخفاء الخيارات",
"mute_modal.show_options": "إظهار الخيارات",
"mute_modal.they_can_mention_and_follow": "سيكون بإمكانه الإشارة إليك ومتابعتك، لكنك لن تره.",
"mute_modal.they_wont_know": "لن يَعرف أنه قد تم كتمه.",
"mute_modal.title": "أتريد كتم المُستخدم؟",
"mute_modal.you_wont_see_mentions": "سوف لن تر المنشورات التي يُشار إليه.",
"mute_modal.you_wont_see_posts": "سيكون بإمكانه رؤية منشوراتك، لكنك لن ترى منشوراته.",
"navigation_bar.about": "عن",
"navigation_bar.advanced_interface": "افتحه في واجهة الويب المتقدمة",
"navigation_bar.blocks": "الحسابات المحجوبة",
@ -429,14 +463,21 @@
"notification.own_poll": "انتهى استطلاعك للرأي",
"notification.poll": "لقد انتهى استطلاع رأي شاركتَ فيه",
"notification.reblog": "قام {name} بمشاركة منشورك",
"notification.relationships_severance_event.learn_more": "اعرف المزيد",
"notification.status": "{name} نشر للتو",
"notification.update": "عدّلَ {name} منشورًا",
"notification_requests.accept": "موافقة",
"notification_requests.dismiss": "تخطي",
"notification_requests.notifications_from": "إشعارات من {name}",
"notification_requests.title": "الإشعارات المصفاة",
"notifications.clear": "مسح الإشعارات",
"notifications.clear_confirmation": "متأكد من أنك تود مسح جميع الإشعارات الخاصة بك و المتلقاة إلى حد الآن ؟",
"notifications.column_settings.admin.report": "التبليغات الجديدة:",
"notifications.column_settings.admin.sign_up": "التسجيلات الجديدة:",
"notifications.column_settings.alert": "إشعارات سطح المكتب",
"notifications.column_settings.favourite": "المفضلة:",
"notifications.column_settings.filter_bar.advanced": "عرض جميع الفئات",
"notifications.column_settings.filter_bar.category": "شريط التصفية السريعة",
"notifications.column_settings.follow": "متابعُون جُدُد:",
"notifications.column_settings.follow_request": "الطلبات الجديد لِمتابَعتك:",
"notifications.column_settings.mention": "الإشارات:",
@ -462,6 +503,10 @@
"notifications.permission_denied": "تنبيهات سطح المكتب غير متوفرة بسبب رفض أذونات المتصفح مسبقاً",
"notifications.permission_denied_alert": "لا يمكن تفعيل إشعارات سطح المكتب، لأن إذن المتصفح قد تم رفضه سابقاً",
"notifications.permission_required": "إشعارات سطح المكتب غير متوفرة لأنه لم يتم منح الإذن المطلوب.",
"notifications.policy.filter_new_accounts_title": "حسابات جديدة",
"notifications.policy.filter_not_followers_title": "أشخاص لا يتابعونك",
"notifications.policy.filter_not_following_hint": "حتى توافق عليهم يدويا",
"notifications.policy.filter_not_following_title": "أشخاص لا تتابعهم",
"notifications_permission_banner.enable": "تفعيل إشعارات سطح المكتب",
"notifications_permission_banner.how_to_control": "لتلقي الإشعارات عندما لا يكون ماستدون مفتوح، قم بتفعيل إشعارات سطح المكتب، يمكنك التحكم بدقة في أنواع التفاعلات التي تولد إشعارات سطح المكتب من خلال زر الـ{icon} أعلاه بمجرد تفعيلها.",
"notifications_permission_banner.title": "لا تفوت شيئاً أبداً",
@ -638,6 +683,7 @@
"status.direct": "إشارة خاصة لـ @{name}",
"status.direct_indicator": "إشارة خاصة",
"status.edit": "تعديل",
"status.edited": "آخر تعديل يوم {date}",
"status.edited_x_times": "عُدّل {count, plural, zero {} one {مرةً واحدة} two {مرّتان} few {{count} مرات} many {{count} مرة} other {{count} مرة}}",
"status.embed": "إدماج",
"status.favourite": "فضّل",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Скарыстайцеся існуючай катэгорыяй або стварыце новую",
"filter_modal.select_filter.title": "Фільтраваць гэты допіс",
"filter_modal.title.status": "Фільтраваць допіс",
"filtered_notifications_banner.mentions": "{count, plural, one {згадванне} few {згадванні} many {згадванняў} other {згадвання}}",
"filtered_notifications_banner.pending_requests": "Апавяшчэнні ад {count, plural, =0 {# людзей якіх} one {# чалавека якіх} few {# чалавек якіх} many {# людзей якіх} other {# чалавека якіх}} вы магчыма ведаеце",
"filtered_notifications_banner.title": "Адфільтраваныя апавяшчэнні",
"firehose.all": "Усе",
@ -471,6 +472,7 @@
"notification.own_poll": "Ваша апытанне скончылася",
"notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася",
"notification.reblog": "{name} пашырыў ваш допіс",
"notification.relationships_severance_event.learn_more": "Даведацца больш",
"notification.status": "Новы допіс ад {name}",
"notification.update": "Допіс {name} адрэдагаваны",
"notification_requests.accept": "Прыняць",
@ -483,6 +485,8 @@
"notifications.column_settings.admin.sign_up": "Новыя ўваходы:",
"notifications.column_settings.alert": "Апавяшчэнні на працоўным стале",
"notifications.column_settings.favourite": "Упадабанае:",
"notifications.column_settings.filter_bar.advanced": "Паказаць усе катэгорыі",
"notifications.column_settings.filter_bar.category": "Панэль хуткай фільтрацыі",
"notifications.column_settings.follow": "Новыя падпісчыкі:",
"notifications.column_settings.follow_request": "Новыя запыты на падпіску:",
"notifications.column_settings.mention": "Згадванні:",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Изберете съществуваща категория или създайте нова",
"filter_modal.select_filter.title": "Филтриране на публ.",
"filter_modal.title.status": "Филтриране на публ.",
"filtered_notifications_banner.mentions": "{count, plural, one {споменаване} other {споменавания}}",
"filtered_notifications_banner.pending_requests": "Известията от {count, plural, =0 {никого, когото може да познавате} one {едно лице, което може да познавате} other {# души, които може да познавате}}",
"filtered_notifications_banner.title": "Филтрирани известия",
"firehose.all": "Всичко",
@ -471,7 +472,11 @@
"notification.own_poll": "Анкетата ви приключи",
"notification.poll": "Анкета, в която гласувахте, приключи",
"notification.reblog": "{name} подсили ваша публикация",
"notification.severed_relationships": "Връзката с {name} е прекъсната",
"notification.relationships_severance_event": "Изгуби се връзката с {name}",
"notification.relationships_severance_event.account_suspension": "Администратор от {from} спря {target}, което значи че повече не може да получавате новости от тях или да взаимодействате с тях.",
"notification.relationships_severance_event.domain_block": "Администратор от {from} блокира {target}, вкючващо {followersCount} от последователите ви и {followingCount, plural, one {# акаунт, който} other {# акаунта, които}} следвате.",
"notification.relationships_severance_event.learn_more": "Научете повече",
"notification.relationships_severance_event.user_domain_block": "Блокирахте {target}, премахвайки {followersCount} от последователите си и {followingCount, plural, one {# акаунт, който} other {# акаунта, които}} следвате.",
"notification.status": "{name} току-що публикува",
"notification.update": "{name} промени публикация",
"notification_requests.accept": "Приемам",
@ -484,6 +489,8 @@
"notifications.column_settings.admin.sign_up": "Нови регистрации:",
"notifications.column_settings.alert": "Известия на работния плот",
"notifications.column_settings.favourite": "Любими:",
"notifications.column_settings.filter_bar.advanced": "Показване на всички категории",
"notifications.column_settings.filter_bar.category": "Лента за бърз филтър",
"notifications.column_settings.follow": "Нови последователи:",
"notifications.column_settings.follow_request": "Нови заявки за последване:",
"notifications.column_settings.mention": "Споменавания:",
@ -588,12 +595,6 @@
"refresh": "Опресняване",
"regeneration_indicator.label": "Зареждане…",
"regeneration_indicator.sublabel": "Подготовка на началния ви инфоканал!",
"relationship_severance_notification.purged_data": "прочистено от администраторите",
"relationship_severance_notification.relationships": "{count, plural, one {# връзка} other {# връзки}}",
"relationship_severance_notification.types.account_suspension": "Акаунтът е спрян",
"relationship_severance_notification.types.domain_block": "Домейнът е спрян",
"relationship_severance_notification.types.user_domain_block": "Блокирахте този домейн",
"relationship_severance_notification.view": "Преглед",
"relative_time.days": "{number} д.",
"relative_time.full.days": "преди {number, plural, one {# ден} other {# дни}}",
"relative_time.full.hours": "преди {number, plural, one {# час} other {# часа}}",

View file

@ -256,6 +256,7 @@
"filter_modal.select_filter.subtitle": "Implijout ur rummad a zo anezhañ pe krouiñ unan nevez",
"filter_modal.select_filter.title": "Silañ an toud-mañ",
"filter_modal.title.status": "Silañ un toud",
"filtered_notifications_banner.mentions": "{count, plural, one {meneg} two {veneg} few {meneg} other {a venegoù}}",
"firehose.all": "Pep tra",
"firehose.local": "Ar servijer-mañ",
"firehose.remote": "Servijerioù all",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Usa una categoria existent o crea'n una de nova",
"filter_modal.select_filter.title": "Filtra aquest tut",
"filter_modal.title.status": "Filtra un tut",
"filtered_notifications_banner.mentions": "{count, plural, one {menció} other {mencions}}",
"filtered_notifications_banner.pending_requests": "Notificacions {count, plural, =0 {de ningú} one {d'una persona} other {de # persones}} que potser coneixes",
"filtered_notifications_banner.title": "Notificacions filtrades",
"firehose.all": "Tots",
@ -307,6 +308,8 @@
"follow_requests.unlocked_explanation": "Tot i que el teu compte no està blocat, el personal de {domain} ha pensat que és possible que vulguis revisar manualment les sol·licituds de seguiment daquests comptes.",
"follow_suggestions.curated_suggestion": "Tria de l'equip",
"follow_suggestions.dismiss": "No ho tornis a mostrar",
"follow_suggestions.featured_longer": "Triat personalment per l'equip de {domain}",
"follow_suggestions.friends_of_friends_longer": "Popular entre la gent que segueixes",
"follow_suggestions.hints.featured": "L'equip de {domain} ha seleccionat aquest perfil.",
"follow_suggestions.hints.friends_of_friends": "Aquest perfil és popular entre la gent que seguiu.",
"follow_suggestions.hints.most_followed": "Aquest perfil és un dels més seguits a {domain}.",
@ -314,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "Aquest perfil és similar a d'altres que heu seguit recentment.",
"follow_suggestions.personalized_suggestion": "Suggeriment personalitzat",
"follow_suggestions.popular_suggestion": "Suggeriment popular",
"follow_suggestions.popular_suggestion_longer": "Popular a {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Semblant a perfils que has seguit fa poc",
"follow_suggestions.view_all": "Mostra-ho tot",
"follow_suggestions.who_to_follow": "A qui seguir",
"followed_tags": "Etiquetes seguides",
@ -471,7 +476,11 @@
"notification.own_poll": "La teva enquesta ha finalitzat",
"notification.poll": "Ha finalitzat una enquesta en què has votat",
"notification.reblog": "{name} t'ha impulsat",
"notification.severed_relationships": "S'han eliminat les relacions amb {name}",
"notification.relationships_severance_event": "S'han perdut les connexions amb {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspès {target}; això vol dir que ja no en podreu rebre actualitzacions o interactuar-hi.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} ha blocat {target}, incloent-hi {followersCount} dels vostres seguidors i {followingCount, plural, one {# compte} other {# comptes}} que seguiu.",
"notification.relationships_severance_event.learn_more": "Per a saber-ne més",
"notification.relationships_severance_event.user_domain_block": "Heu blocat {target}, eliminant {followersCount} dels vostres seguidors i {followingCount, plural, one {# compte} other {# comptes}} que seguiu.",
"notification.status": "{name} acaba de publicar",
"notification.update": "{name} ha editat un tut",
"notification_requests.accept": "Accepta",
@ -484,6 +493,8 @@
"notifications.column_settings.admin.sign_up": "Registres nous:",
"notifications.column_settings.alert": "Notificacions d'escriptori",
"notifications.column_settings.favourite": "Favorits:",
"notifications.column_settings.filter_bar.advanced": "Mostra totes les categories",
"notifications.column_settings.filter_bar.category": "Barra ràpida de filtres",
"notifications.column_settings.follow": "Nous seguidors:",
"notifications.column_settings.follow_request": "Noves sol·licituds de seguiment:",
"notifications.column_settings.mention": "Mencions:",
@ -588,12 +599,6 @@
"refresh": "Actualitza",
"regeneration_indicator.label": "Es carrega…",
"regeneration_indicator.sublabel": "Es prepara la teva línia de temps d'Inici!",
"relationship_severance_notification.purged_data": "purgat pels administradors",
"relationship_severance_notification.relationships": "{count, plural, one {# relació} other {# relacions}}",
"relationship_severance_notification.types.account_suspension": "S'ha suspès el compte",
"relationship_severance_notification.types.domain_block": "S'ha suspès el domini",
"relationship_severance_notification.types.user_domain_block": "Heu blocat aquest domini",
"relationship_severance_notification.view": "Visualitza",
"relative_time.days": "{number}d",
"relative_time.full.days": "fa {number, plural, one {# dia} other {# dies}}",
"relative_time.full.hours": "fa {number, plural, one {# hora} other {# hores}}",
@ -704,7 +709,7 @@
"status.edited_x_times": "Editat {count, plural, one {{count} vegada} other {{count} vegades}}",
"status.embed": "Incrusta",
"status.favourite": "Favorit",
"status.favourites": "{count, plural, one {# favorit} other {# favorits}}",
"status.favourites": "{count, plural, one {favorit} other {favorits}}",
"status.filter": "Filtra aquest tut",
"status.filtered": "Filtrada",
"status.hide": "Amaga el tut",
@ -725,7 +730,7 @@
"status.reblog": "Impulsa",
"status.reblog_private": "Impulsa amb la visibilitat original",
"status.reblogged_by": "impulsat per {name}",
"status.reblogs": "{count, plural, one {# impuls} other {# impulsos}}",
"status.reblogs": "{count, plural, one {impuls} other {impulsos}}",
"status.reblogs.empty": "Encara no ha impulsat ningú aquest tut. Quan algú ho faci, apareixerà aquí.",
"status.redraft": "Esborra i reescriu",
"status.remove_bookmark": "Elimina el marcador",

View file

@ -89,6 +89,14 @@
"announcement.announcement": "Oznámení",
"attachments_list.unprocessed": "(nezpracováno)",
"audio.hide": "Skrýt zvuk",
"block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou být stále viditelné pro nepřihlášené uživatele.",
"block_modal.show_less": "Zobrazit méně",
"block_modal.show_more": "Zobrazit více",
"block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.",
"block_modal.they_cant_see_posts": "Nemůže vidět vaše příspěvky a vy neuvidíte jeho.",
"block_modal.they_will_know": "Může vidět, že je zablokovaný.",
"block_modal.title": "Zablokovat uživatele?",
"block_modal.you_wont_see_mentions": "Neuvidíte příspěvky, které ho zmiňují.",
"boost_modal.combo": "Příště můžete pro přeskočení stisknout {combo}",
"bundle_column_error.copy_stacktrace": "Zkopírovat zprávu o chybě",
"bundle_column_error.error.body": "Požadovanou stránku nelze vykreslit. Může to být způsobeno chybou v našem kódu nebo problémem s kompatibilitou prohlížeče.",
@ -169,6 +177,7 @@
"confirmations.delete_list.message": "Opravdu chcete tento seznam navždy smazat?",
"confirmations.discard_edit_media.confirm": "Zahodit",
"confirmations.discard_edit_media.message": "Máte neuložené změny popisku médií nebo náhledu, chcete je přesto zahodit?",
"confirmations.domain_block.confirm": "Blokovat server",
"confirmations.domain_block.message": "Opravdu chcete blokovat celou doménu {domain}? Ve většině případů stačí blokovat nebo skrýt pár konkrétních uživatelů, což také doporučujeme. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.",
"confirmations.edit.confirm": "Upravit",
"confirmations.edit.message": "Editovat teď znamená přepsání zprávy, kterou právě tvoříte. Opravdu chcete pokračovat?",
@ -200,6 +209,27 @@
"dismissable_banner.explore_statuses": "Toto jsou příspěvky ze sociálních sítí, které dnes získávají na popularitě. Novější příspěvky s větším počtem boostů a oblíbení jsou hodnoceny výše.",
"dismissable_banner.explore_tags": "Tyto hashtagy právě teď získávají na popularitě mezi lidmi na tomto a dalších serverech decentralizované sítě.",
"dismissable_banner.public_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí na sociální síti, které sledují lidé na {domain}.",
"domain_block_modal.block": "Blokovat server",
"domain_block_modal.block_account_instead": "Raději blokovat @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Lidé z tohoto serveru mohou interagovat s vašimi starými příspěvky.",
"domain_block_modal.they_cant_follow": "Nikdo z tohoto serveru vás nemůže sledovat.",
"domain_block_modal.they_wont_know": "Nebude vědět, že je zablokován.",
"domain_block_modal.title": "Blokovat doménu?",
"domain_block_modal.you_will_lose_followers": "Všichni vaši sledující z tohoto serveru budou odstraněni.",
"domain_block_modal.you_wont_see_posts": "Neuvidíte příspěvky ani upozornění od uživatelů z tohoto serveru.",
"domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i s dalšími sociálními aplikacemi.",
"domain_pill.activitypub_like_language": "ActivityPub je jako jazyk, kterým Mastodon mluví s jinými sociálními sítěmi.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Handle:",
"domain_pill.their_server": "Digitální domov, kde žijí všechny příspěvky.",
"domain_pill.their_username": "Jedinečný identikátor na serveru. Je možné najít uživatele se stejným uživatelským jménem na různých serverech.",
"domain_pill.username": "Uživatelské jméno",
"domain_pill.whats_in_a_handle": "Co obsahuje handle?",
"domain_pill.who_they_are": "Protože handle říkají kdo je kdo a také kde, je možné interagovat s lidmi napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
"domain_pill.who_you_are": "Protože handle říká kdo jsi a kde jsi, mohou s tebou lidé komunikovat napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
"domain_pill.your_handle": "Tvůj handle:",
"domain_pill.your_server": "Tvůj digitální domov, kde žijí všechny tvé příspěvky. Nelíbí se ti? Kdykoliv se přesuň na jiný server a vezmi si sebou i své sledující.",
"domain_pill.your_username": "Tvůj jedinečný identifikátor na tomto serveru. Je možné najít uživatele se stejným uživatelským jménem na jiných serverech.",
"embed.instructions": "Pro přidání příspěvku na vaši webovou stránku zkopírujte níže uvedený kód.",
"embed.preview": "Takhle to bude vypadat:",
"emoji_button.activity": "Aktivita",
@ -236,6 +266,7 @@
"empty_column.list": "V tomto seznamu zatím nic není. Až nějaký člen z tohoto seznamu zveřejní nový příspěvek, objeví se zde.",
"empty_column.lists": "Zatím nemáte žádné seznamy. Až nějaký vytvoříte, zobrazí se zde.",
"empty_column.mutes": "Zatím jste neskryli žádného uživatele.",
"empty_column.notification_requests": "Vyčištěno! Nic tu není. Jakmile obdržíš nové notifikace, objeví se zde podle tvého nastavení.",
"empty_column.notifications": "Zatím nemáte žádná oznámení. Až s vámi někdo bude interagovat, uvidíte to zde.",
"empty_column.public": "Tady nic není! Napište něco veřejně, nebo začněte ručně sledovat uživatele z jiných serverů, aby tu něco přibylo",
"error.unexpected_crash.explanation": "Kvůli chybě v našem kódu nebo problému s kompatibilitou prohlížeče nemohla být tato stránka správně zobrazena.",
@ -266,6 +297,9 @@
"filter_modal.select_filter.subtitle": "Použít existující kategorii nebo vytvořit novou kategorii",
"filter_modal.select_filter.title": "Filtrovat tento příspěvek",
"filter_modal.title.status": "Filtrovat příspěvek",
"filtered_notifications_banner.mentions": "{count, plural, one {zmínka} few {zmínky} many {zmínek} other {zmínek}}",
"filtered_notifications_banner.pending_requests": "Oznámení od {count, plural, =0 {nikoho} one {jednoho člověka, kterého znáte} few {# lidí, které znáte} many {# lidí, které znáte} other {# lidí, které znáte}}",
"filtered_notifications_banner.title": "Filtrovaná oznámení",
"firehose.all": "Vše",
"firehose.local": "Tento server",
"firehose.remote": "Ostatní servery",
@ -394,6 +428,15 @@
"loading_indicator.label": "Načítání…",
"media_gallery.toggle_visible": "{number, plural, one {Skrýt obrázek} few {Skrýt obrázky} many {Skrýt obrázky} other {Skrýt obrázky}}",
"moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
"mute_modal.hide_from_notifications": "Skrýt z notifikací",
"mute_modal.hide_options": "Skrýt možnosti",
"mute_modal.indefinite": "Dokud je neodkryju",
"mute_modal.show_options": "Zobrazit možnosti",
"mute_modal.they_can_mention_and_follow": "Mohou vás zmínit a sledovat, ale neuvidíte je.",
"mute_modal.they_wont_know": "Nebudou vědět, že byli skryti.",
"mute_modal.title": "Ztlumit uživatele?",
"mute_modal.you_wont_see_mentions": "Neuvidíte příspěvky, které je zmiňují.",
"mute_modal.you_wont_see_posts": "Stále budou moci vidět vaše příspěvky, ale vy jejich neuvidíte.",
"navigation_bar.about": "O aplikaci",
"navigation_bar.advanced_interface": "Otevřít pokročilé webové rozhraní",
"navigation_bar.blocks": "Blokovaní uživatelé",
@ -429,14 +472,25 @@
"notification.own_poll": "Vaše anketa skončila",
"notification.poll": "Anketa, ve které jste hlasovali, skončila",
"notification.reblog": "Uživatel {name} boostnul váš příspěvek",
"notification.relationships_severance_event": "Kontakt ztracen s {name}",
"notification.relationships_severance_event.account_suspension": "Administrátor z {from} pozastavil {target}, což znamená, že již od nich nemůžete přijímat aktualizace nebo s nimi interagovat.",
"notification.relationships_severance_event.domain_block": "Administrátor z {from} pozastavil {target}, včetně {followersCount} z vašich sledujících a {followingCount, plural, one {# účet, který sledujete} few {# účty, které sledujete} many {# účtů, které sledujete} other {# účtů, které sledujete}}.",
"notification.relationships_severance_event.learn_more": "Zjistit více",
"notification.relationships_severance_event.user_domain_block": "Zablokovali jste {target}, čímž jste odebrali {followersCount} z vašich sledujících a {followingCount, plural, one {# účet, který sledujete} few {# účty, které sledujete} many {# účtů, které sledujete} other {# účtů, které sledujete}}.",
"notification.status": "Uživatel {name} právě přidal příspěvek",
"notification.update": "Uživatel {name} upravil příspěvek",
"notification_requests.accept": "Přijmout",
"notification_requests.dismiss": "Zamítnout",
"notification_requests.notifications_from": "Oznámení od {name}",
"notification_requests.title": "Vyfiltrovaná oznámení",
"notifications.clear": "Vyčistit oznámení",
"notifications.clear_confirmation": "Opravdu chcete trvale smazat všechna vaše oznámení?",
"notifications.column_settings.admin.report": "Nová hlášení:",
"notifications.column_settings.admin.sign_up": "Nové registrace:",
"notifications.column_settings.alert": "Oznámení na počítači",
"notifications.column_settings.favourite": "Oblíbené:",
"notifications.column_settings.filter_bar.advanced": "Zobrazit všechny kategorie",
"notifications.column_settings.filter_bar.category": "Panel rychlého filtrování",
"notifications.column_settings.follow": "Noví sledující:",
"notifications.column_settings.follow_request": "Nové žádosti o sledování:",
"notifications.column_settings.mention": "Zmínky:",
@ -462,6 +516,15 @@
"notifications.permission_denied": "Oznámení na ploše nejsou k dispozici, protože byla zamítnuta žádost o oprávnění je zobrazovat",
"notifications.permission_denied_alert": "Oznámení na ploše není možné zapnout, protože oprávnění bylo v minulosti zamítnuto",
"notifications.permission_required": "Oznámení na ploše nejsou k dispozici, protože nebylo uděleno potřebné oprávnění.",
"notifications.policy.filter_new_accounts.hint": "Vytvořeno během {days, plural, one {včerejška} few {posledních # dnů} many {posledních # dní} other {posledních # dní}}",
"notifications.policy.filter_new_accounts_title": "Nové účty",
"notifications.policy.filter_not_followers_hint": "Včetně lidí, kteří vás sledovali méně než {days, plural, one {jeden den} few {# dny} many {# dní} other {# dní}}",
"notifications.policy.filter_not_followers_title": "Lidé, kteří vás nesledují",
"notifications.policy.filter_not_following_hint": "Dokud je ručně neschválíte",
"notifications.policy.filter_not_following_title": "Lidé, které nesledujete",
"notifications.policy.filter_private_mentions_hint": "Vyfiltrováno, pokud to není odpověď na vaši zmínku nebo pokud sledujete odesílatele",
"notifications.policy.filter_private_mentions_title": "Nevyžádané soukromé zmínky",
"notifications.policy.title": "Vyfiltrovat oznámení od…",
"notifications_permission_banner.enable": "Povolit oznámení na ploše",
"notifications_permission_banner.how_to_control": "Chcete-li dostávat oznámení, i když nemáte Mastodon otevřený, povolte oznámení na ploše. Můžete si zvolit, o kterých druzích interakcí chcete být oznámením na ploše informování pod tlačítkem {icon} výše.",
"notifications_permission_banner.title": "Nenechte si nic uniknout",
@ -638,9 +701,11 @@
"status.direct": "Soukromě zmínit @{name}",
"status.direct_indicator": "Soukromá zmínka",
"status.edit": "Upravit",
"status.edited": "Naposledy upraveno {date}",
"status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}",
"status.embed": "Vložit na web",
"status.favourite": "Oblíbit",
"status.favourites": "{count, plural, one {oblíbený} few {oblíbené} many {oblíbených} other {oblíbených}}",
"status.filter": "Filtrovat tento příspěvek",
"status.filtered": "Filtrováno",
"status.hide": "Skrýt příspěvek",
@ -661,6 +726,7 @@
"status.reblog": "Boostnout",
"status.reblog_private": "Boostnout s původní viditelností",
"status.reblogged_by": "Uživatel {name} boostnul",
"status.reblogs": "{count, plural, one {boost} few {boosty} many {boostů} other {boostů}}",
"status.reblogs.empty": "Tento příspěvek ještě nikdo neboostnul. Pokud to někdo udělá, zobrazí se zde.",
"status.redraft": "Smazat a přepsat",
"status.remove_bookmark": "Odstranit ze záložek",

View file

@ -220,7 +220,7 @@
"domain_pill.activitypub_lets_connect": "Det muliggør at komme i forbindelse og interagere med folk ikke kun på Mastodon, men også på tværs af forskellige sociale apps.",
"domain_pill.activitypub_like_language": "ActivityPub er \"sproget\", Mastodon taler med andre sociale netværk.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Deres handle:",
"domain_pill.their_handle": "Vedkommendes handle:",
"domain_pill.username": "Brugernavn",
"domain_pill.whats_in_a_handle": "Hvad er der i et handle (@brugernavn)?",
"domain_pill.who_they_are": "Da et handle fortæller, hvem nogen er, og hvor de er, kan man interagere med folk på tværs af det sociale net af <button>ActivityPub-drevne platforme</button>.",
@ -295,6 +295,7 @@
"filter_modal.select_filter.subtitle": "Vælg en eksisterende kategori eller opret en ny",
"filter_modal.select_filter.title": "Filtrér dette indlæg",
"filter_modal.title.status": "Filtrér et indlæg",
"filtered_notifications_banner.mentions": "{count, plural, one {omtale} other {omtaler}}",
"filtered_notifications_banner.pending_requests": "Notifikationer fra {count, plural, =0 {ingen} one {én person} other {# personer}} du måske kender",
"filtered_notifications_banner.title": "Filtrerede notifikationer",
"firehose.all": "Alle",
@ -305,6 +306,8 @@
"follow_requests.unlocked_explanation": "Selvom din konto ikke er låst, synes {domain}-personalet, du måske bør gennemgå disse anmodninger manuelt.",
"follow_suggestions.curated_suggestion": "Personaleudvalgt",
"follow_suggestions.dismiss": "Vis ikke igen",
"follow_suggestions.featured_longer": "Håndplukket af {domain}-teamet",
"follow_suggestions.friends_of_friends_longer": "Populært blandt personer, som følges",
"follow_suggestions.hints.featured": "Denne profil er håndplukket af {domain}-teamet.",
"follow_suggestions.hints.friends_of_friends": "Denne profil er populær blandt de personer, som følges.",
"follow_suggestions.hints.most_followed": "Denne profil er en af de mest fulgte på {domain}.",
@ -312,6 +315,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "Denne profil svarer til de profiler, som senest er blevet fulgt.",
"follow_suggestions.personalized_suggestion": "Personligt forslag",
"follow_suggestions.popular_suggestion": "Populært forslag",
"follow_suggestions.popular_suggestion_longer": "Populært på {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Svarende til profiler, som for nylig er fulgt",
"follow_suggestions.view_all": "Vis alle",
"follow_suggestions.who_to_follow": "Hvem, som skal følges",
"followed_tags": "Hashtag, som følges",
@ -466,9 +471,23 @@
"notification.follow": "{name} begyndte at følge dig",
"notification.follow_request": "{name} har anmodet om at følge dig",
"notification.mention": "{name} nævnte dig",
"notification.moderation-warning.learn_more": "Læs mere",
"notification.moderation_warning": "Du er tildelt en moderationsadvarsel",
"notification.moderation_warning.action_delete_statuses": "Nogle af dine indlæg er blevet fjernet.",
"notification.moderation_warning.action_disable": "Din konto er blevet deaktiveret.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Nogle af dine indlæg er blevet markeret som sensitive.",
"notification.moderation_warning.action_none": "Din konto er tildelt en moderationsadvarsel.",
"notification.moderation_warning.action_sensitive": "Dine indlæg markeres fra nu af som sensitive.",
"notification.moderation_warning.action_silence": "Din konto er blevet begrænset.",
"notification.moderation_warning.action_suspend": "Din konto er suspenderet.",
"notification.own_poll": "Din afstemning er afsluttet",
"notification.poll": "En afstemning, hvori du stemte, er slut",
"notification.reblog": "{name} boostede dit indlæg",
"notification.relationships_severance_event": "Mistede forbindelser med {name}",
"notification.relationships_severance_event.account_suspension": "En admin fra {from} har suspenderet {target}, hvofor opdateringer herfra eller interaktion hermed ikke længer er mulig.",
"notification.relationships_severance_event.domain_block": "En admin fra {from} har blokeret {target}, herunder {followersCount} tilhængere og {followingCount, plural, one {# konto, der} other {# konti, som}} følges.",
"notification.relationships_severance_event.learn_more": "Læs mere",
"notification.relationships_severance_event.user_domain_block": "{target} er blevet blokeret, og {followersCount} tilhængere samt {followingCount, plural, one {# konto, der} other {# konti, som}} følges, er hermed fjernet.",
"notification.status": "{name} har netop postet",
"notification.update": "{name} redigerede et indlæg",
"notification_requests.accept": "Acceptér",
@ -481,6 +500,8 @@
"notifications.column_settings.admin.sign_up": "Nye tilmeldinger:",
"notifications.column_settings.alert": "Computernotifikationer",
"notifications.column_settings.favourite": "Favoritter:",
"notifications.column_settings.filter_bar.advanced": "Vis alle kategorier",
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.follow_request": "Nye følgeanmodninger:",
"notifications.column_settings.mention": "Omtaler:",
@ -585,12 +606,6 @@
"refresh": "Genindlæs",
"regeneration_indicator.label": "Indlæser…",
"regeneration_indicator.sublabel": "Din hjemmetidslinje klargøres!",
"relationship_severance_notification.purged_data": "renset af administratorer",
"relationship_severance_notification.relationships": "{count, plural, one {# forhold} other {# forhold}}",
"relationship_severance_notification.types.account_suspension": "Konto er blevet suspenderet",
"relationship_severance_notification.types.domain_block": "Domæne er blevet suspenderet",
"relationship_severance_notification.types.user_domain_block": "Dette domæne blev blokeret",
"relationship_severance_notification.view": "Vis",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# dag} other {# dage}} siden",
"relative_time.full.hours": "{number, plural, one {# time} other {# timer}} siden",

View file

@ -85,7 +85,7 @@
"alert.rate_limited.message": "Bitte versuche es nach {retry_time, time, medium} erneut.",
"alert.rate_limited.title": "Anfragelimit überschritten",
"alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
"alert.unexpected.title": "Ups!",
"alert.unexpected.title": "Oha!",
"announcement.announcement": "Ankündigung",
"attachments_list.unprocessed": "(ausstehend)",
"audio.hide": "Audio ausblenden",
@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Einem vorhandenen Filter hinzufügen oder einen neuen erstellen",
"filter_modal.select_filter.title": "Diesen Beitrag filtern",
"filter_modal.title.status": "Beitrag per Filter ausblenden",
"filtered_notifications_banner.mentions": "{count, plural, one {Erwähnung} other {Erwähnungen}}",
"filtered_notifications_banner.pending_requests": "Benachrichtigungen von {count, plural, =0 {keinem Profil, das du möglicherweise kennst} one {einem Profil, das du möglicherweise kennst} other {# Profilen, die du möglicherweise kennst}}",
"filtered_notifications_banner.title": "Gefilterte Benachrichtigungen",
"firehose.all": "Alles",
@ -307,6 +308,8 @@
"follow_requests.unlocked_explanation": "Auch wenn dein Konto öffentlich bzw. nicht geschützt ist, haben die Moderator*innen von {domain} gedacht, dass du diesen Follower lieber manuell bestätigen solltest.",
"follow_suggestions.curated_suggestion": "Vom Server-Team empfohlen",
"follow_suggestions.dismiss": "Nicht mehr anzeigen",
"follow_suggestions.featured_longer": "Vom {domain}-Team ausgewählt",
"follow_suggestions.friends_of_friends_longer": "Beliebt bei Leuten, denen du folgst",
"follow_suggestions.hints.featured": "Dieses Profil wurde vom {domain}-Team ausgewählt.",
"follow_suggestions.hints.friends_of_friends": "Dieses Profil ist bei deinen Followern beliebt.",
"follow_suggestions.hints.most_followed": "Dieses Profil ist eines der am meisten gefolgten auf {domain}.",
@ -314,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "Dieses Profil ähnelt den Profilen, denen du in letzter Zeit gefolgt hast.",
"follow_suggestions.personalized_suggestion": "Persönliche Empfehlung",
"follow_suggestions.popular_suggestion": "Beliebte Empfehlung",
"follow_suggestions.popular_suggestion_longer": "Beliebt auf {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Ähnlich zu Profilen, denen du seit kurzem folgst",
"follow_suggestions.view_all": "Alle anzeigen",
"follow_suggestions.who_to_follow": "Empfohlene Profile",
"followed_tags": "Gefolgte Hashtags",
@ -468,10 +473,23 @@
"notification.follow": "{name} folgt dir",
"notification.follow_request": "{name} möchte dir folgen",
"notification.mention": "{name} erwähnte dich",
"notification.moderation-warning.learn_more": "Mehr erfahren",
"notification.moderation_warning": "Du wurdest von den Moderator*innen verwarnt",
"notification.moderation_warning.action_delete_statuses": "Einige deiner Beiträge sind entfernt worden.",
"notification.moderation_warning.action_disable": "Dein Konto wurde deaktiviert.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Einige deiner Beiträge wurden mit einer Inhaltswarnung versehen.",
"notification.moderation_warning.action_none": "Dein Konto ist von den Moderator*innen verwarnt worden.",
"notification.moderation_warning.action_sensitive": "Deine zukünftigen Beiträge werden mit einer Inhaltswarnung versehen.",
"notification.moderation_warning.action_silence": "Dein Konto wurde eingeschränkt.",
"notification.moderation_warning.action_suspend": "Dein Konto wurde gesperrt.",
"notification.own_poll": "Deine Umfrage ist beendet",
"notification.poll": "Eine Umfrage, an der du teilgenommen hast, ist beendet",
"notification.reblog": "{name} teilte deinen Beitrag",
"notification.severed_relationships": "Beziehungen zu {name} getrennt",
"notification.relationships_severance_event": "Verbindungen mit {name} verloren",
"notification.relationships_severance_event.account_suspension": "Ein Admin von {from} hat {target} gesperrt. Du wirst von diesem Profil keine Updates mehr erhalten und auch nicht mit ihm interagieren können.",
"notification.relationships_severance_event.domain_block": "Ein Admin von {from} hat {target} blockiert darunter {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst.",
"notification.relationships_severance_event.learn_more": "Mehr erfahren",
"notification.relationships_severance_event.user_domain_block": "Du hast {target} blockiert {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst, wurden entfernt.",
"notification.status": "{name} hat gerade etwas gepostet",
"notification.update": "{name} bearbeitete einen Beitrag",
"notification_requests.accept": "Akzeptieren",
@ -484,6 +502,8 @@
"notifications.column_settings.admin.sign_up": "Neue Registrierungen:",
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
"notifications.column_settings.favourite": "Favoriten:",
"notifications.column_settings.filter_bar.advanced": "Alle Filterkategorien anzeigen",
"notifications.column_settings.filter_bar.category": "Filterleiste",
"notifications.column_settings.follow": "Neue Follower:",
"notifications.column_settings.follow_request": "Neue Follower-Anfragen:",
"notifications.column_settings.mention": "Erwähnungen:",
@ -529,11 +549,11 @@
"onboarding.follows.empty": "Bedauerlicherweise können aktuell keine Ergebnisse angezeigt werden. Du kannst die Suche verwenden oder den Reiter „Entdecken“ auswählen, um neue Leute zum Folgen zu finden oder du versuchst es später erneut.",
"onboarding.follows.lead": "Deine Startseite ist der primäre Anlaufpunkt, um Mastodon zu erleben. Je mehr Profilen du folgst, umso aktiver und interessanter wird sie. Damit du direkt loslegen kannst, gibt es hier ein paar Vorschläge:",
"onboarding.follows.title": "Personalisiere deine Startseite",
"onboarding.profile.discoverable": "Mein Profil auffindbar machen",
"onboarding.profile.discoverable": "Mein Profil darf entdeckt werden",
"onboarding.profile.discoverable_hint": "Wenn du entdeckt werden möchtest, dann können deine Beiträge in Suchergebnissen und Trends erscheinen. Dein Profil kann ebenfalls anderen mit ähnlichen Interessen vorgeschlagen werden.",
"onboarding.profile.display_name": "Anzeigename",
"onboarding.profile.display_name_hint": "Dein richtiger Name oder dein Fantasiename …",
"onboarding.profile.lead": "Du kannst das später in den Einstellungen vervollständigen, wo noch mehr Anpassungsmöglichkeiten zur Verfügung stehen.",
"onboarding.profile.lead": "Du kannst dein Profil später in den Einstellungen vervollständigen. Dort stehen weitere Anpassungsmöglichkeiten zur Verfügung.",
"onboarding.profile.note": "Über mich",
"onboarding.profile.note_hint": "Du kannst andere @Profile erwähnen oder #Hashtags verwenden …",
"onboarding.profile.save_and_continue": "Speichern und fortfahren",
@ -549,16 +569,16 @@
"onboarding.start.title": "Du hast es geschafft!",
"onboarding.steps.follow_people.body": "Interessanten Profilen zu folgen ist das, was Mastodon ausmacht.",
"onboarding.steps.follow_people.title": "Personalisiere deine Startseite",
"onboarding.steps.publish_status.body": "Begrüße die Welt mit Text, Fotos, Videos oder Umfragen {emoji}",
"onboarding.steps.publish_status.body": "Begrüße die Welt mit Text, Fotos, Videos oder Umfragen. {emoji}",
"onboarding.steps.publish_status.title": "Erstelle deinen ersten Beitrag",
"onboarding.steps.setup_profile.body": "Mit einem vollständigen Profil interagieren andere eher mit dir.",
"onboarding.steps.setup_profile.title": "Personalisiere dein Profil",
"onboarding.steps.share_profile.body": "Lass deine Freund*innen wissen, wie sie dich auf Mastodon finden können",
"onboarding.steps.share_profile.body": "Lass deine Freund*innen wissen, wie sie dich auf Mastodon finden können.",
"onboarding.steps.share_profile.title": "Teile dein Mastodon-Profil",
"onboarding.tips.2fa": "<strong>Wusstest du schon?</strong> Du kannst die Sicherheit deines Kontos erhöhen, indem du die Zwei-Faktor-Authentisierung in deinen Kontoeinstellungen aktivierst. Dafür ist keine Telefonnummer notwendig und es funktioniert jede beliebige TOTP-App!",
"onboarding.tips.accounts_from_other_servers": "<strong>Wusstest du schon?</strong> Da Mastodon dezentralisiert ist, werden einige Profile, denen du begegnest, auf anderen Servern als deinem bereitgestellt. Und trotzdem kannst du uneingeschränkt mit ihnen interagieren! Der Servername befindet sich in der zweiten Hälfte ihres Profilnamens!",
"onboarding.tips.migration": "<strong>Wusstest du schon?</strong> Wenn du das Gefühl hast, dass {domain} in Zukunft nicht die richtige Serverwahl für dich ist, kannst du auf einen anderen Mastodon-Server umziehen, ohne deine Follower zu verlieren. Du kannst sogar deinen eigenen Server betreiben!",
"onboarding.tips.verification": "<strong>Wusstest du schon?</strong> Du kannst dein Konto verifizieren, indem du auf deiner Website auf dein Mastodon-Profil verlinkst und den Link deiner Website zu deinem Profil hinzufügst. Keine Gebühren oder Dokumente erforderlich!",
"onboarding.tips.verification": "<strong>Wusstest du schon?</strong> Du kannst dein Konto verifizieren, indem du auf deiner Website auf dein Mastodon-Profil verlinkst und den Link deiner Website zu deinem Profil hinzufügst. Völlig kostenlos und ohne Dokumente einsenden zu müssen!",
"password_confirmation.exceeds_maxlength": "Passwortbestätigung überschreitet die maximal erlaubte Zeichenanzahl",
"password_confirmation.mismatching": "Passwortbestätigung stimmt nicht überein",
"picture_in_picture.restore": "Zurücksetzen",
@ -588,12 +608,6 @@
"refresh": "Aktualisieren",
"regeneration_indicator.label": "Wird geladen …",
"regeneration_indicator.sublabel": "Deine Startseite wird gerade vorbereitet!",
"relationship_severance_notification.purged_data": "von Administrator*innen entfernt",
"relationship_severance_notification.relationships": "{count, plural, one {# Beziehung} other {# Beziehungen}}",
"relationship_severance_notification.types.account_suspension": "Konto wurde gesperrt",
"relationship_severance_notification.types.domain_block": "Domain wurde gesperrt",
"relationship_severance_notification.types.user_domain_block": "Du hast diese Domain blockiert",
"relationship_severance_notification.view": "Anzeigen",
"relative_time.days": "{number} T.",
"relative_time.full.days": "vor {number, plural, one {# Tag} other {# Tagen}}",
"relative_time.full.hours": "vor {number, plural, one {# Stunde} other {# Stunden}}",

View file

@ -89,6 +89,14 @@
"announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio",
"block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.",
"block_modal.show_less": "Show less",
"block_modal.show_more": "Show more",
"block_modal.they_cant_mention": "They can't mention or follow you.",
"block_modal.they_cant_see_posts": "They can't see your posts and you won't see theirs.",
"block_modal.they_will_know": "They can see that they're blocked.",
"block_modal.title": "Block user?",
"block_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.copy_stacktrace": "Copy error report",
"bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
@ -169,6 +177,7 @@
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Block server",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"confirmations.edit.confirm": "Edit",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
@ -200,6 +209,27 @@
"dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralised network right now.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
"domain_block_modal.block": "Block server",
"domain_block_modal.block_account_instead": "Block @{name} instead",
"domain_block_modal.they_can_interact_with_old_posts": "People from this server can interact with your old posts.",
"domain_block_modal.they_cant_follow": "Nobody from this server can follow you.",
"domain_block_modal.they_wont_know": "They won't know they've been blocked.",
"domain_block_modal.title": "Block domain?",
"domain_block_modal.you_will_lose_followers": "All your followers from this server will be removed.",
"domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.",
"domain_pill.activitypub_lets_connect": "It lets you connect and interact with people not just on Mastodon, but across different social apps too.",
"domain_pill.activitypub_like_language": "ActivityPub is like the language Mastodon speaks with other social networks.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Their handle:",
"domain_pill.their_server": "Their digital home, where all of their posts live.",
"domain_pill.their_username": "Their unique identifier on their server. Its possible to find users with the same username on different servers.",
"domain_pill.username": "Username",
"domain_pill.whats_in_a_handle": "What's in a handle?",
"domain_pill.who_they_are": "Since handles say who someone is and where they are, you can interact with people across the social web of <button>ActivityPub-powered platforms</button>.",
"domain_pill.who_you_are": "Because your handle says who you are and where you are, people can interact with you across the social web of <button>ActivityPub-powered platforms</button>.",
"domain_pill.your_handle": "Your handle:",
"domain_pill.your_server": "Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.",
"domain_pill.your_username": "Your unique identifier on this server. Its possible to find users with the same username on different servers.",
"embed.instructions": "Embed this post on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
@ -236,6 +266,7 @@
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.mutes": "You haven't muted any users yet.",
"empty_column.notification_requests": "All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.",
"empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
@ -266,13 +297,22 @@
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
"filter_modal.select_filter.title": "Filter this post",
"filter_modal.title.status": "Filter a post",
"filtered_notifications_banner.mentions": "{count, plural, one {mention} other {mentions}}",
"filtered_notifications_banner.pending_requests": "Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know",
"filtered_notifications_banner.title": "Filtered notifications",
"firehose.all": "All",
"firehose.local": "This server",
"firehose.remote": "Other servers",
"follow_request.authorize": "Authorise",
"follow_request.reject": "Reject",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"follow_suggestions.curated_suggestion": "Staff pick",
"follow_suggestions.dismiss": "Don't show again",
"follow_suggestions.hints.featured": "This profile has been hand-picked by the {domain} team.",
"follow_suggestions.hints.friends_of_friends": "This profile is popular among the people you follow.",
"follow_suggestions.hints.most_followed": "This profile is one of the most followed on {domain}.",
"follow_suggestions.hints.most_interactions": "This profile has been recently getting a lot of attention on {domain}.",
"follow_suggestions.hints.similar_to_recently_followed": "This profile is similar to the profiles you have most recently followed.",
"follow_suggestions.personalized_suggestion": "Personalised suggestion",
"follow_suggestions.popular_suggestion": "Popular suggestion",
"follow_suggestions.view_all": "View all",
@ -388,6 +428,15 @@
"loading_indicator.label": "Loading…",
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
"mute_modal.hide_from_notifications": "Hide from notifications",
"mute_modal.hide_options": "Hide options",
"mute_modal.indefinite": "Until I unmute them",
"mute_modal.show_options": "Show options",
"mute_modal.they_can_mention_and_follow": "They can mention and follow you, but you won't see them.",
"mute_modal.they_wont_know": "They won't know they've been muted.",
"mute_modal.title": "Mute user?",
"mute_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.",
"navigation_bar.about": "About",
"navigation_bar.advanced_interface": "Open in advanced web interface",
"navigation_bar.blocks": "Blocked users",
@ -423,14 +472,25 @@
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your status",
"notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
"notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.relationships_severance_event.learn_more": "Learn more",
"notification.relationships_severance_event.user_domain_block": "You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.status": "{name} just posted",
"notification.update": "{name} edited a post",
"notification_requests.accept": "Accept",
"notification_requests.dismiss": "Dismiss",
"notification_requests.notifications_from": "Notifications from {name}",
"notification_requests.title": "Filtered notifications",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.admin.report": "New reports:",
"notifications.column_settings.admin.sign_up": "New sign-ups:",
"notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.follow_request": "New follow requests:",
"notifications.column_settings.mention": "Mentions:",
@ -456,6 +516,15 @@
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications.policy.filter_new_accounts.hint": "Created within the past {days, plural, one {one day} other {# days}}",
"notifications.policy.filter_new_accounts_title": "New accounts",
"notifications.policy.filter_not_followers_hint": "Including people who have been following you fewer than {days, plural, one {one day} other {# days}}",
"notifications.policy.filter_not_followers_title": "People not following you",
"notifications.policy.filter_not_following_hint": "Until you manually approve them",
"notifications.policy.filter_not_following_title": "People you don't follow",
"notifications.policy.filter_private_mentions_hint": "Filtered unless it's in reply to your own mention or if you follow the sender",
"notifications.policy.filter_private_mentions_title": "Unsolicited private mentions",
"notifications.policy.title": "Filter out notifications from…",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
@ -632,9 +701,11 @@
"status.direct": "Privately mention @{name}",
"status.direct_indicator": "Private mention",
"status.edit": "Edit",
"status.edited": "Last edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Embed",
"status.favourite": "Favourite",
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
"status.filter": "Filter this post",
"status.filtered": "Filtered",
"status.hide": "Hide post",
@ -655,6 +726,7 @@
"status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility",
"status.reblogged_by": "{name} boosted",
"status.reblogs": "{count, plural, one {boost} other {boosts}}",
"status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft",
"status.remove_bookmark": "Remove bookmark",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
"filter_modal.select_filter.title": "Filter this post",
"filter_modal.title.status": "Filter a post",
"filtered_notifications_banner.mentions": "{count, plural, one {mention} other {mentions}}",
"filtered_notifications_banner.pending_requests": "Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know",
"filtered_notifications_banner.title": "Filtered notifications",
"firehose.all": "All",
@ -307,6 +308,8 @@
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"follow_suggestions.curated_suggestion": "Staff pick",
"follow_suggestions.dismiss": "Don't show again",
"follow_suggestions.featured_longer": "Hand-picked by the {domain} team",
"follow_suggestions.friends_of_friends_longer": "Popular among people you follow",
"follow_suggestions.hints.featured": "This profile has been hand-picked by the {domain} team.",
"follow_suggestions.hints.friends_of_friends": "This profile is popular among the people you follow.",
"follow_suggestions.hints.most_followed": "This profile is one of the most followed on {domain}.",
@ -314,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "This profile is similar to the profiles you have most recently followed.",
"follow_suggestions.personalized_suggestion": "Personalized suggestion",
"follow_suggestions.popular_suggestion": "Popular suggestion",
"follow_suggestions.popular_suggestion_longer": "Popular on {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Similar to profiles you recently followed",
"follow_suggestions.view_all": "View all",
"follow_suggestions.who_to_follow": "Who to follow",
"followed_tags": "Followed hashtags",
@ -468,10 +473,23 @@
"notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",
"notification.mention": "{name} mentioned you",
"notification.moderation-warning.learn_more": "Learn more",
"notification.moderation_warning": "Your have received a moderation warning",
"notification.moderation_warning.action_delete_statuses": "Some of your posts have been removed.",
"notification.moderation_warning.action_disable": "Your account has been disabled.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Some of your posts have been marked as sensitive.",
"notification.moderation_warning.action_none": "Your account has received a moderation warning.",
"notification.moderation_warning.action_sensitive": "Your posts will be marked as sensitive from now on.",
"notification.moderation_warning.action_silence": "Your account has been limited.",
"notification.moderation_warning.action_suspend": "Your account has been suspended.",
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your post",
"notification.severed_relationships": "Relationships with {name} severed",
"notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
"notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.relationships_severance_event.learn_more": "Learn more",
"notification.relationships_severance_event.user_domain_block": "You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.status": "{name} just posted",
"notification.update": "{name} edited a post",
"notification_requests.accept": "Accept",
@ -590,12 +608,6 @@
"refresh": "Refresh",
"regeneration_indicator.label": "Loading…",
"regeneration_indicator.sublabel": "Your home feed is being prepared!",
"relationship_severance_notification.purged_data": "purged by administrators",
"relationship_severance_notification.relationships": "{count, plural, one {# relationship} other {# relationships}}",
"relationship_severance_notification.types.account_suspension": "Account has been suspended",
"relationship_severance_notification.types.domain_block": "Domain has been suspended",
"relationship_severance_notification.types.user_domain_block": "You blocked this domain",
"relationship_severance_notification.view": "View",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva",
"filter_modal.select_filter.title": "Filtrar este mensaje",
"filter_modal.title.status": "Filtrar un mensaje",
"filtered_notifications_banner.mentions": "{count, plural, one {mención} other {menciones}}",
"filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer",
"filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todos",
@ -427,7 +428,7 @@
"loading_indicator.label": "Cargando…",
"media_gallery.toggle_visible": "Ocultar {number, plural, one {imagen} other {imágenes}}",
"moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.",
"mute_modal.hide_from_notifications": "Ocultar de las notificaciones",
"mute_modal.hide_from_notifications": "Ocultar en las notificaciones",
"mute_modal.hide_options": "Ocultar opciones",
"mute_modal.indefinite": "Hasta que deje de silenciarlos",
"mute_modal.show_options": "Mostrar opciones",
@ -471,7 +472,11 @@
"notification.own_poll": "Tu encuesta finalizó",
"notification.poll": "Finalizó una encuesta en la que votaste",
"notification.reblog": "{name} adhirió a tu mensaje",
"notification.severed_relationships": "Relaciones con {name} cortadas",
"notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} suspendió a {target}, lo que significa que ya no podés recibir actualizaciones de esa cuenta o interactuar con la misma.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} bloqueó a {target}, incluyendo {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que seguís.",
"notification.relationships_severance_event.learn_more": "Aprendé más",
"notification.relationships_severance_event.user_domain_block": "Bloqueaste a {target}, eliminando {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que seguís.",
"notification.status": "{name} acaba de enviar un mensaje",
"notification.update": "{name} editó un mensaje",
"notification_requests.accept": "Aceptar",
@ -484,6 +489,8 @@
"notifications.column_settings.admin.sign_up": "Nuevos registros:",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:",
@ -588,12 +595,6 @@
"refresh": "Refrescar",
"regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Se está preparando tu línea temporal principal!",
"relationship_severance_notification.purged_data": "purgada por administradores",
"relationship_severance_notification.relationships": "{count, plural, one {# relación} other {# relaciones}}",
"relationship_severance_notification.types.account_suspension": "La cuenta fue suspendida",
"relationship_severance_notification.types.domain_block": "El dominio fue suspendido",
"relationship_severance_notification.types.user_domain_block": "Bloqueaste este dominio",
"relationship_severance_notification.view": "Ver",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural,one {hace # día} other {hace # días}}",
"relative_time.full.hours": "{number, plural,one {hace # hora} other {hace # horas}}",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva",
"filter_modal.select_filter.title": "Filtrar esta publicación",
"filter_modal.title.status": "Filtrar una publicación",
"filtered_notifications_banner.mentions": "{count, plural, one {mención} other {menciones}}",
"filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer",
"filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todas",
@ -471,6 +472,11 @@
"notification.own_poll": "Tu encuesta ha terminado",
"notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.reblog": "{name} ha retooteado tu estado",
"notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspendido {target}, lo que significa que ya no puedes recibir actualizaciones de sus cuentas o interactuar con ellas.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} ha bloqueado {target}, incluyendo {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
"notification.relationships_severance_event.learn_more": "Más información",
"notification.relationships_severance_event.user_domain_block": "Has bloqueado {target}, eliminando {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
"notification.status": "{name} acaba de publicar",
"notification.update": "{name} editó una publicación",
"notification_requests.accept": "Aceptar",
@ -483,6 +489,8 @@
"notifications.column_settings.admin.sign_up": "Registros nuevos:",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva",
"filter_modal.select_filter.title": "Filtrar esta publicación",
"filter_modal.title.status": "Filtrar una publicación",
"filtered_notifications_banner.mentions": "{count, plural, one {mención} other {menciones}}",
"filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer",
"filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todas",
@ -471,6 +472,11 @@
"notification.own_poll": "Tu encuesta ha terminado",
"notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.reblog": "{name} ha impulsado tu publicación",
"notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspendido {target}, lo que significa que ya no puedes recibir actualizaciones de sus cuentas o interactuar con ellas.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} ha bloqueado {target}, incluyendo {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
"notification.relationships_severance_event.learn_more": "Más información",
"notification.relationships_severance_event.user_domain_block": "Has bloqueado {target}, eliminando {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
"notification.status": "{name} acaba de publicar",
"notification.update": "{name} editó una publicación",
"notification_requests.accept": "Aceptar",
@ -483,6 +489,8 @@
"notifications.column_settings.admin.sign_up": "Nuevos registros:",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:",
@ -634,7 +642,7 @@
"report.statuses.subtitle": "Selecciona todos los que correspondan",
"report.statuses.title": "¿Hay alguna publicación que respalde este informe?",
"report.submit": "Enviar",
"report.target": "Reportando",
"report.target": "Reportando {target}",
"report.thanks.take_action": "Aquí están tus opciones para controlar lo que ves en Mastodon:",
"report.thanks.take_action_actionable": "Mientras revisamos esto, puedes tomar medidas contra @{name}:",
"report.thanks.title": "¿No quieres esto?",

View file

@ -92,7 +92,11 @@
"block_modal.remote_users_caveat": "Serverile {domain} edastatakse palve otsust järgida. Ometi pole see tagatud, kuna mõned serverid võivad blokeeringuid käsitleda omal moel. Avalikud postitused võivad tuvastamata kasutajatele endiselt näha olla.",
"block_modal.show_less": "Kuva vähem",
"block_modal.show_more": "Kuva rohkem",
"block_modal.they_cant_mention": "Ta ei saa mainida sind ega jälgida.",
"block_modal.they_cant_see_posts": "Ta ei näe sinu postitusi ja sa ei näe tema omi.",
"block_modal.they_will_know": "Ta näeb, et ta on blokeeritud.",
"block_modal.title": "Blokeeri kasutaja?",
"block_modal.you_wont_see_mentions": "Sa ei näe postitusi, mis mainivad teda.",
"boost_modal.combo": "Vajutades {combo}, saab selle edaspidi vahele jätta",
"bundle_column_error.copy_stacktrace": "Kopeeri veateade",
"bundle_column_error.error.body": "Soovitud lehte ei õnnestunud esitada. See võib olla meie koodiviga või probleem brauseri ühilduvusega.",
@ -206,8 +210,26 @@
"dismissable_banner.explore_tags": "Need sildid siit ja teistes serveritest detsentraliseeritud võrgus koguvad tähelepanu just praegu selles serveris.",
"dismissable_banner.public_timeline": "Need on kõige uuemad avalikud postitused inimestelt sotsiaalvõrgustikus, mida {domain} inimesed jälgivad.",
"domain_block_modal.block": "Blokeeri server",
"domain_block_modal.block_account_instead": "Selle asemel blokeeri @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Inimesed sellest serverist saavad interakteeruda sinu vanade postitustega.",
"domain_block_modal.they_cant_follow": "Sellest serverist ei saa keegi sind jälgida.",
"domain_block_modal.they_wont_know": "Nad ei tea, et nad on blokeeritud.",
"domain_block_modal.title": "Blokeerida domeen?",
"domain_block_modal.you_will_lose_followers": "Kõik sinu sellest serverist pärit jälgijad eemaldatakse.",
"domain_block_modal.you_wont_see_posts": "Sa ei näe selle serveri kasutajate postitusi ega teavitusi.",
"domain_pill.activitypub_lets_connect": "See võimaldab sul ühenduda inimestega ja nendega suhelda mitte ainult Mastodonis, vaid ka teistes suhtlusrakendustes.",
"domain_pill.activitypub_like_language": "ActivityPub on nagu keel, mida Mastodon räägib teiste suhtlusvõrgustikega.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Tema tunnus:",
"domain_pill.their_server": "Tema digitaalne kodu, kus kõik tema postitused on.",
"domain_pill.their_username": "Tema unikaalne tunnus tema serveris. On võimalik, et mingites teistes serverites on sama kasutajanimega kasutajaid.",
"domain_pill.username": "Kasutajanimi",
"domain_pill.whats_in_a_handle": "Mis on tunnuses?",
"domain_pill.who_they_are": "Kuna tunnus ütleb, kes keegi on ja kus, saad suhelda inimestega üle <button>ActivityPub-poolt toetatud sotsiaalvõrkude platvormide</button>.",
"domain_pill.who_you_are": "Kuna tunnus ütleb, kes sa oled ja kus, saavad inimesed sinuga suhelda üle <button>ActivityPub-poolt toetatud sotsiaalvõrkude platvormide</button>.",
"domain_pill.your_handle": "Sinu tunnus:",
"domain_pill.your_server": "Sinu digitaalne kodu, kus on kõik sinu postitused. Sulle ei meeldi see? Vaheta mistahes ajal serverit ja võta jälgijad ka.",
"domain_pill.your_username": "Sinu unikaalne identifikaator siin serveris. On võimalik, et leiad teistes serverites samasuguse kasutajanimega kasutajaid.",
"embed.instructions": "Lisa see postitus oma veebilehele, kopeerides alloleva koodi.",
"embed.preview": "Nii näeb see välja:",
"emoji_button.activity": "Tegevus",
@ -244,6 +266,7 @@
"empty_column.list": "Siin loetelus pole veel midagi. Kui loetelu liikmed teevad uusi postitusi, näed neid siin.",
"empty_column.lists": "Pole veel ühtegi nimekirja. Kui lood mõne, näed neid siin.",
"empty_column.mutes": "Sa pole veel ühtegi kasutajat vaigistanud.",
"empty_column.notification_requests": "Kõik tühi! Siin pole mitte midagi. Kui saad uusi teavitusi, ilmuvad need siin vastavalt sinu seadistustele.",
"empty_column.notifications": "Ei ole veel teateid. Kui keegi suhtleb sinuga, näed seda siin.",
"empty_column.public": "Siin pole midagi! Kirjuta midagi avalikku või jälgi ise kasutajaid täitmaks seda ruumi",
"error.unexpected_crash.explanation": "Meie poolse probleemi või veebilehitseja ühilduvusprobleemi tõttu ei suutnud me seda lehekülge korrektselt näidata.",
@ -275,6 +298,7 @@
"filter_modal.select_filter.title": "Filtreeri seda postitust",
"filter_modal.title.status": "Postituse filtreerimine",
"filtered_notifications_banner.pending_requests": "Teateid {count, plural, =0 {mitte üheltki} one {ühelt} other {#}} inimeselt, keda võid teada",
"filtered_notifications_banner.title": "Filtreeritud teavitused",
"firehose.all": "Kõik",
"firehose.local": "See server",
"firehose.remote": "Teised serverid",
@ -403,8 +427,15 @@
"loading_indicator.label": "Laadimine…",
"media_gallery.toggle_visible": "{number, plural, one {Varja pilt} other {Varja pildid}}",
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
"mute_modal.hide_from_notifications": "Peida teavituste hulgast",
"mute_modal.hide_options": "Peida valikud",
"mute_modal.indefinite": "Kuni eemaldan neilt vaigistuse",
"mute_modal.show_options": "Kuva valikud",
"mute_modal.they_can_mention_and_follow": "Ta saab sind mainida ja sind jälgida, kuid sa ei näe teda.",
"mute_modal.they_wont_know": "Ta ei tea, et ta on vaigistatud.",
"mute_modal.title": "Vaigistada kasutaja?",
"mute_modal.you_wont_see_mentions": "Sa ei näe postitusi, mis teda mainivad.",
"mute_modal.you_wont_see_posts": "Ta näeb jätkuvalt sinu postitusi, kuid sa ei näe tema omi.",
"navigation_bar.about": "Teave",
"navigation_bar.advanced_interface": "Ava kohandatud veebiliides",
"navigation_bar.blocks": "Blokeeritud kasutajad",
@ -440,16 +471,25 @@
"notification.own_poll": "Su küsitlus on lõppenud",
"notification.poll": "Küsitlus, milles osalesid, on lõppenud",
"notification.reblog": "{name} jagas edasi postitust",
"notification.relationships_severance_event": "Kadunud ühendus kasutajaga {name}",
"notification.relationships_severance_event.account_suspension": "{from} admin on kustutanud {target}, mis tähendab, et sa ei saa enam neilt uuendusi või suhelda nendega.",
"notification.relationships_severance_event.domain_block": "{from} admin on blokeerinud {target}, sealhulgas {followersCount} sinu jälgijat ja {followingCount, plural, one {# konto} other {# kontot}}, mida jälgid.",
"notification.relationships_severance_event.learn_more": "Saa rohkem teada",
"notification.relationships_severance_event.user_domain_block": "Blokeerisid {target}, eemaldades oma jälgijate hulgast {followersCount} ja jälgitavate hulgast {followingCount, plural, one {# konto} other {# kontot}}.",
"notification.status": "{name} just postitas",
"notification.update": "{name} muutis postitust",
"notification_requests.accept": "Nõus",
"notification_requests.dismiss": "Hülga",
"notification_requests.notifications_from": "Teavitus kasutajalt {name}",
"notification_requests.title": "Filtreeritud teavitused",
"notifications.clear": "Puhasta teated",
"notifications.clear_confirmation": "Oled kindel, et soovid püsivalt kõik oma teated eemaldada?",
"notifications.column_settings.admin.report": "Uued teavitused:",
"notifications.column_settings.admin.sign_up": "Uued kasutajad:",
"notifications.column_settings.alert": "Töölauateated",
"notifications.column_settings.favourite": "Lemmikud:",
"notifications.column_settings.filter_bar.advanced": "Näita kõiki kategooriaid",
"notifications.column_settings.filter_bar.category": "Kiirfiltri riba",
"notifications.column_settings.follow": "Uued jälgijad:",
"notifications.column_settings.follow_request": "Uued jälgimistaotlused:",
"notifications.column_settings.mention": "Mainimised:",
@ -475,7 +515,15 @@
"notifications.permission_denied": "Töölauamärguanded pole saadaval, kuna eelnevalt keelduti lehitsejale teavituste luba andmast",
"notifications.permission_denied_alert": "Töölaua märguandeid ei saa lubada, kuna brauseri luba on varem keeldutud",
"notifications.permission_required": "Töölaua märguanded ei ole saadaval, kuna vajalik luba pole antud.",
"notifications.policy.filter_new_accounts.hint": "Loodud viimase {days, plural, one {ühe päeva} other {# päeva}} jooksul",
"notifications.policy.filter_new_accounts_title": "Uued kontod",
"notifications.policy.filter_not_followers_hint": "Kaasates kasutajad, kes on sind jälginud vähem kui {days, plural, one {ühe päeva} other {# päeva}}",
"notifications.policy.filter_not_followers_title": "Sind mittejälgivad kasutajad",
"notifications.policy.filter_not_following_hint": "Kuni sa nad käsitsi kinnitad",
"notifications.policy.filter_not_following_title": "Inimesed, keda sa ei jälgi",
"notifications.policy.filter_private_mentions_hint": "Filtreeritud, kui see pole vastus sinupoolt mainimisele või kui jälgid saatjat",
"notifications.policy.filter_private_mentions_title": "Soovimatud privaatsed mainimised",
"notifications.policy.title": "Filtreeri välja teavitused kohast…",
"notifications_permission_banner.enable": "Luba töölaua märguanded",
"notifications_permission_banner.how_to_control": "Et saada teateid, ajal mil Mastodon pole avatud, luba töölauamärguanded. Saad täpselt määrata, mis tüüpi tegevused tekitavad märguandeid, kasutates peale teadaannete sisse lülitamist üleval olevat nuppu {icon}.",
"notifications_permission_banner.title": "Ära jää millestki ilma",
@ -652,9 +700,11 @@
"status.direct": "Maini privaatselt @{name}",
"status.direct_indicator": "Privaatne mainimine",
"status.edit": "Muuda",
"status.edited": "Viimati muudetud {date}",
"status.edited_x_times": "Muudetud {count, plural, one{{count} kord} other {{count} korda}}",
"status.embed": "Manustamine",
"status.favourite": "Lemmik",
"status.favourites": "{count, plural, one {lemmik} other {lemmikud}}",
"status.filter": "Filtreeri seda postitust",
"status.filtered": "Filtreeritud",
"status.hide": "Peida postitus",
@ -675,6 +725,7 @@
"status.reblog": "Jaga",
"status.reblog_private": "Jaga algse nähtavusega",
"status.reblogged_by": "{name} jagas",
"status.reblogs": "{count, plural, one {jagamine} other {jagamist}}",
"status.reblogs.empty": "Keegi pole seda postitust veel jaganud. Kui keegi seda teeb, näeb seda siin.",
"status.redraft": "Kustuta & alga uuesti",
"status.remove_bookmark": "Eemalda järjehoidja",

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