Compare commits

...

168 commits

Author SHA1 Message Date
Jeremy Kescher 2bcce397a9
Bump version 2023-05-27 15:06:05 +02:00
Jeremy Kescher c26771561d
Fix translations 2023-05-27 14:12:22 +02:00
Jeremy Kescher 919469bf9b
Move status_reactions.js to status_reactions.jsx 2023-05-27 13:55:31 +02:00
Jeremy Kescher 2beefd3185
Add frozen_string_literal to FixForeignKeysStatusReactions 2023-05-27 13:31:08 +02:00
Plastikmensch 9309a20117
Add missing name param.
Follow-up to 3a91f535fa
Missed these while porting changes.

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>
2023-05-27 13:27:01 +02:00
Jeremy Kescher 473620cdb1
Use named import for AnimatedNumber 2023-05-27 13:05:47 +02:00
Jeremy Kescher 2370d45a1e
Fix some RubyCop offenses 2023-05-27 12:58:46 +02:00
Jeremy Kescher 4920ccb302
Merge remote-tracking branch 'upstream/main' into develop 2023-05-27 12:23:36 +02:00
Jeremy Kescher 77ecf04abc
Apply https://github.com/CatCatNya/catstodon/pull/4 to dev branch 2023-05-27 12:19:29 +02:00
Claire 0222df6047
Merge pull request #2236 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to e387175fc9
2023-05-27 11:28:10 +02:00
Claire 1347ca6eb0 fixup! [Glitch] Upgrade to React 18 2023-05-26 18:44:18 +02:00
Claire 61f6cd45e3 Fix glitch-soc-only tests being broken because of test refactor 2023-05-26 00:10:57 +02:00
たいち ひ 60c7e559d8 [Glitch] Rewrite <TimelineHint /> as FC and TS
Port 9a472efe7c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 23:58:54 +02:00
たいち ひ 6746e5d430 [Glitch] Rewrite <Skeleton/> as FC and TS
Port 8066118d1f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 23:55:36 +02:00
Claire 892b3c16f5 [Glitch] Allow scripts in post embed previews
Port 711a037660 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 23:52:04 +02:00
Claire e2ab9d4dad Merge commit 'e387175fc9a3ebfd72ab45ebfe43ecfabef7b0c3' into glitch-soc/merge-upstream 2023-05-25 23:47:28 +02:00
Renaud Chaput 3b375ee395 [Glitch] Upgrade to React 18
Port 8d6aea3326 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 23:45:01 +02:00
Claire 45d7358100 Merge commit '8d6aea33260dedeacb3d22ac1a6d2f9cc3856a5e' into glitch-soc/merge-upstream 2023-05-25 23:18:57 +02:00
Claire 2e02d03524 Merge commit '4a22e72b9b1b8f14792efcc649b0db8bc27f0df2' into glitch-soc/merge-upstream 2023-05-25 22:59:30 +02:00
Claire ad1098970b Merge commit 'bec6a1cad4c509c53deb378c7ba984ba7e2de5a9' into glitch-soc/merge-upstream
Conflicts:
- `app/controllers/auth/confirmations_controller.rb`:
  Upstream merged our captcha code, but there are some
  conflicts due to glitch-soc's theming system.
- `app/views/admin/settings/registrations/show.html.haml`:
  Upstream merged our captcha code, but there are some
  conflicts due to glitch-soc's theming system.

Additional changes:
- `Gemfile`:
  Upstream added hcaptcha dependency in another place in the file.
- `config/settings.yml`:
  Upstream added the `captcha_enabled` setting in another place in the file.
2023-05-25 22:49:18 +02:00
Claire ba73f0ea3a [Glitch] Add polling and automatic redirection to /start on email confirmation
Port e60414792d to glitch-soc
2023-05-25 22:37:14 +02:00
Claire f959f6cdbb Merge commit 'e60414792d86a99c0f401f3c1bab92ee37835d39' into glitch-soc/merge-upstream 2023-05-25 22:18:55 +02:00
Claire b0ec3bfcf9 [Glitch] Fix being unable to load past a full page of filtered posts in Home timeline
Port 7b54e47d03 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 22:00:07 +02:00
Christian Schmidt 4a1f4cb6a2 [Glitch] Fix UI crash in moderation interface when opening the media modal
Port 5241f7b2fd to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 21:57:19 +02:00
Eugen Rochko d728222121 [Glitch] Change "Sign in" to "Login"
Port 3869e8c210 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 21:52:37 +02:00
Claire 602ae7f2f6 [Glitch] Fix videos being improperly positioned on safari
Port 0eed06073f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 21:47:48 +02:00
Renaud Chaput e6a7cfd12e [Glitch] Add stricter ESLint rules for Typescript files
Port 5eeb40bdbe to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 21:43:19 +02:00
Nick Schonning e8fc445e14 [Glitch] Enable ESLint react/no-deprecated
Port b878e3d8df to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 21:07:38 +02:00
Renaud Chaput 79c43b61a6 [Glitch] Disable RTK safety middlewares
Port 6f8db56a01 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 20:57:42 +02:00
fusagiko / takayamaki 7d9b7f28b8 [Glitch] Add type annotation for DisplayName component
Port 349cae0b57 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 20:57:07 +02:00
fusagiko / takayamaki ea1f9b4223 [Glitch] Rename Image component to ServerHeroImage
Port ab7716cff4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-25 20:44:09 +02:00
Claire b735954971 Merge commit '2ce0b666a139726dc406e6c1887728553b947e59' into glitch-soc/merge-upstream
Conflicts:
- `config/webpack/generateLocalePacks.js`:
  A dependency update changed how functions are imported.
  Also, some linting fixes not applicable to glitch-soc.
2023-05-25 20:43:25 +02:00
Claire 646cde71d1
Change captcha to be presented even for invited users (#2227) 2023-05-25 20:13:18 +02:00
Claire 69903b5830
Fix margins around small avatars (reply indicators, autocompletion suggestion) (#2231)
Fix regression from #2156
2023-05-25 19:40:33 +02:00
Claire cb6f445b90
Greatly simplify history management code (#2230)
Fixes #2220

This drops the ability to shift+click on “Back” to get back to a pinned
column, but that was inconsistent, broken, and undocumented.

This also brings us slightly closer to upstream.
2023-05-25 19:14:51 +02:00
Claire 2f2f74efd8
[Glitch] Update style of captcha confirmation page to match sign-up form (#2226) 2023-05-25 19:14:37 +02:00
Matt Jankowski e387175fc9
Fix RSpec/RepeatedExample cop (#24849) 2023-05-23 10:49:23 +02:00
Matt Jankowski 9f5deb310b
Fix Performance/MapCompact cop (#24797)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-05-23 10:49:12 +02:00
Matt Jankowski 0664704cd9
Fix Performance/StartWith cop (#24818) 2023-05-23 10:16:50 +02:00
たいち ひ 9a472efe7c
Rewrite <TimelineHint /> as FC and TS (#25091) 2023-05-23 10:04:10 +02:00
Matt Jankowski 2877c80dbc
Add specs for admin/announcements CRUD actions (#25077) 2023-05-23 10:03:51 +02:00
dependabot[bot] 753e6df04a
Bump rubocop from 1.50.2 to 1.51.0 (#24995)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:52:54 +02:00
dependabot[bot] 5b332112fc
Bump stylelint from 15.6.1 to 15.6.2 (#25078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:47:59 +02:00
dependabot[bot] c37ecbcd10
Bump aws-sdk-s3 from 1.121.0 to 1.122.0 (#24923)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:44:47 +02:00
dependabot[bot] c2cbe90a89
Bump rimraf from 5.0.0 to 5.0.1 (#25082)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:44:14 +02:00
dependabot[bot] 9e59186f78
Bump glob from 10.2.2 to 10.2.6 (#25083)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:34:31 +02:00
dependabot[bot] c7cbded282
Bump webpack-merge from 5.8.0 to 5.9.0 (#25084)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:27:55 +02:00
dependabot[bot] d7fd2c5763
Bump rqrcode from 2.1.2 to 2.2.0 (#25086)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:25:43 +02:00
dependabot[bot] 4ea24537cf
Bump rubocop-performance from 1.17.1 to 1.18.0 (#25089)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:25:32 +02:00
dependabot[bot] 7cfa6424f1
Bump @typescript-eslint/eslint-plugin from 5.59.6 to 5.59.7 (#25085)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:25:20 +02:00
dependabot[bot] 16d3e76a71
Bump @typescript-eslint/parser from 5.59.6 to 5.59.7 (#25080)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:15:43 +02:00
dependabot[bot] 9628d949ef
Bump connection_pool from 2.4.0 to 2.4.1 (#25088)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:07:01 +02:00
dependabot[bot] a5fa30a2d2
Bump rspec-rails from 6.0.1 to 6.0.2 (#25092)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 09:06:43 +02:00
Daniel M Brasil b473df9a14
Improve test coverage for /api/v1/featured_tags (#25076) 2023-05-23 09:01:11 +02:00
Nick Schonning 520e00a3c1
Don't run Rubocop excluded files for lint-staged (#25090) 2023-05-23 08:58:40 +02:00
たいち ひ 8066118d1f
Rewrite <Skeleton/> as FC and TS (#25055) 2023-05-23 08:58:08 +02:00
Claire 711a037660
Allow scripts in post embed previews (#25071) 2023-05-22 21:18:21 +02:00
Daniel M Brasil 785e650ab4
Fix uncaught TypeError in POST /api/v1/featured_tags (#25072)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-05-22 19:14:54 +02:00
Daniel M Brasil 45d98959ac
Fix uncaught NoMethodError in POST /api/v1/featured_tags (#25063) 2023-05-22 18:11:28 +02:00
Claire 2a61f14753
Fix account confirmation flow not returning to app after captcha validation (#25057) 2023-05-22 17:38:05 +02:00
Renaud Chaput 8d6aea3326
Upgrade to React 18 (#24916) 2023-05-22 15:48:01 +02:00
Daniel M Brasil 4a22e72b9b
Improve test coverage for /api/v1/admin/canonical_email_blocks (#24985) 2023-05-22 15:27:35 +02:00
Matt Jankowski 325d5f0183
Regenerate rubocop-todo (#24846) 2023-05-22 14:49:10 +02:00
Claire e13d2edd47
Fix “Authorized applications” inefficiently and incorrectly getting last use date (#25060) 2023-05-22 14:03:38 +02:00
Daniel M Brasil ce8b5899ae
Fix POST /api/v1/admin/domain_allows returning 200 when no domain is specified (#24958) 2023-05-22 13:44:49 +02:00
Matt Jankowski e328ab7e5a
Implement pending specs for StatusesController (#23969) 2023-05-22 13:43:05 +02:00
Daniel M Brasil f3feb4c891
Improve test coverage for /api/v1/admin/email_domain_blocks (#25017) 2023-05-22 13:28:11 +02:00
Nick Schonning c0b9664a31
Autofix Rubocop spacing in config (#25022) 2023-05-22 13:17:56 +02:00
Emelia Smith 19f9098551
Allow reports with long comments from remote instances, but truncate (#25028) 2023-05-22 13:15:21 +02:00
Daniel M Brasil d51464283c
Improve test coverage for /api/v1/admin/ip_blocks_controller (#25031) 2023-05-22 12:50:44 +02:00
Nick Schonning 23a4ecf444
Remove duplicate JPG type (#25054) 2023-05-22 12:46:20 +02:00
Nick Schonning 7d805cfa90
Remove requestAnimationFrame test polyfill (#25056) 2023-05-22 12:45:29 +02:00
Claire 7bb8030cc1
Change OpenGraph-based embeds to allow fullscreen (#25058) 2023-05-22 12:25:56 +02:00
Frankie Roberto 36a77748b4
Order sessions by most-recent to least-recently updated (#25005) 2023-05-22 11:40:00 +02:00
Nick Schonning c1e70a2072
Cleanup and document bundle test/dev deps (#24457)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-05-19 17:48:15 +02:00
Nick Schonning ed349b14e2
Add Postgres 15 testing for migrations (#23887) 2023-05-19 17:14:15 +02:00
Nick Schonning 99e2e9b81f
Fix minor typos in comments and spec names (#21831) 2023-05-19 17:13:29 +02:00
Claire b805b7f021
Add tests for avatar/header in backup service (#25037) 2023-05-19 12:04:18 +02:00
dependabot[bot] 27ec8f297d
Bump jsdom from 21.1.2 to 22.0.0 (#24828)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-19 11:28:56 +02:00
Essem 5fd8d1e417
Fix oversight in backup service (#25034) 2023-05-19 11:27:10 +02:00
Matt Jankowski d34d94d08f
Add spec for migration warning module (#25033) 2023-05-19 10:53:50 +02:00
Claire 058898802a
Fix AvatarComposite and DisplayName referencing undefined props (#2222) 2023-05-17 23:24:27 +02:00
Claire 45ba9ada34
Fix race condition when reblogging a status (#25016) 2023-05-17 00:09:21 +02:00
Claire 5cd55d8aaf
Fix being able to vote on your own polls (#25015) 2023-05-17 00:08:42 +02:00
Claire bec6a1cad4
Add hCaptcha support (#25019) 2023-05-16 23:27:35 +02:00
Claire e60414792d
Add polling and automatic redirection to /start on email confirmation (#25013) 2023-05-16 18:03:52 +02:00
dependabot[bot] 2ce0b666a1
Bump pg-connection-string from 2.5.0 to 2.6.0 (#24999)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 15:39:18 +02:00
Claire 7b54e47d03
Fix being unable to load past a full page of filtered posts in Home timeline (#24930) 2023-05-16 15:36:25 +02:00
Claire aa4c9730f6
Change composer highlight border size to be more noticeable (#25010) 2023-05-16 14:59:44 +02:00
Claire 3ed3d54bf3
Fix reports not being closed when performing batch suspensions (#24988) 2023-05-16 14:56:49 +02:00
dependabot[bot] 6ce4fd1cf2
Bump thor from 1.2.1 to 1.2.2 (#25001)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 10:58:36 +02:00
dependabot[bot] e95e896f10
Bump capybara from 3.39.0 to 3.39.1 (#25004)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 10:52:07 +02:00
Nick Schonning cee4369cf5
Autofix Rubocop Lint/AmbiguousOperatorPrecedence (#25002) 2023-05-16 10:51:59 +02:00
dependabot[bot] 520462799f
Bump @typescript-eslint/eslint-plugin from 5.59.5 to 5.59.6 (#24998)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 10:51:02 +02:00
dependabot[bot] bbe4800b04
Bump @typescript-eslint/parser from 5.59.5 to 5.59.6 (#25000)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 10:45:53 +02:00
dependabot[bot] 4d6a8ee34a
Bump eslint-plugin-jsdoc from 43.1.1 to 44.2.4 (#24994)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 10:10:02 +02:00
Steven Munn 52d36f0f98
Fix spelling of "Lets" on the onboarding page after clicking the confirmation email (#24959)
Co-authored-by: Steven Munn <stevenjmunn@gmail.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-05-15 22:42:07 +02:00
Matt Jankowski 604e1c2b11
Remove usage of random sample values in specs (#24869) 2023-05-15 20:20:13 +02:00
Matt Jankowski b84bc2de5d
Replace i18n view spec with helper spec (#24966) 2023-05-15 17:25:04 +02:00
Renaud Chaput 2e1c6e93ad
Bump mkdirp major version (#24978) 2023-05-15 09:40:24 +02:00
Claire 054df2d67e
Merge pull request #2216 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2023-05-14 15:39:47 +02:00
dependabot[bot] 713d217384
Bump eslint from 8.39.0 to 8.40.0 (#24919)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-14 15:09:33 +02:00
Matt Jankowski 679aca46da
I18n pluralization errors (#24963) 2023-05-12 18:53:30 +02:00
Daniel M Brasil 433ab0c9a3
Fix uncaught NoMethodError error in /api/v1/admin/canonical_email_blocks/test (#24947)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-05-12 13:46:16 +02:00
Claire 9015c2d646
Change profile updates to be sent to recently-mentioned servers (#24852) 2023-05-12 13:13:04 +02:00
Matt Jankowski 2c2d924942
Fix RSpec/RepeatedExampleGroupDescription cop (#24850)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-05-12 12:25:32 +02:00
Matt Jankowski bf3ebeb42f
Fix RSpec/SharedContext cop (#24847) 2023-05-12 09:25:43 +02:00
Christian Schmidt 5241f7b2fd
Fix UI crash in moderation interface when opening the media modal (#24816) 2023-05-11 12:41:55 +02:00
Matt Jankowski a610a02d4f
Fix RSpec/ScatteredSetup cop (#24848) 2023-05-11 10:32:09 +02:00
Daniel M Brasil 9cbda99941
Add test coverage for Mastodon::IpBlocksCLI (#24935) 2023-05-11 10:19:24 +02:00
Emelia Smith b8a2430642
Fix Onboarding Errors (#24883) 2023-05-11 07:55:10 +02:00
Claire 6b0942d107
Change AccessTokensVacuum to also delete expired tokens (#24868) 2023-05-11 04:40:03 +02:00
Eugen Rochko 3869e8c210
Change "Sign in" to "Login" (#24942) 2023-05-10 20:17:55 +02:00
Claire 0eed06073f
Fix videos being improperly positioned on safari (#24943) 2023-05-10 17:22:34 +02:00
Renaud Chaput 5eeb40bdbe
Add stricter ESLint rules for Typescript files (#24926) 2023-05-10 12:59:29 +02:00
Nick Schonning b878e3d8df
Enable ESLint react/no-deprecated (#24471) 2023-05-10 09:05:32 +02:00
たいち ひ 2d5e257938
Rewrite logo.tsx as FC (#24909) 2023-05-10 08:58:21 +02:00
Renaud Chaput 6f8db56a01
Disable RTK safety middlewares (#24936) 2023-05-10 08:38:02 +02:00
Claire f371464639 Remove our copy of image.d.ts as tsconfig already uses upstream's 2023-05-10 00:00:28 +02:00
Claire 96e99e2170 Run prettier on Typescript files
Port 51b83ed195 to glitch-soc
2023-05-09 23:41:18 +02:00
Claire facc7ab03c Merge commit '51b83ed19536b06ce3f57b260400ecec2d1dd187' into glitch-soc/merge-upstream 2023-05-09 23:37:38 +02:00
Renaud Chaput 5aa08826cf [Glitch] Type Redux store and middleware
Port 6aeb162927 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 23:28:40 +02:00
Renaud Chaput a56c71faba [Glitch] Remove unused iOS agent sniffing function
Port 224d458f7e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 23:20:57 +02:00
Renaud Chaput 526fe33e2f [Glitch] Rework polyfills loading
Port a3a2414f0e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 23:20:24 +02:00
fusagiko / takayamaki e22a88b512 [Glitch] Add more detailed type annotation for Account
Port 6579e3af7d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 23:16:23 +02:00
Claire 1a664560cc Merge commit '6aeb162927e6f9bbfd597632a10d82d9656c2385' into glitch-soc/merge-upstream
Conflicts:
- `.github/dependabot.yml`:
  We deleted it.
  Kept it removed.
- `app/javascript/packs/public.jsx`:
  Upstream changed an import, we have slightly different ones.
  Ported upstream changes.
2023-05-09 23:12:48 +02:00
fusagiko / takayamaki 349cae0b57
Add type annotation for DisplayName component (#24752) 2023-05-09 23:08:54 +02:00
fusagiko / takayamaki ab7716cff4
Rename Image component to ServerHeroImage (#24894) 2023-05-09 23:08:28 +02:00
Claire 0fff2b67de Disable broken onboarding code 2023-05-09 23:03:23 +02:00
Renaud Chaput 5f2071d055 [Glitch] Enforce stricter rules for Typescript files
Port c8181eb0a4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 22:56:26 +02:00
Renaud Chaput b509b96504 [Glitch] Enforce React Rules of Hooks with eslint
Port d9b93bd15e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 22:28:53 +02:00
Renaud Chaput 108720d7b0 [Glitch] Dont use CommonJS (require, module.exports) anywhere
Port 955179fc55 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 22:28:53 +02:00
Claire c81d1b0d38 Merge commit 'c8181eb0a41c4f5c1655d4e400cab071aee4182a' into glitch-soc/merge-upstream
Conflicts:
- `app/javascript/packs/admin.jsx`:
  Upstream reworked imports, but we had many changes.
  Reworked imports as upstream did.
- `app/javascript/packs/public.jsx`:
  Upstream reworked imports, but we had many changes.
  Reworked imports as upstream did.
2023-05-09 22:12:05 +02:00
たいち ひ 887112a065 [Glitch] Add TypeScript support for mastodon alias and image imports
Port 7c1305b3a4 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 21:50:48 +02:00
Claire 42bdc2add9 Copy hooks/useHovering.ts to glitch-soc 2023-05-09 21:30:10 +02:00
Claire 8e806b6e88 Fix IconButton prop types in glitch-soc 2023-05-09 21:11:23 +02:00
Renaud Chaput 6415981056 [Glitch] Mark wheel events on scrollable list as passive
Port 89269e4b71 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 20:42:07 +02:00
fusagiko / takayamaki f94eb65cf9 [Glitch] Rewrite Domain component as function component
Port 9818f34273 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 20:33:58 +02:00
fusagiko / takayamaki 468dfffd26 [Glitch] Use LayoutType from is_mobile in actions/app
Port 5bc8e2d1fd to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 20:32:30 +02:00
たいち ひ 87a704f70b [Glitch] Rewrite RadioButton component as FC
Port 76264e3fe8 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 20:25:31 +02:00
たいち ひ bd851d3b58 [Glitch] Rewrite Image component as function component
Port a65d2d1045 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 20:24:49 +02:00
たいち ひ 1edaf58fb9 [Glitch] Rewrite <NotSignedInIndicator /> as FC
Port 490ccbf40b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-09 20:24:01 +02:00
Claire 8b568755ad Merge commit '89269e4b713e3291a5c8c29b8d2e7b950b60eb35' into glitch-soc/merge-upstream
Conflicts:
- `tsconfig.json`:
  Upstream changed the config to properly process imports.
  Glitch-soc had previously already done so.
  Changed the config to better match upstream.
2023-05-09 20:12:33 +02:00
Nick Schonning 51b83ed195
Use Prettier for ESLint formatting TypeScript (#23631) 2023-05-09 19:02:12 +02:00
Renaud Chaput 6aeb162927
Type Redux store and middleware (#24843) 2023-05-09 16:56:26 +02:00
Claire e1f466fbbe
Fix javascript on moderation interface (#24933) 2023-05-09 16:42:02 +02:00
Renaud Chaput 224d458f7e
Remove unused iOS agent sniffing function (#24931) 2023-05-09 15:48:53 +02:00
dependabot[bot] 72dcb2ef58
Bump @typescript-eslint/eslint-plugin from 5.59.2 to 5.59.5 (#24922)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-09 15:42:21 +02:00
dependabot[bot] a5ba98d2df
Bump @typescript-eslint/parser from 5.59.2 to 5.59.5 (#24921)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-09 15:25:49 +02:00
eggplants af7bf59b3e
Fix wrong documentation link (#24924) 2023-05-09 14:56:02 +02:00
Renaud Chaput a3a2414f0e
Rework polyfills loading (#24907) 2023-05-09 14:55:35 +02:00
Daniel M Brasil 536dd046d4
Add ability to block sign-ups from IP using the CLI (#24870) 2023-05-09 14:46:00 +02:00
Daniel M Brasil ffb3fef7db
Fix uncaught ActiveRecord::StatementInvalid in Mastodon::IpBlocksCLI (#24861) 2023-05-09 14:45:47 +02:00
Renaud Chaput aec486b4ec
Upgrade uuid to 9.0.0 (#24917) 2023-05-09 14:43:07 +02:00
fusagiko / takayamaki 6579e3af7d
Add more detailed type annotation for Account (#24815) 2023-05-09 12:09:32 +02:00
Renaud Chaput c8181eb0a4
Enforce stricter rules for Typescript files (#24910) 2023-05-09 03:11:56 +02:00
Renaud Chaput 64ec41d89c
Make Webpack fail on failed imports (#24908) 2023-05-09 03:10:04 +02:00
Renaud Chaput d9b93bd15e
Enforce React Rules of Hooks with eslint (#24911) 2023-05-09 03:09:11 +02:00
Renaud Chaput 955179fc55
Dont use CommonJS (require, module.exports) anywhere (#24913) 2023-05-09 03:08:47 +02:00
Renaud Chaput 89269e4b71
Mark wheel events on scrollable list as passive (#24914) 2023-05-09 03:07:13 +02:00
fusagiko / takayamaki 9818f34273
Rewrite Domain component as function component (#24896) 2023-05-08 15:12:12 +02:00
fusagiko / takayamaki 5bc8e2d1fd
Use LayoutType from is_mobile in actions/app (#24863) 2023-05-08 15:10:21 +02:00
たいち ひ 7c1305b3a4
Add TypeScript support for mastodon alias and image imports (#24895) 2023-05-08 11:28:36 +02:00
たいち ひ 76264e3fe8
Rewrite RadioButton component as FC (#24897) 2023-05-08 11:12:53 +02:00
たいち ひ a65d2d1045
Rewrite Image component as function component (#24893) 2023-05-08 11:12:44 +02:00
たいち ひ 490ccbf40b
Rewrite <NotSignedInIndicator /> as FC (#24903) 2023-05-08 11:12:13 +02:00
たいち ひ 6fdbee240c
Rewrite <Check /> as FC (#24901) 2023-05-08 08:26:02 +02:00
fusagiko / takayamaki 140aa6b054
Rewrite VerifiedBadge component as function component (#24892) 2023-05-07 09:10:58 +02:00
581 changed files with 5901 additions and 3003 deletions

View file

@ -4,10 +4,12 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:promise/recommended',
'plugin:jsdoc/recommended',
'plugin:prettier/recommended',
],
env: {
@ -53,28 +55,14 @@ module.exports = {
'\\.(css|scss|json)$',
],
'import/resolver': {
node: {
paths: ['app/javascript'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
typescript: {},
},
},
rules: {
'brace-style': 'warn',
'comma-dangle': ['error', 'always-multiline'],
'comma-spacing': [
'warn',
{
before: false,
after: true,
},
],
'comma-style': ['warn', 'last'],
'consistent-return': 'error',
'dot-notation': 'error',
eqeqeq: ['error', 'always', { 'null': 'ignore' }],
indent: ['warn', 2],
'jsx-quotes': ['error', 'prefer-single'],
'no-case-declarations': 'off',
'no-catch-shadow': 'error',
@ -94,7 +82,6 @@ module.exports = {
{ property: 'substr', message: 'Use .slice instead of .substr.' },
],
'no-self-assign': 'off',
'no-trailing-spaces': 'warn',
'no-unused-expressions': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
@ -102,33 +89,18 @@ module.exports = {
{
vars: 'all',
args: 'after-used',
destructuredArrayIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'object-curly-spacing': ['error', 'always'],
'padded-blocks': [
'error',
{
classes: 'always',
},
],
quotes: ['error', 'single'],
semi: 'error',
'valid-typeof': 'error',
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }],
'react/jsx-boolean-value': 'error',
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
'react/jsx-curly-spacing': 'error',
'react/display-name': 'off',
'react/jsx-equals-spacing': 'error',
'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
'react/jsx-indent': ['error', 2],
'react/jsx-no-bind': 'error',
'react/jsx-no-target-blank': 'off',
'react/jsx-tag-spacing': 'error',
'react/jsx-wrap-multilines': 'error',
'react/no-deprecated': 'off',
'react/no-unknown-property': 'off',
'react/self-closing-comp': 'error',
@ -192,11 +164,14 @@ module.exports = {
{
js: 'never',
jsx: 'never',
mjs: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-anonymous-default-export': 'error',
'import/no-extraneous-dependencies': [
'error',
{
@ -208,6 +183,12 @@ module.exports = {
],
},
],
'import/no-amd': 'error',
'import/no-commonjs': 'error',
'import/no-import-module-exports': 'error',
'import/no-relative-packages': 'error',
'import/no-self-import': 'error',
'import/no-useless-path-segments': 'error',
'import/no-webpack-loader-syntax': 'error',
'promise/always-return': 'off',
@ -255,6 +236,7 @@ module.exports = {
'*.config.js',
'.*rc.js',
'ide-helper.js',
'config/webpack/**/*',
],
env: {
@ -264,6 +246,10 @@ module.exports = {
parserOptions: {
sourceType: 'script',
},
rules: {
'import/no-commonjs': 'off',
},
},
{
files: [
@ -274,18 +260,85 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:promise/recommended',
'plugin:jsdoc/recommended',
'plugin:prettier/recommended',
],
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import/order': [
'error',
{
alphabetize: { order: 'asc' },
'newlines-between': 'always',
groups: [
'builtin',
'external',
'internal',
'parent',
['index', 'sibling'],
'object',
],
pathGroups: [
// React core packages
{
pattern: '{react,react-dom,prop-types}',
group: 'builtin',
position: 'after',
},
// I18n
{
pattern: 'react-intl',
group: 'builtin',
position: 'after',
},
// Common React utilities
{
pattern: '{classnames,react-helmet}',
group: 'external',
position: 'before',
},
// Immutable / Redux / data store
{
pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}',
group: 'external',
position: 'before',
},
// Internal packages
{
pattern: '{mastodon/**,flavours/glitch-soc/**}',
group: 'internal',
position: 'after',
},
],
pathGroupsExcludedImportTypes: [],
},
],
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'jsdoc/require-jsdoc': 'off',
// Those rules set stricter rules for TS files
// to enforce better practices when converting from JS
'import/no-default-export': 'warn',
'react/prefer-stateless-function': 'warn',
'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }],
},
},
{
@ -298,5 +351,13 @@ module.exports = {
jest: true,
},
},
{
files: [
'streaming/**/*',
],
rules: {
'import/no-commonjs': 'off',
},
},
],
};

View file

@ -23,9 +23,17 @@ jobs:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
strategy:
fail-fast: false
matrix:
postgres:
- 14-alpine
- 15-alpine
services:
postgres:
image: postgres:14-alpine
image: postgres:${{ matrix.postgres}}
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres

View file

@ -23,9 +23,17 @@ jobs:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
strategy:
fail-fast: false
matrix:
postgres:
- 14-alpine
- 15-alpine
services:
postgres:
image: postgres:14-alpine
image: postgres:${{ matrix.postgres}}
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres

View file

@ -70,8 +70,6 @@ app/javascript/styles/mastodon/reset.scss
# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631
*.js
*.jsx
*.ts
*.tsx
# Ignore HTML till cleaned and included in CI
*.html

View file

@ -1,3 +1,4 @@
module.exports = {
singleQuote: true
singleQuote: true,
jsxSingleQuote: true
}

View file

@ -157,7 +157,7 @@ Metrics/MethodLength:
- 'lib/mastodon/*_cli.rb'
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
Metrics/ModuleLength:
CountAsOne: [array, heredoc]

View file

@ -21,12 +21,6 @@ Layout/ArgumentAlignment:
- 'config/initializers/cors.rb'
- 'config/initializers/session_store.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'config/initializers/omniauth.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# SupportedHashRocketStyles: key, separator, table
@ -39,12 +33,6 @@ Layout/HashAlignment:
- 'config/initializers/rack_attack.rb'
- 'config/routes.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Width, AllowedPatterns.
Layout/IndentationWidth:
Exclude:
- 'config/initializers/ffmpeg.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment.
Layout/LeadingCommentSpace:
@ -52,14 +40,6 @@ Layout/LeadingCommentSpace:
- 'config/application.rb'
- 'config/initializers/omniauth.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
# SupportedStyles: space, no_space
# SupportedStylesForEmptyBraces: space, no_space
Layout/SpaceBeforeBlockBraces:
Exclude:
- 'config/initializers/paperclip.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_no_space, require_space
@ -68,19 +48,6 @@ Layout/SpaceInLambdaLiteral:
- 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: space, no_space
Layout/SpaceInsideStringInterpolation:
Exclude:
- 'config/initializers/webauthn.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowInHeredoc.
Layout/TrailingWhitespace:
Exclude:
- 'config/initializers/paperclip.rb'
# Configuration parameters: AllowedMethods, AllowedPatterns.
Lint/AmbiguousBlockAssociation:
Exclude:
@ -94,11 +61,6 @@ Lint/AmbiguousBlockAssociation:
- 'spec/services/unsuspend_account_service_spec.rb'
- 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
Lint/AmbiguousOperatorPrecedence:
Exclude:
- 'config/initializers/rack_attack.rb'
# Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock:
Exclude:
@ -277,31 +239,6 @@ Naming/VariableNumber:
- 'spec/models/user_spec.rb'
- 'spec/services/activitypub/fetch_featured_collection_service_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/MapCompact:
Exclude:
- 'app/lib/admin/metrics/dimension.rb'
- 'app/lib/admin/metrics/measure.rb'
- 'app/lib/feed_manager.rb'
- 'app/models/account.rb'
- 'app/models/account_statuses_cleanup_policy.rb'
- 'app/models/account_suggestions/setting_source.rb'
- 'app/models/account_suggestions/source.rb'
- 'app/models/follow_recommendation_filter.rb'
- 'app/models/notification.rb'
- 'app/models/user_role.rb'
- 'app/models/webhook.rb'
- 'app/services/process_mentions_service.rb'
- 'app/validators/existing_username_validator.rb'
- 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
- 'spec/presenters/status_relationships_presenter_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeMultiline.
Performance/StartWith:
Exclude:
- 'app/lib/extractor.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/UnfreezeString:
Exclude:
@ -626,7 +563,6 @@ RSpec/NoExpectationExample:
RSpec/PendingWithoutReason:
Exclude:
- 'spec/controllers/statuses_controller_spec.rb'
- 'spec/models/account_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
@ -638,32 +574,6 @@ RSpec/PredicateMatcher:
- 'spec/models/user_spec.rb'
- 'spec/services/post_status_service_spec.rb'
RSpec/RepeatedExample:
Exclude:
- 'spec/policies/status_policy_spec.rb'
RSpec/RepeatedExampleGroupBody:
Exclude:
- 'spec/controllers/statuses_controller_spec.rb'
RSpec/RepeatedExampleGroupDescription:
Exclude:
- 'spec/controllers/admin/reports/actions_controller_spec.rb'
- 'spec/policies/report_note_policy_spec.rb'
RSpec/ScatteredSetup:
Exclude:
- 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb'
- 'spec/controllers/activitypub/outboxes_controller_spec.rb'
- 'spec/controllers/admin/disputes/appeals_controller_spec.rb'
- 'spec/controllers/auth/registrations_controller_spec.rb'
- 'spec/services/activitypub/process_account_service_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
RSpec/SharedContext:
Exclude:
- 'spec/services/unsuspend_account_service_spec.rb'
RSpec/StubbedMock:
Exclude:
- 'spec/controllers/api/base_controller_spec.rb'

View file

@ -55,7 +55,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV DEBIAN_FRONTEND="noninteractive" \
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use
# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use
# hadolint ignore=DL3008,DL3009
RUN apt-get update && \
echo "Etc/UTC" > /etc/localtime && \

92
Gemfile
View file

@ -17,7 +17,7 @@ gem 'makara', '~> 0.5'
gem 'pghero'
gem 'dotenv-rails', '~> 2.8'
gem 'aws-sdk-s3', '~> 1.120', require: false
gem 'aws-sdk-s3', '~> 1.122', require: false
gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b'
@ -75,7 +75,7 @@ gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-s
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.1'
gem 'rqrcode', '~> 2.2'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.7'
@ -99,54 +99,87 @@ gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2'
gem 'rdf-normalize', '~> 0.5'
group :development, :test do
gem 'fabrication', '~> 2.30'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 1.0', require: false
gem 'rspec-rails', '~> 6.0'
gem 'rspec_chunked', '~> 0.6'
gem 'rubocop-capybara', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'rubocop', require: false
end
group :production, :test do
gem 'private_address_check', '~> 0.5'
end
gem 'private_address_check', '~> 0.5'
group :test do
gem 'capybara', '~> 3.39'
gem 'climate_control'
gem 'faker', '~> 3.2'
gem 'json-schema', '~> 4.0'
gem 'rack-test', '~> 2.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec_junit_formatter', '~> 0.6'
# RSpec runner for rails
gem 'rspec-rails', '~> 6.0'
# Used to split testing into chunks in CI
gem 'rspec_chunked', '~> 0.6'
# RSpec progress bar formatter
gem 'fuubar', '~> 2.5'
# Extra RSpec extenion methods and helpers for sidekiq
gem 'rspec-sidekiq', '~> 3.1'
# Browser integration testing
gem 'capybara', '~> 3.39'
# Used to mock environment variables
gem 'climate_control', '~> 0.2'
# Generating fake data for specs
gem 'faker', '~> 3.2'
# Generate test objects for specs
gem 'fabrication', '~> 2.30'
# Add back helpers functions removed in Rails 5.1
gem 'rails-controller-testing', '~> 1.0'
# Validate schemas in specs
gem 'json-schema', '~> 4.0'
# Test harness fo rack components
gem 'rack-test', '~> 2.1'
# Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false
gem 'simplecov', '~> 0.22', require: false
# Stub web requests for specs
gem 'webmock', '~> 3.18'
end
group :development do
# Code linting CLI and plugins
gem 'rubocop', require: false
gem 'rubocop-capybara', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
# Annotates modules with schema
gem 'annotate', '~> 3.2'
# Enhanced error message pages for development
gem 'better_errors', '~> 2.9'
gem 'binding_of_caller', '~> 1.0'
# Preview mail in the browser
gem 'letter_opener', '~> 1.8'
gem 'letter_opener_web', '~> 2.0'
gem 'memory_profiler'
# Security analysis CLI tools
gem 'brakeman', '~> 5.4', require: false
gem 'bundler-audit', '~> 0.9', require: false
# Linter CLI for HAML files
gem 'haml_lint', require: false
# Deployment automation
gem 'capistrano', '~> 3.17'
gem 'capistrano-rails', '~> 1.6'
gem 'capistrano-rbenv', '~> 2.2'
gem 'capistrano-yarn', '~> 2.0'
gem 'stackprof'
# Validate missing i18n keys
gem 'i18n-tasks', '~> 1.0', require: false
# Profiling tools
gem 'memory_profiler', require: false
gem 'stackprof', require: false
end
group :production do
@ -157,8 +190,9 @@ gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
gem 'hcaptcha', '~> 7.1'
gem 'cocoon', '~> 1.2'
gem 'net-http', '~> 0.3.2'
gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1'

View file

@ -109,16 +109,16 @@ GEM
attr_required (1.0.1)
awrence (1.2.1)
aws-eventstream (1.2.0)
aws-partitions (1.752.0)
aws-sdk-core (3.171.0)
aws-partitions (1.761.0)
aws-sdk-core (3.172.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.63.0)
aws-sdk-kms (1.64.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.121.0)
aws-sdk-s3 (1.122.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@ -166,7 +166,7 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (3.39.0)
capybara (3.39.1)
addressable
matrix
mini_mime (>= 0.1.3)
@ -189,7 +189,7 @@ GEM
coderay (1.1.3)
color_diff (0.1)
concurrent-ruby (1.2.2)
connection_pool (2.4.0)
connection_pool (2.4.1)
cose (1.3.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -331,7 +331,7 @@ GEM
httplog (1.6.2)
rack (>= 2.0)
rainbow (>= 2.0.0)
i18n (1.12.0)
i18n (1.13.0)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.12)
activesupport (>= 4.0.2)
@ -398,9 +398,9 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.20.0)
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
@ -418,7 +418,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_mime (1.1.2)
mini_portile2 (2.8.1)
mini_portile2 (2.8.2)
minitest (5.18.0)
msgpack (1.7.0)
multi_json (1.15.0)
@ -576,7 +576,7 @@ GEM
rexml (3.2.5)
rotp (6.2.2)
rpam2 (4.0.2)
rqrcode (2.1.2)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
@ -588,22 +588,20 @@ GEM
rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.1)
rspec-rails (6.0.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.11)
rspec-expectations (~> 3.11)
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.12.0)
rspec_chunked (0.6)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.50.2)
rubocop (1.51.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
@ -613,11 +611,11 @@ GEM
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.0)
rubocop-ast (1.28.1)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-performance (1.17.1)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.19.1)
@ -698,7 +696,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
thor (1.2.1)
thor (1.2.2)
tilt (2.1.0)
timeout (0.3.2)
tpm-key_attestation (0.12.0)
@ -763,7 +761,7 @@ GEM
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.7)
zeitwerk (2.6.8)
PLATFORMS
ruby
@ -772,7 +770,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10)
addressable (~> 2.8)
annotate (~> 3.2)
aws-sdk-s3 (~> 1.120)
aws-sdk-s3 (~> 1.122)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
@ -787,7 +785,7 @@ DEPENDENCIES
capybara (~> 3.39)
charlock_holmes (~> 0.7.7)
chewy (~> 7.3)
climate_control
climate_control (~> 0.2)
cocoon (~> 1.2)
color_diff (~> 0.1)
concurrent-ruby
@ -862,11 +860,10 @@ DEPENDENCIES
redcarpet (~> 3.6)
redis (~> 4.5)
redis-namespace (~> 1.10)
rqrcode (~> 2.1)
rqrcode (~> 2.2)
rspec-rails (~> 6.0)
rspec-sidekiq (~> 3.1)
rspec_chunked (~> 0.6)
rspec_junit_formatter (~> 0.6)
rubocop
rubocop-capybara
rubocop-performance

View file

@ -58,7 +58,7 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
end
def set_canonical_email_blocks_from_test
@canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email])
@canonical_email_blocks = CanonicalEmailBlock.matching_email(params.require(:email))
end
def set_canonical_email_block

View file

@ -29,7 +29,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
def create
authorize :domain_allow, :create?
@domain_allow = DomainAllow.find_by(resource_params)
@domain_allow = DomainAllow.find_by(domain: resource_params[:domain])
if @domain_allow.nil?
@domain_allow = DomainAllow.create!(resource_params)

View file

@ -1,9 +1,10 @@
# frozen_string_literal: true
class Api::V1::Emails::ConfirmationsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user_owned_by_application!
before_action :require_user_not_confirmed!
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :check
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check
before_action :require_user_owned_by_application!, except: :check
before_action :require_user_not_confirmed!, except: :check
def create
current_user.update!(email: params[:email]) if params.key?(:email)
@ -12,6 +13,10 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController
render_empty
end
def check
render json: current_user.confirmed?
end
private
def require_user_owned_by_application!

View file

@ -13,7 +13,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController
end
def create
featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name])
featured_tag = CreateFeaturedTagService.new.call(current_account, params.require(:name))
render json: featured_tag, serializer: REST::FeaturedTagSerializer
end
@ -33,6 +33,6 @@ class Api::V1::FeaturedTagsController < Api::BaseController
end
def featured_tag_params
params.permit(:name)
params.require(:name)
end
end

View file

@ -2,6 +2,8 @@
class Api::V1::Statuses::ReblogsController < Api::BaseController
include Authorization
include Redisable
include Lockable
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user!
@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
override_rate_limit_headers :create, family: :statuses
def create
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
end
render json: @status, serializer: REST::StatusSerializer
end

View file

@ -57,9 +57,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
def captcha_user_bypass?
return true if @confirmation_user.nil? || @confirmation_user.confirmed?
invite = Invite.find(@confirmation_user.invite_id) if @confirmation_user.invite_id.present?
invite.present? && !invite.max_uses.nil?
end
def set_pack

View file

@ -132,7 +132,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def set_sessions
@sessions = current_user.session_activations
@sessions = current_user.session_activations.order(updated_at: :desc)
end
def set_strikes

View file

@ -45,6 +45,6 @@ class Auth::SetupController < ApplicationController
end
def set_pack
use_pack 'auth'
use_pack 'sign_up'
end
end

View file

@ -10,6 +10,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :set_body_classes
before_action :set_cache_headers
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
skip_before_action :require_functional!
include Localized
@ -40,4 +42,14 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
def set_last_used_at_by_app
@last_used_at_by_app = Doorkeeper::AccessToken
.select('DISTINCT ON (application_id) application_id, last_used_at')
.where(resource_owner_id: current_resource_owner.id)
.where.not(last_used_at: nil)
.order(application_id: :desc, last_used_at: :desc)
.pluck(:application_id, :last_used_at)
.to_h
end
end

View file

@ -1,3 +1,3 @@
require('../styles/mailer.scss');
import '../styles/mailer.scss';
require.context('../icons');

View file

@ -2,7 +2,7 @@
import 'packs/public-path';
const { delegate } = require('@rails/ujs');
import { delegate } from '@rails/ujs';
const getProfileAvatarAnimationHandler = (swapTo) => {
//animate avatar gifs on the profile page when moused over

View file

@ -3,7 +3,7 @@
import 'packs/public-path';
import escapeTextContentForBrowser from 'escape-html';
const { delegate } = require('@rails/ujs');
import { delegate } from '@rails/ujs';
import emojify from '../mastodon/features/emoji/emoji';

View file

@ -16,4 +16,5 @@ pack:
modal: public.js
public: public.js
settings: settings.js
sign_up:
share:

View file

@ -1,7 +1,9 @@
import { createAction } from '@reduxjs/toolkit';
type ChangeLayoutPayload = {
layout: 'mobile' | 'single-column' | 'multi-column';
};
import type { LayoutType } from '../is_mobile';
interface ChangeLayoutPayload {
layout: LayoutType;
}
export const changeLayout =
createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');

View file

@ -1,6 +1,6 @@
import api from '../api';
import { debounce } from 'lodash';
import compareId from '../compare_id';
import { compareId } from '../compare_id';
import { List as ImmutableList } from 'immutable';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';

View file

@ -13,7 +13,7 @@ import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from 'flavours/glitch/utils/html';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import compareId from 'flavours/glitch/compare_id';
import { compareId } from 'flavours/glitch/compare_id';
import { requestNotificationPermission } from 'flavours/glitch/utils/notifications';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';

View file

@ -1,12 +1,12 @@
import api from '../api';
import { importFetchedStatuses } from './importer';
import { me } from 'flavours/glitch/initial_state';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
import { me } from 'flavours/glitch/initial_state';
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest());

View file

@ -52,8 +52,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
/**
* @param {function(Function, Function): void} fallback
*/
const useFallback = fallback => {
fallback(dispatch, () => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
});
};

View file

@ -2,7 +2,7 @@ import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers';
import api, { getLinks } from 'flavours/glitch/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/compare_id';
import { compareId } from 'flavours/glitch/compare_id';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import { toServerSideType } from 'flavours/glitch/utils/filters';

View file

@ -98,9 +98,9 @@ export const decode83 = (str: string) => {
};
export const intToRGB = (int: number) => ({
r: Math.max(0, (int >> 16)),
r: Math.max(0, int >> 16),
g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, (int & 255)),
b: Math.max(0, int & 255),
});
export const getAverageFromBlurhash = (blurhash: string) => {

View file

@ -1,4 +1,4 @@
export default function compareId (id1: string, id2: string) {
export function compareId(id1: string, id2: string) {
if (id1 === id2) {
return 0;
}

View file

@ -1,15 +1,15 @@
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import DisplayName from './display_name';
import { Avatar } from './avatar';
import { DisplayName } from './display_name';
import Permalink from './permalink';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'flavours/glitch/initial_state';
import RelativeTimestamp from './relative_timestamp';
import Skeleton from 'flavours/glitch/components/skeleton';
import { RelativeTimestamp } from './relative_timestamp';
import { Skeleton } from 'flavours/glitch/components/skeleton';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },

View file

@ -4,7 +4,7 @@ import api from 'flavours/glitch/api';
import { FormattedNumber } from 'react-intl';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import classNames from 'classnames';
import Skeleton from 'flavours/glitch/components/skeleton';
import { Skeleton } from 'flavours/glitch/components/skeleton';
const percIncrease = (a, b) => {
let percent;

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import api from 'flavours/glitch/api';
import { FormattedNumber } from 'react-intl';
import { roundTo10 } from 'flavours/glitch/utils/numbers';
import Skeleton from 'flavours/glitch/components/skeleton';
import { Skeleton } from 'flavours/glitch/components/skeleton';
export default class Dimension extends React.PureComponent {

View file

@ -1,8 +1,11 @@
import React, { useCallback, useState } from 'react';
import ShortNumber from './short_number';
import { TransitionMotion, spring } from 'react-motion';
import { reduceMotion } from '../initial_state';
import ShortNumber from './short_number';
const obfuscatedCount = (count: number) => {
if (count < 0) {
return 0;
@ -13,16 +16,13 @@ const obfuscatedCount = (count: number) => {
}
};
type Props = {
interface Props {
value: number;
obfuscate?: boolean;
}
export const AnimatedNumber: React.FC<Props> = ({
value,
obfuscate,
})=> {
export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
const [previousValue, setPreviousValue] = useState(value);
const [direction, setDirection] = useState<1|-1>(1);
const [direction, setDirection] = useState<1 | -1>(1);
if (previousValue !== value) {
setPreviousValue(value);
@ -30,29 +30,52 @@ export const AnimatedNumber: React.FC<Props> = ({
}
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]);
const willLeave = useCallback(
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
[direction]
);
if (reduceMotion) {
return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />;
return obfuscate ? (
<>{obfuscatedCount(value)}</>
) : (
<ShortNumber value={value} />
);
}
const styles = [{
key: `${value}`,
data: value,
style: { y: spring(0, { damping: 35, stiffness: 400 }) },
}];
const styles = [
{
key: `${value}`,
data: value,
style: { y: spring(0, { damping: 35, stiffness: 400 }) },
},
];
return (
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
{items => (
<TransitionMotion
styles={styles}
willEnter={willEnter}
willLeave={willLeave}
>
{(items) => (
<span className='animated-number'>
{items.map(({ key, data, style }) => (
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
<span
key={key}
style={{
position: direction * style.y > 0 ? 'absolute' : 'static',
transform: `translateY(${style.y * 100}%)`,
}}
>
{obfuscate ? (
obfuscatedCount(data as number)
) : (
<ShortNumber value={data as number} />
)}
</span>
))}
</span>
)}
</TransitionMotion>
);
};
export default AnimatedNumber;

View file

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];

View file

@ -154,7 +154,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
this.input.focus();
};
componentWillReceiveProps (nextProps) {
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}

View file

@ -153,7 +153,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
this.textarea.focus();
};
componentWillReceiveProps (nextProps) {
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}

View file

@ -1,10 +1,12 @@
import * as React from 'react';
import classNames from 'classnames';
import { useHovering } from 'flavours/glitch/hooks/useHovering';
import { autoPlayGif } from 'flavours/glitch/initial_state';
import { useHovering } from 'hooks/useHovering';
import type { Account } from 'flavours/glitch/types/resources';
type Props = {
interface Props {
account: Account | undefined;
className?: string;
size: number;
@ -19,7 +21,8 @@ export const Avatar: React.FC<Props> = ({
inline = false,
style: styleFromParent,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif);
const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const style = {
...styleFromParent,
@ -29,12 +32,18 @@ export const Avatar: React.FC<Props> = ({
};
if (account) {
style.backgroundImage = `url(${account.get(hovering ? 'avatar' : 'avatar_static')})`;
style.backgroundImage = `url(${account.get(
hovering ? 'avatar' : 'avatar_static'
)})`;
}
return (
<div
className={classNames('account__avatar', { 'account__avatar-inline': inline }, className)}
className={classNames(
'account__avatar',
{ 'account__avatar-inline': inline },
className
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={style}
@ -44,5 +53,3 @@ export const Avatar: React.FC<Props> = ({
/>
);
};
export default Avatar;

View file

@ -9,7 +9,6 @@ export default class AvatarComposite extends React.PureComponent {
accounts: ImmutablePropTypes.list.isRequired,
animate: PropTypes.bool,
size: PropTypes.number.isRequired,
onAccountClick: PropTypes.func.isRequired,
};
static defaultProps = {
@ -80,15 +79,7 @@ export default class AvatarComposite extends React.PureComponent {
};
return (
<a
href={account.get('url')}
target='_blank'
onClick={(e) => this.props.onAccountClick(account.get('acct'), e)}
title={`@${account.get('acct')}`}
key={account.get('id')}
>
<div style={style} data-avatar-of={`@${account.get('acct')}`} />
</a>
<div key={account.get('id')} style={style} data-avatar-of={`@${account.get('acct')}`} />
);
}

View file

@ -1,26 +1,27 @@
import { decode } from 'blurhash';
import React, { useRef, useEffect } from 'react';
type Props = {
import { decode } from 'blurhash';
interface Props extends React.HTMLAttributes<HTMLCanvasElement> {
hash: string;
width?: number;
height?: number;
dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
children?: never;
[key: string]: any;
}
function Blurhash({
const Blurhash: React.FC<Props> = ({
hash,
width = 32,
height = width,
dummy = false,
...canvasProps
}: Props) {
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const canvas = canvasRef.current!;
// eslint-disable-next-line no-self-assign
canvas.width = canvas.width; // resets canvas
@ -40,6 +41,8 @@ function Blurhash({
return (
<canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
);
}
};
export default React.memo(Blurhash);
const MemoizedBlurhash = React.memo(Blurhash);
export { MemoizedBlurhash as Blurhash };

View file

@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollTop } from '../scroll';
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
export default class Column extends React.PureComponent {
static propTypes = {
@ -37,17 +39,17 @@ export default class Column extends React.PureComponent {
componentDidMount () {
if (this.props.bindToDocument) {
document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
document.addEventListener('wheel', this.handleWheel, listenerOptions);
} else {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
}
}
componentWillUnmount () {
if (this.props.bindToDocument) {
document.removeEventListener('wheel', this.handleWheel);
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
} else {
this.node.removeEventListener('wheel', this.handleWheel);
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
}
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { createPortal } from 'react-dom';
export default class ColumnBackButton extends React.PureComponent {
@ -14,17 +14,15 @@ export default class ColumnBackButton extends React.PureComponent {
multiColumn: PropTypes.bool,
};
handleClick = (event) => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history.state) {
const state = this.context.router.history.location.state;
if (event.shiftKey && state && state.mastodonBackSteps) {
this.context.router.history.go(-state.mastodonBackSteps);
} else {
this.context.router.history.goBack();
}
handleClick = () => {
const { router } = this.context;
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
if (router.route.location.key) {
router.history.goBack();
} else {
this.context.router.history.push('/');
router.history.push('/');
}
};

View file

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
export default class ColumnBackButtonSlim extends React.PureComponent {
@ -9,17 +9,15 @@ export default class ColumnBackButtonSlim extends React.PureComponent {
router: PropTypes.object,
};
handleClick = (event) => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history.state) {
const state = this.context.router.history.location.state;
if (event.shiftKey && state && state.mastodonBackSteps) {
this.context.router.history.go(-state.mastodonBackSteps);
} else {
this.context.router.history.goBack();
}
handleClick = () => {
const { router } = this.context;
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
if (router.route.location.key) {
router.history.goBack();
} else {
this.context.router.history.push('/');
router.history.push('/');
}
};

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
@ -42,20 +42,6 @@ class ColumnHeader extends React.PureComponent {
animating: false,
};
historyBack = (skip) => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history.state) {
const state = this.context.router.history.location.state;
if (skip && state && state.mastodonBackSteps) {
this.context.router.history.go(-state.mastodonBackSteps);
} else {
this.context.router.history.goBack();
}
} else {
this.context.router.history.push('/');
}
};
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true });
@ -73,8 +59,16 @@ class ColumnHeader extends React.PureComponent {
this.props.onMove(1);
};
handleBackClick = (event) => {
this.historyBack(event.shiftKey);
handleBackClick = () => {
const { router } = this.context;
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
if (router.route.location.key) {
router.history.goBack();
} else {
router.history.push('/');
}
};
handleTransitionEnd = () => {
@ -83,8 +77,9 @@ class ColumnHeader extends React.PureComponent {
handlePin = () => {
if (!this.props.pinned) {
this.historyBack();
this.context.router.history.replace('/');
}
this.props.onPin();
};

View file

@ -1,5 +1,5 @@
import React from 'react';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import { bannerSettings } from 'flavours/glitch/settings';

View file

@ -1,104 +0,0 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { autoPlayGif } from 'flavours/glitch/initial_state';
import Skeleton from 'flavours/glitch/components/skeleton';
export default class DisplayName extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
className: PropTypes.string,
inline: PropTypes.bool,
localDomain: PropTypes.string,
others: ImmutablePropTypes.list,
handleClick: PropTypes.func,
onAccountClick: PropTypes.func,
};
handleMouseEnter = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
};
render() {
const { account, className, inline, localDomain, others, onAccountClick } = this.props;
const computedClass = classNames('display-name', { inline }, className);
let displayName, suffix;
let acct;
if (account) {
acct = account.get('acct');
if (acct.indexOf('@') === -1 && localDomain) {
acct = `${acct}@${localDomain}`;
}
}
if (others && others.size > 0) {
displayName = others.take(2).map(a => (
<a
key={a.get('id')}
href={a.get('url')}
target='_blank'
onClick={(e) => onAccountClick(a.get('acct'), e)}
title={`@${a.get('acct')}`}
rel='noopener noreferrer'
>
<bdi key={a.get('id')}>
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} />
</bdi>
</a>
)).reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) {
displayName.push(` +${others.size - 2}`);
}
suffix = (
<a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('acct'), e)} rel='noopener noreferrer'>
<span className='display-name__account'>@{acct}</span>
</a>
);
} else if (account) {
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
suffix = <span className='display-name__account'>@{acct}</span>;
} else {
displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>;
suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>;
}
return (
<span className={computedClass} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
{displayName}
{inline ? ' ' : null}
{suffix}
</span>
);
}
}

View file

@ -0,0 +1,124 @@
import React from 'react';
import classNames from 'classnames';
import type { List } from 'immutable';
import type { Account } from 'flavours/glitch/types/resources';
import { autoPlayGif } from '../initial_state';
import { Skeleton } from './skeleton';
interface Props {
account: Account;
others: List<Account>;
localDomain: string;
inline?: boolean;
}
export class DisplayName extends React.PureComponent<Props> {
handleMouseEnter: React.ReactEventHandler<HTMLSpanElement> = ({
currentTarget,
}) => {
if (autoPlayGif) {
return;
}
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
emojis.forEach((emoji) => {
const originalSrc = emoji.getAttribute('data-original');
if (originalSrc != null) emoji.src = originalSrc;
});
};
handleMouseLeave: React.ReactEventHandler<HTMLSpanElement> = ({
currentTarget,
}) => {
if (autoPlayGif) {
return;
}
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
emojis.forEach((emoji) => {
const staticSrc = emoji.getAttribute('data-static');
if (staticSrc != null) emoji.src = staticSrc;
});
};
render() {
const { others, localDomain, inline } = this.props;
let displayName: React.ReactNode, suffix: React.ReactNode, account: Account;
if (others && others.size > 1) {
displayName = others
.take(2)
.map((a) => (
<bdi key={a.get('id')}>
<strong
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
/>
</bdi>
))
.reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`;
}
} else if ((others && others.size > 0) || this.props.account) {
if (others && others.size > 0) {
account = others.first();
} else {
account = this.props.account;
}
let acct = account.get('acct');
if (acct.indexOf('@') === -1 && localDomain) {
acct = `${acct}@${localDomain}`;
}
displayName = (
<bdi>
<strong
className='display-name__html'
dangerouslySetInnerHTML={{
__html: account.get('display_name_html'),
}}
/>
</bdi>
);
suffix = <span className='display-name__account'>@{acct}</span>;
} else {
displayName = (
<bdi>
<strong className='display-name__html'>
<Skeleton width='10ch' />
</strong>
</bdi>
);
suffix = (
<span className='display-name__account'>
<Skeleton width='7ch' />
</span>
);
}
return (
<span
className={classNames('display-name', { inline })}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{displayName}
{inline ? ' ' : null}
{suffix}
</span>
);
}
}

View file

@ -1,43 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
});
class Account extends ImmutablePureComponent {
static propTypes = {
domain: PropTypes.string,
onUnblockDomain: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleDomainUnblock = () => {
this.props.onUnblockDomain(this.props.domain);
};
render () {
const { domain, intl } = this.props;
return (
<div className='domain'>
<div className='domain__wrapper'>
<span className='domain__domain-name'>
<strong>{domain}</strong>
</span>
<div className='domain__buttons'>
<IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
</div>
</div>
</div>
);
}
}
export default injectIntl(Account);

View file

@ -0,0 +1,45 @@
import React, { useCallback } from 'react';
import type { InjectedIntl } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import { IconButton } from './icon_button';
const messages = defineMessages({
unblockDomain: {
id: 'account.unblock_domain',
defaultMessage: 'Unblock domain {domain}',
},
});
interface Props {
domain: string;
onUnblockDomain: (domain: string) => void;
intl: InjectedIntl;
}
const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
const handleDomainUnblock = useCallback(() => {
onUnblockDomain(domain);
}, [domain, onUnblockDomain]);
return (
<div className='domain'>
<div className='domain__wrapper'>
<span className='domain__domain-name'>
<strong>{domain}</strong>
</span>
<div className='domain__buttons'>
<IconButton
active
icon='unlock'
title={intl.formatMessage(messages.unblockDomain, { domain })}
onClick={handleDomainUnblock}
/>
</div>
</div>
</div>
);
};
export const Domain = injectIntl(_Domain);

View file

@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import Overlay from 'react-overlays/Overlay';
import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames';
import { CircularProgress } from 'flavours/glitch/components/loading_indicator';
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
let id = 0;
class DropdownMenu extends React.PureComponent {
@ -35,12 +35,13 @@ class DropdownMenu extends React.PureComponent {
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
e.stopPropagation();
}
};
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('click', this.handleDocumentClick, { capture: true });
document.addEventListener('keydown', this.handleKeyDown, { capture: true });
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) {
@ -49,8 +50,8 @@ class DropdownMenu extends React.PureComponent {
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
document.removeEventListener('keydown', this.handleKeyDown, { capture: true });
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}

View file

@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import DropdownMenu from './containers/dropdown_menu_container';
import { connect } from 'react-redux';
import { openModal } from 'flavours/glitch/actions/modal';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import InlineAccount from 'flavours/glitch/components/inline_account';
const mapDispatchToProps = (dispatch, { statusId }) => ({

View file

@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
type Props = {
interface Props {
src: string;
key: string;
alt?: string;
@ -17,19 +17,23 @@ export const GIFV: React.FC<Props> = ({
width,
height,
onClick,
})=> {
}) => {
const [loading, setLoading] = useState(true);
const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
setLoading(false);
}, [setLoading]);
const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
useCallback(() => {
setLoading(false);
}, [setLoading]);
const handleClick: React.MouseEventHandler = useCallback((e) => {
if (onClick) {
e.stopPropagation();
onClick();
}
}, [onClick]);
const handleClick: React.MouseEventHandler = useCallback(
(e) => {
if (onClick) {
e.stopPropagation();
onClick();
}
},
[onClick]
);
return (
<div className='gifv' style={{ position: 'relative' }}>
@ -64,5 +68,3 @@ export const GIFV: React.FC<Props> = ({
</div>
);
};
export default GIFV;

View file

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from './permalink';
import ShortNumber from 'flavours/glitch/components/short_number';
import Skeleton from 'flavours/glitch/components/skeleton';
import { Skeleton } from 'flavours/glitch/components/skeleton';
import classNames from 'classnames';
class SilentErrorBoundary extends React.Component {

View file

@ -1,14 +1,22 @@
import React from 'react';
import classNames from 'classnames';
type Props = {
interface Props extends React.HTMLAttributes<HTMLImageElement> {
id: string;
className?: string;
fixedWidth?: boolean;
children?: never;
[key: string]: any;
}
export const Icon: React.FC<Props> = ({ id, className, fixedWidth, ...other }) =>
<i className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />;
export default Icon;
export const Icon: React.FC<Props> = ({
id,
className,
fixedWidth,
...other
}) => (
<i
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
{...other}
/>
);

View file

@ -1,9 +1,11 @@
import React from 'react';
import classNames from 'classnames';
import { Icon } from './icon';
import { AnimatedNumber } from './animated_number';
type Props = {
import classNames from 'classnames';
import { AnimatedNumber } from './animated_number';
import { Icon } from './icon';
interface Props {
className?: string;
title: string;
icon: string;
@ -21,18 +23,17 @@ type Props = {
animate: boolean;
overlay: boolean;
tabIndex: number;
label: string;
label?: string;
counter?: number;
obfuscateCount?: boolean;
href?: string;
ariaHidden: boolean;
}
type States = {
activate: boolean,
deactivate: boolean,
interface States {
activate: boolean;
deactivate: boolean;
}
export default class IconButton extends React.PureComponent<Props, States> {
export class IconButton extends React.PureComponent<Props, States> {
static defaultProps = {
size: 18,
active: false,
@ -48,7 +49,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
deactivate: false,
};
UNSAFE_componentWillReceiveProps (nextProps: Props) {
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (!nextProps.animate) return;
if (this.props.active && !nextProps.active) {
@ -58,7 +59,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
}
}
handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
if (!this.props.disabled && this.props.onClick != null) {
@ -84,7 +85,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
}
};
render () {
render() {
// Hack required for some icons which have an overriden size
let containerSize = '1.28571429em';
if (this.props.style?.fontSize) {
@ -120,10 +121,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
ariaHidden,
} = this.props;
const {
activate,
deactivate,
} = this.state;
const { activate, deactivate } = this.state;
const classes = classNames(className, 'icon-button', {
active,
@ -141,7 +139,12 @@ export default class IconButton extends React.PureComponent<Props, States> {
let contents = (
<React.Fragment>
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
<Icon id={icon} fixedWidth aria-hidden='true' />{' '}
{typeof counter !== 'undefined' && (
<span className='icon-button__counter'>
<AnimatedNumber value={counter} obfuscate={obfuscateCount} />
</span>
)}
{this.props.label}
</React.Fragment>
);
@ -174,5 +177,4 @@ export default class IconButton extends React.PureComponent<Props, States> {
</button>
);
}
}

View file

@ -1,20 +1,26 @@
import React from 'react';
import { Icon } from './icon';
const formatNumber = (num: number): number | string => num > 40 ? '40+' : num;
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);
type Props = {
interface Props {
id: string;
count: number;
issueBadge: boolean;
className: string;
}
const IconWithBadge: React.FC<Props> = ({ id, count, issueBadge, className }) => (
export const IconWithBadge: React.FC<Props> = ({
id,
count,
issueBadge,
className,
}) => (
<i className='icon-with-badge'>
<Icon id={id} fixedWidth className={className} />
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
{count > 0 && (
<i className='icon-with-badge__badge'>{formatNumber(count)}</i>
)}
{issueBadge && <i className='icon-with-badge__issue-badge' />}
</i>
);
export default IconWithBadge;

View file

@ -1,33 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Blurhash from './blurhash';
import classNames from 'classnames';
export default class Image extends React.PureComponent {
static propTypes = {
src: PropTypes.string,
srcSet: PropTypes.string,
blurhash: PropTypes.string,
className: PropTypes.string,
};
state = {
loaded: false,
};
handleLoad = () => this.setState({ loaded: true });
render () {
const { src, srcSet, blurhash, className } = this.props;
const { loaded } = this.state;
return (
<div className={classNames('image', { loaded }, className)} role='presentation'>
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
<img src={src} srcSet={srcSet} alt='' onLoad={this.handleLoad} />
</div>
);
}
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { makeGetAccount } from 'flavours/glitch/selectors';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },

View file

@ -2,12 +2,12 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { is } from 'immutable';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import { debounce } from 'lodash';
import Blurhash from 'flavours/glitch/components/blurhash';
import { Blurhash } from 'flavours/glitch/components/blurhash';
const messages = defineMessages({
hidden: {
@ -254,7 +254,7 @@ class MediaGallery extends React.PureComponent {
window.removeEventListener('resize', this.handleResize);
}
componentWillReceiveProps (nextProps) {
UNSAFE_componentWillReceiveProps (nextProps) {
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
@ -286,7 +286,7 @@ class MediaGallery extends React.PureComponent {
};
handleClick = (index) => {
this.props.onOpenMedia(this.props.media, index);
this.props.onOpenMedia(this.props.media, index, this.props.lang);
};
handleRef = (node) => {

View file

@ -62,7 +62,7 @@ export default class ModalRoot extends React.PureComponent {
}
}
componentWillReceiveProps (nextProps) {
UNSAFE_componentWillReceiveProps (nextProps) {
if (!!nextProps.children && !this.props.children) {
this.activeElement = document.activeElement;

View file

@ -1,12 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
const NotSignedInIndicator = () => (
<div className='scrollable scrollable--flex'>
<div className='empty-column-indicator'>
<FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' />
</div>
</div>
);
export default NotSignedInIndicator;

View file

@ -0,0 +1,14 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
export const NotSignedInIndicator: React.FC = () => (
<div className='scrollable scrollable--flex'>
<div className='empty-column-indicator'>
<FormattedMessage
id='not_signed_in_indicator.not_signed_in'
defaultMessage='You need to login to access this resource.'
/>
</div>
</div>
);

View file

@ -10,7 +10,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';
const messages = defineMessages({

View file

@ -24,9 +24,7 @@ export default class Permalink extends React.PureComponent {
if (this.context.router) {
e.preventDefault();
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(this.props.to, state);
this.context.router.history.push(this.props.to);
}
}
};

View file

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';

View file

@ -8,8 +8,8 @@ import Motion from 'flavours/glitch/features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'flavours/glitch/features/emoji/emoji';
import RelativeTimestamp from './relative_timestamp';
import Icon from 'flavours/glitch/components/icon';
import { RelativeTimestamp } from './relative_timestamp';
import { Icon } from 'flavours/glitch/components/icon';
const messages = defineMessages({
closed: {

View file

@ -1,35 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class RadioButton extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
checked: PropTypes.bool,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
label: PropTypes.node.isRequired,
};
render () {
const { name, value, checked, onChange, label } = this.props;
return (
<label className='radio-button'>
<input
name={name}
type='radio'
value={value}
checked={checked}
onChange={onChange}
/>
<span className={classNames('radio-button__input', { checked })} />
<span>{label}</span>
</label>
);
}
}

View file

@ -0,0 +1,35 @@
import React from 'react';
import classNames from 'classnames';
interface Props {
value: string;
checked: boolean;
name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode;
}
export const RadioButton: React.FC<Props> = ({
name,
value,
checked,
onChange,
label,
}) => {
return (
<label className='radio-button'>
<input
name={name}
type='radio'
value={value}
checked={checked}
onChange={onChange}
/>
<span className={classNames('radio-button__input', { checked })} />
<span>{label}</span>
</label>
);
};

View file

@ -1,23 +1,55 @@
import React from 'react';
import { injectIntl, defineMessages, InjectedIntl } from 'react-intl';
import type { InjectedIntl } from 'react-intl';
import { injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
today: { id: 'relative_time.today', defaultMessage: 'today' },
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' },
just_now_full: {
id: 'relative_time.full.just_now',
defaultMessage: 'just now',
},
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
seconds_full: {
id: 'relative_time.full.seconds',
defaultMessage: '{number, plural, one {# second} other {# seconds}} ago',
},
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' },
minutes_full: {
id: 'relative_time.full.minutes',
defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago',
},
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
hours_full: {
id: 'relative_time.full.hours',
defaultMessage: '{number, plural, one {# hour} other {# hours}} ago',
},
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
days_full: {
id: 'relative_time.full.days',
defaultMessage: '{number, plural, one {# day} other {# days}} ago',
},
moments_remaining: {
id: 'time_remaining.moments',
defaultMessage: 'Moments remaining',
},
seconds_remaining: {
id: 'time_remaining.seconds',
defaultMessage: '{number, plural, one {# second} other {# seconds}} left',
},
minutes_remaining: {
id: 'time_remaining.minutes',
defaultMessage: '{number, plural, one {# minute} other {# minutes}} left',
},
hours_remaining: {
id: 'time_remaining.hours',
defaultMessage: '{number, plural, one {# hour} other {# hours}} left',
},
days_remaining: {
id: 'time_remaining.days',
defaultMessage: '{number, plural, one {# day} other {# days}} left',
},
});
const dateFormatOptions = {
@ -36,8 +68,8 @@ const shortDateFormatOptions = {
const SECOND = 1000;
const MINUTE = 1000 * 60;
const HOUR = 1000 * 60 * 60;
const DAY = 1000 * 60 * 60 * 24;
const HOUR = 1000 * 60 * 60;
const DAY = 1000 * 60 * 60 * 24;
const MAX_DELAY = 2147483647;
@ -57,20 +89,27 @@ const selectUnits = (delta: number) => {
const getUnitDelay = (units: string) => {
switch (units) {
case 'second':
return SECOND;
case 'minute':
return MINUTE;
case 'hour':
return HOUR;
case 'day':
return DAY;
default:
return MAX_DELAY;
case 'second':
return SECOND;
case 'minute':
return MINUTE;
case 'hour':
return HOUR;
case 'day':
return DAY;
default:
return MAX_DELAY;
}
};
export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year: number, timeGiven: boolean, short?: boolean) => {
export const timeAgoString = (
intl: InjectedIntl,
date: Date,
now: number,
year: number,
timeGiven: boolean,
short?: boolean
) => {
const delta = now - date.getTime();
let relativeTime;
@ -78,27 +117,49 @@ export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year:
if (delta < DAY && !timeGiven) {
relativeTime = intl.formatMessage(messages.today);
} else if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full);
relativeTime = intl.formatMessage(
short ? messages.just_now : messages.just_now_full
);
} else if (delta < 7 * DAY) {
if (delta < MINUTE) {
relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) });
relativeTime = intl.formatMessage(
short ? messages.seconds : messages.seconds_full,
{ number: Math.floor(delta / SECOND) }
);
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) });
relativeTime = intl.formatMessage(
short ? messages.minutes : messages.minutes_full,
{ number: Math.floor(delta / MINUTE) }
);
} else if (delta < DAY) {
relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) });
relativeTime = intl.formatMessage(
short ? messages.hours : messages.hours_full,
{ number: Math.floor(delta / HOUR) }
);
} else {
relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) });
relativeTime = intl.formatMessage(
short ? messages.days : messages.days_full,
{ number: Math.floor(delta / DAY) }
);
}
} else if (date.getFullYear() === year) {
relativeTime = intl.formatDate(date, shortDateFormatOptions);
} else {
relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
relativeTime = intl.formatDate(date, {
...shortDateFormatOptions,
year: 'numeric',
});
}
return relativeTime;
};
const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGiven = true) => {
const timeRemainingString = (
intl: InjectedIntl,
date: Date,
now: number,
timeGiven = true
) => {
const delta = date.getTime() - now;
let relativeTime;
@ -108,96 +169,114 @@ const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGi
} else if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.moments_remaining);
} else if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
relativeTime = intl.formatMessage(messages.seconds_remaining, {
number: Math.floor(delta / SECOND),
});
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
relativeTime = intl.formatMessage(messages.minutes_remaining, {
number: Math.floor(delta / MINUTE),
});
} else if (delta < DAY) {
relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
relativeTime = intl.formatMessage(messages.hours_remaining, {
number: Math.floor(delta / HOUR),
});
} else {
relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
relativeTime = intl.formatMessage(messages.days_remaining, {
number: Math.floor(delta / DAY),
});
}
return relativeTime;
};
type Props = {
interface Props {
intl: InjectedIntl;
timestamp: string;
year: number;
futureDate?: boolean;
short?: boolean;
}
type States = {
interface States {
now: number;
}
class RelativeTimestamp extends React.Component<Props, States> {
state = {
now: this.props.intl.now(),
};
static defaultProps = {
year: (new Date()).getFullYear(),
year: new Date().getFullYear(),
short: true,
};
_timer: number | undefined;
shouldComponentUpdate (nextProps: Props, nextState: States) {
shouldComponentUpdate(nextProps: Props, nextState: States) {
// As of right now the locale doesn't change without a new page load,
// but we might as well check in case that ever changes.
return this.props.timestamp !== nextProps.timestamp ||
return (
this.props.timestamp !== nextProps.timestamp ||
this.props.intl.locale !== nextProps.intl.locale ||
this.state.now !== nextState.now;
this.state.now !== nextState.now
);
}
UNSAFE_componentWillReceiveProps (nextProps: Props) {
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.timestamp !== nextProps.timestamp) {
this.setState({ now: this.props.intl.now() });
}
}
componentDidMount () {
componentDidMount() {
this._scheduleNextUpdate(this.props, this.state);
}
UNSAFE_componentWillUpdate (nextProps: Props, nextState: States) {
UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) {
this._scheduleNextUpdate(nextProps, nextState);
}
componentWillUnmount () {
componentWillUnmount() {
window.clearTimeout(this._timer);
}
_scheduleNextUpdate (props: Props, state: States) {
_scheduleNextUpdate(props: Props, state: States) {
window.clearTimeout(this._timer);
const { timestamp } = props;
const delta = (new Date(timestamp)).getTime() - state.now;
const unitDelay = getUnitDelay(selectUnits(delta));
const unitRemainder = Math.abs(delta % unitDelay);
const { timestamp } = props;
const delta = new Date(timestamp).getTime() - state.now;
const unitDelay = getUnitDelay(selectUnits(delta));
const unitRemainder = Math.abs(delta % unitDelay);
const updateInterval = 1000 * 10;
const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
const delay =
delta < 0
? Math.max(updateInterval, unitDelay - unitRemainder)
: Math.max(updateInterval, unitRemainder);
this._timer = window.setTimeout(() => {
this.setState({ now: this.props.intl.now() });
}, delay);
}
render () {
render() {
const { timestamp, intl, year, futureDate, short } = this.props;
const timeGiven = timestamp.includes('T');
const date = new Date(timestamp);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
const timeGiven = timestamp.includes('T');
const date = new Date(timestamp);
const relativeTime = futureDate
? timeRemainingString(intl, date, this.state.now, timeGiven)
: timeAgoString(intl, date, this.state.now, year, timeGiven, short);
return (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
<time
dateTime={timestamp}
title={intl.formatDate(date, dateFormatOptions)}
>
{relativeTime}
</time>
);
}
}
export default injectIntl(RelativeTimestamp);
const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp);
export { RelativeTimestampWithIntl as RelativeTimestamp };

View file

@ -8,12 +8,15 @@ import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/inters
import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
import classNames from 'classnames';
import { supportsPassiveEvents } from 'detect-passive-events';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
import LoadingIndicator from './loading_indicator';
import { connect } from 'react-redux';
const MOUSE_IDLE_DELAY = 300;
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const mapStateToProps = (state, { scrollKey }) => {
return {
preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
@ -236,20 +239,20 @@ class ScrollableList extends PureComponent {
attachScrollListener () {
if (this.props.bindToDocument) {
document.addEventListener('scroll', this.handleScroll);
document.addEventListener('wheel', this.handleWheel);
document.addEventListener('wheel', this.handleWheel, listenerOptions);
} else {
this.node.addEventListener('scroll', this.handleScroll);
this.node.addEventListener('wheel', this.handleWheel);
this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
}
}
detachScrollListener () {
if (this.props.bindToDocument) {
document.removeEventListener('scroll', this.handleScroll);
document.removeEventListener('wheel', this.handleWheel);
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
} else {
this.node.removeEventListener('scroll', this.handleScroll);
this.node.removeEventListener('wheel', this.handleWheel);
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
}
}

View file

@ -4,10 +4,10 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { fetchServer } from 'flavours/glitch/actions/server';
import ShortNumber from 'flavours/glitch/components/short_number';
import Skeleton from 'flavours/glitch/components/skeleton';
import { Skeleton } from 'flavours/glitch/components/skeleton';
import Account from 'flavours/glitch/containers/account_container';
import { domain } from 'flavours/glitch/initial_state';
import Image from 'flavours/glitch/components/image';
import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image';
import { Link } from 'react-router-dom';
const messages = defineMessages({
@ -41,7 +41,7 @@ class ServerBanner extends React.PureComponent {
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
</div>
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
<div className='server-banner__description'>
{isLoading ? (

View file

@ -0,0 +1,35 @@
import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import { Blurhash } from './blurhash';
interface Props {
src: string;
srcSet?: string;
blurhash?: string;
className?: string;
}
export const ServerHeroImage: React.FC<Props> = ({
src,
srcSet,
blurhash,
className,
}) => {
const [loaded, setLoaded] = useState(false);
const handleLoad = useCallback(() => {
setLoaded(true);
}, [setLoaded]);
return (
<div
className={classNames('image', { loaded }, className)}
role='presentation'
>
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
<img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
</div>
);
};

View file

@ -1,11 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>&zwnj;</span>;
Skeleton.propTypes = {
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
};
export default Skeleton;

View file

@ -0,0 +1,12 @@
import React from 'react';
interface Props {
width?: number | string;
height?: number | string;
}
export const Skeleton: React.FC<Props> = ({ width, height }) => (
<span className='skeleton' style={{ width, height }}>
&zwnj;
</span>
);

View file

@ -372,9 +372,7 @@ class Status extends ImmutablePureComponent {
status.getIn(['reblog', 'id'], status.get('id'))
}`;
}
let state = { ...router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
router.history.push(destination, state);
router.history.push(destination);
}
e.preventDefault();
}
@ -394,11 +392,12 @@ class Status extends ImmutablePureComponent {
handleOpenVideo = (options) => {
const { status } = this.props;
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options);
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options);
};
handleOpenMedia = (media, index) => {
this.props.onOpenMedia(this.props.status.get('id'), media, index);
const { status } = this.props;
this.props.onOpenMedia(status.get('id'), media, index, status.get('language'));
};
handleHotkeyOpenMedia = e => {
@ -445,16 +444,12 @@ class Status extends ImmutablePureComponent {
};
handleHotkeyOpen = () => {
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
const status = this.props.status;
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state);
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
};
handleHotkeyOpenProfile = () => {
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
};
handleHotkeyMoveUp = e => {

View file

@ -1,12 +1,12 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, maxReactions } from 'flavours/glitch/initial_state';
import RelativeTimestamp from './relative_timestamp';
import { RelativeTimestamp } from './relative_timestamp';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
@ -170,10 +170,9 @@ class StatusActionBar extends ImmutablePureComponent {
handleOpen = () => {
let state = { ...this.context.router.history.location.state };
if (state.mastodonModalKey) {
this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 });
this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
} else {
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, state);
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
}
};

View file

@ -5,7 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import Permalink from './permalink';
import { connect } from 'react-redux';
import classnames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';

View file

@ -4,9 +4,9 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
// Mastodon imports.
import Avatar from './avatar';
import { Avatar } from './avatar';
import AvatarOverlay from './avatar_overlay';
import DisplayName from './display_name';
import { DisplayName } from './display_name';
export default class StatusHeader extends React.PureComponent {

View file

@ -5,9 +5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
// Mastodon imports.
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import VisibilityIcon from './status_visibility_icon';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { languages } from 'flavours/glitch/initial_state';
// Messages for use with internationalization stuff.

View file

@ -26,6 +26,7 @@ export default class StatusList extends ImmutablePureComponent {
alwaysPrepend: PropTypes.bool,
withCounters: PropTypes.bool,
timelineId: PropTypes.string.isRequired,
lastId: PropTypes.string,
regex: PropTypes.string,
};
@ -56,7 +57,8 @@ export default class StatusList extends ImmutablePureComponent {
};
handleLoadOlder = debounce(() => {
this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
const { statusIds, lastId, onLoadMore } = this.props;
onLoadMore(lastId || (statusIds.size > 0 ? statusIds.last() : undefined));
}, 300, { leading: true });
_selectChild (index, align_top) {

View file

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { me } from 'flavours/glitch/initial_state';
export default class StatusPrepend extends React.PureComponent {

View file

@ -7,7 +7,7 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
import classNames from 'classnames';
import React from 'react';
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
import AnimatedNumber from './animated_number';
import { AnimatedNumber } from './animated_number';
import { assetHost } from '../utils/config';
export default class StatusReactions extends ImmutablePureComponent {

View file

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
const messages = defineMessages({
public: { id: 'privacy.public.short', defaultMessage: 'Public' },

View file

@ -1,18 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
const TimelineHint = ({ resource, url }) => (
<div className='timeline-hint'>
<strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
<br />
<a href={url} target='_blank'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
</div>
);
TimelineHint.propTypes = {
resource: PropTypes.node.isRequired,
url: PropTypes.string.isRequired,
};
export default TimelineHint;

View file

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

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import { store } from 'flavours/glitch/store/configureStore';
import { store } from 'flavours/glitch/store';
import { hydrateStore } from 'flavours/glitch/actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from 'mastodon/locales';

View file

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { blockDomain, unblockDomain } from '../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Domain from '../components/domain';
import { Domain } from '../components/domain';
import { openModal } from '../actions/modal';
const messages = defineMessages({

View file

@ -5,7 +5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { Provider as ReduxProvider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import { store } from 'flavours/glitch/store/configureStore';
import { store } from 'flavours/glitch/store';
import UI from 'flavours/glitch/features/ui';
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
import { hydrateStore } from 'flavours/glitch/actions/store';

View file

@ -1,5 +1,5 @@
import React, { PureComponent, Fragment } from 'react';
import ReactDOM from 'react-dom';
import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { fromJS } from 'immutable';
@ -29,19 +29,20 @@ export default class MediaContainer extends PureComponent {
state = {
media: null,
index: null,
lang: null,
time: null,
backgroundColor: null,
options: null,
};
handleOpenMedia = (media, index) => {
handleOpenMedia = (media, index, lang) => {
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media, index });
this.setState({ media, index, lang });
};
handleOpenVideo = (options) => {
handleOpenVideo = (lang, options) => {
const { components } = this.props;
const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
const mediaList = fromJS(media);
@ -49,7 +50,7 @@ export default class MediaContainer extends PureComponent {
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media: mediaList, options });
this.setState({ media: mediaList, lang, options });
};
handleCloseMedia = () => {
@ -94,7 +95,7 @@ export default class MediaContainer extends PureComponent {
}),
});
return ReactDOM.createPortal(
return createPortal(
<Component {...props} key={`media-${i}`} />,
component,
);
@ -105,6 +106,7 @@ export default class MediaContainer extends PureComponent {
<MediaModal
media={this.state.media}
index={this.state.index || 0}
lang={this.state.lang}
currentTime={this.state.options?.startTime}
autoPlay={this.state.options?.autoPlay}
volume={this.state.options?.defaultVolume}

View file

@ -221,12 +221,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(mentionCompose(account, router));
},
onOpenMedia (statusId, media, index) {
dispatch(openModal('MEDIA', { statusId, media, index }));
onOpenMedia (statusId, media, index, lang) {
dispatch(openModal('MEDIA', { statusId, media, index, lang }));
},
onOpenVideo (statusId, media, options) {
dispatch(openModal('VIDEO', { statusId, media, options }));
onOpenVideo (statusId, media, lang, options) {
dispatch(openModal('VIDEO', { statusId, media, lang, options }));
},
onBlock (status) {

View file

@ -8,10 +8,10 @@ import LinkFooter from 'flavours/glitch/features/ui/components/link_footer';
import { Helmet } from 'react-helmet';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
import Account from 'flavours/glitch/containers/account_container';
import Skeleton from 'flavours/glitch/components/skeleton';
import Icon from 'flavours/glitch/components/icon';
import { Skeleton } from 'flavours/glitch/components/skeleton';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';
import Image from 'flavours/glitch/components/image';
import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image';
const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' },
@ -114,7 +114,7 @@ class About extends React.PureComponent {
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
<div className='scrollable about'>
<div className='about__header'>
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
</div>

View file

@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import Textarea from 'react-textarea-autosize';
const messages = defineMessages({

View file

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { NavLink } from 'react-router-dom';
import { FormattedMessage, FormattedNumber } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
class ActionBar extends React.PureComponent {

View file

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
export default class FollowRequestNote extends ImmutablePureComponent {

View file

@ -6,9 +6,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import IconButton from 'flavours/glitch/components/icon_button';
import Avatar from 'flavours/glitch/components/avatar';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { Avatar } from 'flavours/glitch/components/avatar';
import Button from 'flavours/glitch/components/button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container';

View file

@ -1,6 +1,6 @@
import Blurhash from 'flavours/glitch/components/blurhash';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import PropTypes from 'prop-types';
import React from 'react';

View file

@ -142,16 +142,17 @@ class AccountGallery extends ImmutablePureComponent {
handleOpenMedia = attachment => {
const { dispatch } = this.props;
const statusId = attachment.getIn(['status', 'id']);
const lang = attachment.getIn(['status', 'language']);
if (attachment.get('type') === 'video') {
dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } }));
dispatch(openModal('VIDEO', { media: attachment, statusId, lang, options: { autoPlay: true } }));
} else if (attachment.get('type') === 'audio') {
dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } }));
dispatch(openModal('AUDIO', { media: attachment, statusId, lang, options: { autoPlay: true } }));
} else {
const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
dispatch(openModal('MEDIA', { media, index, statusId }));
dispatch(openModal('MEDIA', { media, index, statusId, lang }));
}
};

View file

@ -4,8 +4,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AvatarOverlay from '../../../components/avatar_overlay';
import DisplayName from '../../../components/display_name';
import Icon from 'flavours/glitch/components/icon';
import { DisplayName } from '../../../components/display_name';
import { Icon } from 'flavours/glitch/components/icon';
export default class MovedNote extends ImmutablePureComponent {
@ -21,9 +21,7 @@ export default class MovedNote extends ImmutablePureComponent {
handleAccountClick = e => {
if (e.button === 0) {
e.preventDefault();
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.to.get('acct')}`, state);
this.context.router.history.push(`/@${this.props.to.get('acct')}`);
}
e.stopPropagation();

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