Compare commits

..

322 Commits

Author SHA1 Message Date
Claire
7a51ad7ebd Merge pull request #3121 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 69e14246b8
2025-07-08 16:30:51 +02:00
Claire
06a46e77b8 Merge commit '69e14246b838443985a541e97327494b8d2fdffb' into glitch-soc/merge-4.4 2025-07-08 16:15:29 +02:00
Claire
69e14246b8 Fix 4.4 container images not being marked as latest (#35294) 2025-07-08 16:07:41 +02:00
Claire
174370dec2 Merge pull request #3120 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to c1794fb948
2025-07-08 16:06:31 +02:00
Claire
18e08bf493 Merge commit 'c1794fb948dfb13865214bce8a90e88da44d4ff6' into glitch-soc/merge-4.4 2025-07-08 15:51:09 +02:00
Claire
c1794fb948 Bump version to v4.4.0 (#35268) 2025-07-08 15:25:26 +02:00
Claire
061c966ab3 Merge pull request #3119 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to 333a17a478
2025-07-08 14:02:48 +02:00
diondiondion
326f6bc12a [Glitch] fix: Fix can't skip search field by tabbing
Port 2dcededcf0 to glitch-soc

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com>
2025-06-18 02:04:20 +02:00
diondiondion
474464ffff fix: Tweak focus style & spacing of list/hashtags expand/collapse button (#35075) 2025-06-17 17:54:23 +00:00
Claire
f4c850dff6 Merge pull request #3104 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstrema changes up to 98f98249ff
2025-06-17 19:38:56 +02:00
Claire
5baeb567c0 [Glitch] Fix sidebar rest position on mobile layout on RTL locales
Port af157939d9 to glitch-soc

Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-17 18:58:35 +02:00
diondiondion
a45d2b4398 [Glitch] Bring back vertical borders on search input in new mobile menu (light theme)
Port 9d07a31380 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-17 18:58:14 +02:00
diondiondion
457d392837 [Glitch] fix: Improve support for safe area insets
Port 59dc0bd6f3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-17 18:57:42 +02:00
Claire
fc87ce7ca1 Merge commit '98f98249ffc3de0d020fa2ed9068b1cb82168984' into glitch-soc/merge-upstream 2025-06-17 18:56:38 +02:00
Claire
98f98249ff Bump version to v4.4.0-beta.2 (#35076) 2025-06-17 16:43:17 +00:00
Claire
af157939d9 Fix sidebar rest position on mobile layout on RTL locales (#35067)
Co-authored-by: diondiondion <mail@diondiondion.com>
2025-06-17 14:41:51 +00:00
diondiondion
9d07a31380 fix: Bring back vertical borders on search input in new mobile menu (light theme) (#35072) 2025-06-17 13:46:07 +00:00
github-actions[bot]
1cb026f962 New Crowdin Translations (automated) (#35062)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-17 10:39:43 +00:00
diondiondion
59dc0bd6f3 fix: Improve support for safe area insets (#35065) 2025-06-17 09:53:14 +00:00
Claire
d2d5767f32 Merge pull request #3102 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 69f298731e
2025-06-17 10:49:13 +02:00
Eugen Rochko
56e092927d [Glitch] Change order of items in navigation panel in web UI
Port 7c4393e719 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-16 22:04:18 +02:00
diondiondion
3731738aa0 [Glitch] fix: Fix glitchy iOS media attachment drag interactions
Port 69f298731e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-16 22:04:18 +02:00
Echo
cbdd4b68c7 [Glitch] Change color of pinned carousel to avoid confusion with PMs
Port af6ee7f230 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-16 22:04:11 +02:00
Claire
d17e4d11d0 Fix dev mode for glitch-soc 2025-06-16 21:43:30 +02:00
Claire
0bb83dfe7d Merge commit '69f298731e523f7f2e97ddd6a559dfb485d4495c' into glitch-soc/merge-upstream 2025-06-16 21:07:55 +02:00
Claire
cc618f0738 Merge pull request #3101 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to c644413f8a
2025-06-16 21:03:19 +02:00
diondiondion
0237f79665 [Glitch] fix: Fix zoomed images blurry in Safari
Port ad32834ccd to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-16 20:12:28 +02:00
diondiondion
bde8aa2781 [Glitch] fix: Fix unresponsive status banner button
Port 4d29215ad3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-16 20:12:00 +02:00
diondiondion
19aa68897b [Glitch] fix: Remove redundant focus stop within status
Port ed4788a342 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-16 20:11:31 +02:00
Claire
2c751a6a19 Merge commit 'c644413f8a068490ddb8038441e5b59112e8294e' into glitch-soc/merge-upstream 2025-06-16 20:08:52 +02:00
github-actions[bot]
77862311cf New Crowdin Translations (automated) (#3041)
* New Crowdin translations

* Update no.yml

* Update simple_form.no.yml

---------

Co-authored-by: GitHub Actions <noreply@github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-06-16 20:07:18 +02:00
diondiondion
69f298731e fix: Fix glitchy iOS media attachment drag interactions (#35057) 2025-06-16 15:37:04 +00:00
Echo
af6ee7f230 Change color of pinned carousel to avoid confusion with PMs (#35056) 2025-06-16 15:32:59 +00:00
Eugen Rochko
7c4393e719 Change order of items in navigation panel in web UI (#35029) 2025-06-16 15:06:33 +00:00
Claire
013c527406 Fix vite helpers crash in development mode (#35035)
Co-authored-by: ChaosExAnima <ChaosExAnima@users.noreply.github.com>
2025-06-16 14:25:12 +00:00
Claire
c644413f8a Fix database error instead of form validation on ToS effective date conflict (#35053) 2025-06-16 13:44:48 +00:00
Matt Jankowski
ca3cc36549 Allow more flexible host/port treatment with LOCAL_DOMAIN values in tests (#35040) 2025-06-16 13:12:23 +00:00
David Roetzel
b2506478ba Add FASP follow recommendation support (#34964) 2025-06-16 10:43:27 +00:00
diondiondion
ad32834ccd fix: Fix zoomed images blurry in Safari (#35052) 2025-06-16 10:37:41 +00:00
diondiondion
4d29215ad3 fix: Fix unresponsive status banner button (#35051) 2025-06-16 07:35:32 +00:00
renovate[bot]
0544898b8b fix(deps): update dependency babel-plugin-formatjs to v10.5.39 (#35049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 06:28:26 +00:00
renovate[bot]
aef1b4f5b7 chore(deps): update dependency webauthn to v3.4.1 (#35048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 06:28:22 +00:00
github-actions[bot]
070455cad0 New Crowdin Translations (automated) (#35044)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-16 06:26:25 +00:00
Matt Jankowski
6f2aba989f Move "limited federation mode" config to x.mastodon area (#35041) 2025-06-16 06:13:03 +00:00
Essem
fe337904b2 Fix missing notification badge (#3100) 2025-06-15 21:22:06 +02:00
Claire
a27202fbbc Merge pull request #3099 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 71d4ce1c22
2025-06-14 22:06:54 +02:00
diondiondion
ed4788a342 fix: Remove redundant focus stop within status (#35037) 2025-06-13 16:47:02 +00:00
diondiondion
500fb00c2f [Glitch] fix: Ensure digits in media player time readout have a consistent width
Port 71d4ce1c22 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-13 18:15:58 +02:00
diondiondion
e20ef39da4 [Glitch] fix: Fix glitchy video player controls in Safari
Port e28f86cbe5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-13 18:15:37 +02:00
diondiondion
7ccf7fc456 [Glitch] fix: Fix unreadable text in high-contrast-mode
Port 672d411c2c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-13 18:15:15 +02:00
diondiondion
9e5e3a50a0 [Glitch] fix: Hide limited user info in hover card
Port f92ff6d699 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-13 18:14:12 +02:00
Claire
7da6b07c75 Merge commit '71d4ce1c228bab470fa9d3bcb5a130cf53665103' into glitch-soc/merge-upstream 2025-06-13 18:07:59 +02:00
diondiondion
71d4ce1c22 fix: Ensure digits in media player time readout have a consistent width (#35038) 2025-06-13 15:30:40 +00:00
Claire
e8868af079 Fix crash in StatusEdit serializer when quote posts are involved (#35036) 2025-06-13 14:59:52 +00:00
diondiondion
e28f86cbe5 fix: Fix glitchy video player controls in Safari (#35034) 2025-06-13 14:28:49 +00:00
David Roetzel
83d5016ca3 Re-instate rescuing signature errors (#35033) 2025-06-13 13:36:33 +00:00
diondiondion
672d411c2c fix: Fix unreadable text in high-contrast-mode (#35032) 2025-06-13 12:58:30 +00:00
github-actions[bot]
5ce055759f New Crowdin Translations (automated) (#35030)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-13 08:20:19 +00:00
Claire
39a1a4a8d7 Merge pull request #3098 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 825312d4b0
2025-06-13 10:11:13 +02:00
Matt Jankowski
ab7f50ce4e Reduce hard coding of LOCAL_DOMAIN env value throughout tests (#35025) 2025-06-13 07:58:22 +00:00
Claire
80af1ea434 Merge commit '825312d4b0587c0da5575f8eaabe038438746cd3' into glitch-soc/merge-upstream
Conflicts:
- `app/helpers/theme_helper.rb`:
  Conflict because of different theming system.
  Adapted upstream's changes.
- `vite.config.mts`:
  Conflict because of different theming system.
  Adapted upstream's changes.
2025-06-12 20:32:25 +02:00
Claire
7e85247645 Merge pull request #3097 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 2254f47702
2025-06-12 20:02:12 +02:00
diondiondion
f92ff6d699 fix: Hide limited user info in hover card (#35024) 2025-06-12 16:13:17 +00:00
diondiondion
3fef107791 [Glitch] fix: Avatars too large on notifications page
Port 2254f47702 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 17:54:52 +02:00
diondiondion
d5ba3aa6bd [Glitch] fix: Fix error caused by attempt to fetch non-existent quote
Port 9f94ddcd40 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 17:54:21 +02:00
Eugen Rochko
442401c47e [Glitch] Change viewport behaviour to cover
Port ccf7760205 to glitch-soc

Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 17:53:52 +02:00
Eugen Rochko
b977600e07 [Glitch] Change "Explore" to "Trending" and remove explanation banners
Port d1fb957361 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 17:50:59 +02:00
Eugen Rochko
00c34c6179 [Glitch] Remove prominent logout button from profile card in sidebar in web UI
Port 6fcbd7c17a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 17:49:11 +02:00
diondiondion
c4feceab06 [Glitch] fix: Fetch missing nested quotes
Port d4d77ace97 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 17:46:01 +02:00
diondiondion
5259623523 [Glitch] fix: Show hint for quotes hidden by filter
Port a13756148d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 17:44:34 +02:00
Claire
220b78d4a9 Merge commit '2254f47702368938bdfd0d997f2487fb2950d676' into glitch-soc/merge-upstream 2025-06-12 17:39:03 +02:00
Claire
c08d53ec76 Merge pull request #3096 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 3aed93711c
2025-06-12 17:33:40 +02:00
David Roetzel
319fbbbfac Experimental Async Refreshes API (#34918) 2025-06-12 14:54:00 +00:00
Echo
825312d4b0 Fix theme name requirement regression with efficient lookup by name (#35007)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 14:09:45 +00:00
diondiondion
2254f47702 fix: Avatars too large on notifications page (#35023) 2025-06-12 14:05:34 +00:00
diondiondion
9f94ddcd40 fix: Fix error caused by attempt to fetch non-existent quote (#35022) 2025-06-12 13:29:55 +00:00
Eugen Rochko
ccf7760205 Change viewport behaviour to cover (#34986)
Co-authored-by: diondiondion <mail@diondiondion.com>
2025-06-12 12:26:24 +00:00
Eugen Rochko
24d943fee0 Change media attachments in moderated posts to not be accessible (#34872) 2025-06-12 08:53:02 +00:00
Eugen Rochko
d1fb957361 Change "Explore" to "Trending" and remove explanation banners (#34985) 2025-06-12 08:29:42 +00:00
github-actions[bot]
0ea4267839 New Crowdin Translations (automated) (#35021)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-12 08:23:59 +00:00
Eugen Rochko
6fcbd7c17a Remove prominent logout button from profile card in sidebar in web UI (#35017) 2025-06-12 08:13:31 +00:00
Eugen Rochko
c3ca5d49cf [Glitch] Add "More" to the sidebar menu with links to mutes, blocks, and so on
Port f53bb4cd7d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 09:48:07 +02:00
Echo
3166396a16 [Glitch] Make React Spring respect animation preferences
Port 3aed93711c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 09:48:07 +02:00
diondiondion
180754a14e [Glitch] fix: Fix direction of media gallery arrows
Port 9896bed85f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 09:48:07 +02:00
diondiondion
1a732157d4 [Glitch] fix: Fix cramped layout of follower recommendations on small viewports
Port 2c828748a3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 09:48:07 +02:00
Claire
0629ea4cba [Glitch] Fix quoted posts appearing between text and media
Port 722fb1ff55 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 09:48:07 +02:00
Claire
e9c150393f [Glitch] Remove some unused CSS classes
Port 933ee420c3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 09:48:07 +02:00
Eugen Rochko
9101067154 [Glitch] Change navigation layout on small screens in web UI
Port a13b33d851 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-12 09:48:07 +02:00
Matt Jankowski
1200f70ae7 Simplify QR/OTP generation in 2FA/confirmations spec (#35019) 2025-06-12 06:54:51 +00:00
Matt Jankowski
3509064801 Reduce hard coding of instance hostname in AdminMailer specs (#35020) 2025-06-12 06:53:39 +00:00
diondiondion
d4d77ace97 fix: Fetch missing nested quotes (#35016) 2025-06-12 03:24:58 +00:00
Echo
fa33eff372 [Glitch] Prevent two composers from being shown
Port 8cf246e4d3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-11 23:02:09 +02:00
Claire
86a7a1ef3b Merge commit '3aed93711c0118ab68fa09f4d3ac2635cb3344b8' into glitch-soc/merge-upstream 2025-06-11 22:52:21 +02:00
Claire
14c5e3433c Merge pull request #3095 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 629bb74451
2025-06-11 22:51:03 +02:00
Echo
1190780d79 [Glitch] Ensure featured carousel respects tags
Port c543e823ab to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-11 18:59:45 +02:00
Claire
533af5dcee [Glitch] Fix crash in /about when server returns cached rules without translations attribute
Port c727701839 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-11 18:58:09 +02:00
Eugen Rochko
3c7da05f85 [Glitch] Fix broken colors in some themed SVGs in web UI
Port 1824b1fd29 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-11 18:57:30 +02:00
Eugen Rochko
7c1178ab84 [Glitch] Fix wrong dimensions on blurhash previews of news articles in web UI
Port a2a6117143 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-11 18:57:07 +02:00
Eugen Rochko
fb532ab172 [Glitch] Fix wrong styles on action bar in media modal in web UI
Port f3d60a4a6f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-11 18:56:44 +02:00
Claire
aa6d1d4ac1 Merge commit '629bb74451242060298c6fb305c5337002379cc2' into glitch-soc/merge-upstream 2025-06-11 18:55:03 +02:00
Echo
3aed93711c Make React Spring respect animation preferences (#35018) 2025-06-11 16:51:55 +00:00
Eugen Rochko
f53bb4cd7d Add "More" to the sidebar menu with links to mutes, blocks, and so on (#34987) 2025-06-11 16:12:04 +00:00
diondiondion
9896bed85f fix: Fix direction of media gallery arrows (#35014) 2025-06-11 15:17:14 +00:00
diondiondion
2c828748a3 fix: Fix cramped layout of follower recommendations on small viewports (#34967) 2025-06-11 15:15:12 +00:00
David Roetzel
1623d54ec0 Start local prometheus_exporter server only in puma/sidekiq startup (#35005) 2025-06-11 13:37:59 +00:00
Claire
722fb1ff55 Fix quoted posts appearing between text and media (#35011) 2025-06-11 13:17:42 +00:00
Claire
933ee420c3 Remove some unused CSS classes (#35012) 2025-06-11 13:17:07 +00:00
renovate[bot]
0cdf11d6ad fix(deps): update dependency postcss-preset-env to v10.2.3 (#35009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 12:12:53 +00:00
Eugen Rochko
a13b33d851 Change navigation layout on small screens in web UI (#34910) 2025-06-11 11:55:43 +00:00
Echo
8cf246e4d3 Prevent two composers from being shown (#35006) 2025-06-11 11:12:39 +00:00
Matt Jankowski
629bb74451 Replace selenium-webdriver with playwright (#34867) 2025-06-10 16:33:46 +00:00
renovate[bot]
b8cc9b3290 fix(deps): update dependency postcss-preset-env to v10.2.2 (#34998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 14:12:56 +00:00
renovate[bot]
2c085ea044 fix(deps): update dependency core-js to v3.43.0 (#34999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 14:12:53 +00:00
renovate[bot]
bb89a64af0 chore(deps): update dependency http to '~> 5.3.0' (#34994)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 14:12:44 +00:00
Echo
c543e823ab Ensure featured carousel respects tags (#34995) 2025-06-10 13:28:41 +00:00
Claire
7a7e0ba4cd Add basic support for remote attachments with multiple media types (#34996) 2025-06-10 13:26:29 +00:00
Claire
c727701839 Fix crash in /about when server returns cached rules without translations attribute (#34997) 2025-06-10 13:25:24 +00:00
Eugen Rochko
1824b1fd29 Fix broken colors in some themed SVGs in web UI (#34988) 2025-06-10 08:36:42 +00:00
renovate[bot]
1bf8a642f0 fix(deps): update dependency sass to v1.89.2 (#34993)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 08:36:27 +00:00
Matt Jankowski
dc2cfd50a0 Fix Style/OptionalBooleanParameter cop (#34968) 2025-06-10 08:17:35 +00:00
Eugen Rochko
a2a6117143 Fix wrong dimensions on blurhash previews of news articles in web UI (#34990) 2025-06-10 08:11:46 +00:00
renovate[bot]
24803db2bc fix(deps): update dependency @vitejs/plugin-react to v4.5.2 (#34992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 08:11:21 +00:00
renovate[bot]
90183b6c27 chore(deps): update dependency rubocop to v1.76.1 (#34991)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 07:55:27 +00:00
github-actions[bot]
c3022fe10f New Crowdin Translations (automated) (#34973)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-10 07:52:52 +00:00
Eugen Rochko
f3d60a4a6f Fix wrong styles on action bar in media modal in web UI (#34989) 2025-06-10 07:42:29 +00:00
renovate[bot]
1dd8a99d9f fix(deps): update dependency use-debounce to v10.0.5 (#34982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 07:38:19 +00:00
renovate[bot]
66a42c11ba fix(deps): update dependency rollup-plugin-visualizer to v6.0.3 (#34974)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 07:38:00 +00:00
renovate[bot]
132f32dd70 chore(deps): update dependency libvips to v8.17.0 (#34956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 07:37:16 +00:00
Claire
e557769a3c Remove broken link to /start/share in welcome e-mail (#34962) 2025-06-08 13:58:53 +00:00
Claire
87e72fb8e1 Merge pull request #3093 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to d887790e86
2025-06-06 23:42:55 +02:00
diondiondion
49071fd4c0 [Glitch] fix: Fix indentation of quote posts in threads
Port c92e21813e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-06 18:20:19 +02:00
PGray
9df67e0c74 [Glitch] fix: update search column input on param change
Port 076005eae2 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-06 18:20:02 +02:00
Claire
3b13487e8f [Glitch] Rewrite AccountNote as Typescript functional component
Port 68810643d8 to glitch-soc

Co-authored-by: diondiondion <mail@diondiondion.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-06 18:19:07 +02:00
Claire
564d74ea36 Merge commit 'd887790e86124493df850e9bc52e1e059207b523' into glitch-soc/merge-upstream 2025-06-06 18:16:52 +02:00
Matt Jankowski
d887790e86 Update SafeReblogInsert concern to match Rails 8 method (#34966) 2025-06-06 15:25:09 +00:00
Claire
d0c6f30378 Add fasp queue to sidekiq queue system check (#34965) 2025-06-06 15:23:16 +00:00
Claire
ba75ba3adc Change github workflow for chromatic to only run on official Mastodon repository (#34963) 2025-06-06 12:57:41 +00:00
diondiondion
c92e21813e fix: Fix indentation of quote posts in threads (#34961) 2025-06-06 12:37:56 +00:00
PGray
076005eae2 fix: update search column input on param change (#34951) 2025-06-06 12:25:38 +00:00
Claire
619b9bc2d8 Merge pull request #3092 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to b10fde673d
2025-06-06 13:53:27 +02:00
Claire
ac68d7e471 Merge commit 'b10fde673d4e703b53f43691419a6e91672daf9e' into glitch-soc/merge-upstream
Conflicts:
- `tsconfig.json`:
  Upstream added config for storybook files, while glitch-soc had an addition
  for the glitch flavor on the last line.
  Added upstream's new config.
- `yarn.lock`:
  Upstream added a new dependency to an `eslint` plugin textually adjacent to
  a couple glitch-only-dependencies.
  Added upstream's new dependency.
2025-06-06 13:32:20 +02:00
Claire
00154f3f92 Merge pull request #3091 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 989ca63b59
2025-06-06 13:26:34 +02:00
Claire
4b22aa93a2 Merge commit '989ca63b596c9006c5606edd4e66498e29e83489' into glitch-soc/merge-upstream 2025-06-06 12:56:00 +02:00
Renaud Chaput
b10fde673d Update rack to 3.1.16 (#34959) 2025-06-06 10:15:55 +00:00
Claire
68810643d8 Rewrite AccountNote as Typescript functional component (#34925)
Co-authored-by: diondiondion <mail@diondiondion.com>
2025-06-06 09:59:26 +00:00
diondiondion
f2cfa4f482 feat: Add Storybook for component documentation, testing, and development (#34907)
Co-authored-by: Echo <ChaosExAnima@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2025-06-06 09:27:29 +00:00
Claire
989ca63b59 Remove inbound_quotes feature flag (#34958) 2025-06-06 08:25:59 +00:00
Matt Jankowski
f2cdbefa3c Align versions in templates to current/plausible values (#34952) 2025-06-06 07:42:57 +00:00
Claire
86627624f1 Fix quote post streaming edge cases (#34957) 2025-06-06 07:32:24 +00:00
github-actions[bot]
c09f9a93f1 New Crowdin Translations (automated) (#34953)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-06 07:15:31 +00:00
renovate[bot]
963f4977d6 fix(deps): update dependency postcss-preset-env to v10.2.1 (#34947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 06:29:40 +00:00
renovate[bot]
5fde019e39 chore(deps): update dependency ruby-vips to v2.2.4 (#34949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 06:29:19 +00:00
Claire
ad8f984cef Merge pull request #3090 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 520974e052
2025-06-05 19:34:17 +02:00
Claire
af314a833d [Glitch] Add ability to filter quote posts in home timeline
Port 520974e052 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-05 18:14:55 +02:00
Echo
9a649a2072 [Glitch] Add a way to easily unpin profiles from the featured account area
Port 1fdcaaebbb to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-05 18:09:42 +02:00
Claire
802ade62ce [Glitch] Fix account note textarea being interactable before the relationship gets fetched
Port 375add0c83 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-05 18:09:16 +02:00
diondiondion
4dbbe520fb [Glitch] fix: Fix broken audio player layout in Safari
Port 1dafd8c9dd to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-05 18:08:47 +02:00
diondiondion
6bb29dec13 [Glitch] fix: Fix Safari volume bug
Port 6637ecb460 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-05 18:08:12 +02:00
Echo
a6a35ad1ce [Glitch] Ensure carousel slides don't overflow
Port e9f197740d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-06-05 18:07:37 +02:00
Claire
2085b0b53a Merge commit '520974e05211e988b0447f7f29e88798b1794bcf' into glitch-soc/merge-upstream
Conflicts:
- `app/serializers/rest/status_serializer.rb`:
  Not a real conflict, just glitch-soc code textually adjacent to code added
  upstream.
2025-06-05 18:02:40 +02:00
Claire
520974e052 Add ability to filter quote posts in home timeline (#34946) 2025-06-05 15:36:51 +00:00
Claire
3d474807bf Change “legacy” non-fast-tracked quote posts to not be displayed as such (#34945) 2025-06-05 13:53:57 +00:00
renovate[bot]
09208eafa4 chore(deps): update yarn to v4.9.2 (#34916)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 13:37:43 +00:00
renovate[bot]
25c4574480 chore(deps): update dependency opentelemetry-instrumentation-faraday to '~> 0.27.0' (#34917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 13:37:37 +00:00
Matt Jankowski
e2c5a2abaa Fix Style/FetchEnvVar cop in repo.rake (#34903)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-06-05 13:37:33 +00:00
renovate[bot]
a80f77a996 fix(deps): update dependency pino-http to v10.5.0 (#34920)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 13:36:22 +00:00
Matt Jankowski
1297ad759e Update rubocop to version 1.76.0 (#34926) 2025-06-05 12:09:05 +00:00
Echo
1fdcaaebbb Add a way to easily unpin profiles from the featured account area (#34931) 2025-06-05 10:23:01 +00:00
Claire
375add0c83 Fix account note textarea being interactable before the relationship gets fetched (#34932) 2025-06-05 08:49:26 +00:00
renovate[bot]
a4bc438010 chore(deps): update dependency annotaterb to v4.15.0 (#34870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 07:47:34 +00:00
Matt Jankowski
1d152d2181 Replace local vars with let in JS-enabled system specs (#34905) 2025-06-05 07:08:52 +00:00
github-actions[bot]
250c3b0c1f New Crowdin Translations (automated) (#34939)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-06-05 07:05:25 +00:00
diondiondion
1dafd8c9dd fix: Fix broken audio player layout in Safari, #34930 (#34933) 2025-06-04 15:34:13 +00:00
diondiondion
6637ecb460 fix: Fix Safari volume bug, #34797 (#34929) 2025-06-04 14:35:29 +00:00
Echo
e9f197740d Ensure carousel slides don't overflow (#34927) 2025-06-04 14:29:34 +00:00
785 changed files with 17855 additions and 9385 deletions

View File

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

View File

@@ -47,8 +47,8 @@ body:
attributes:
label: Mastodon version
description: |
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
placeholder: v4.3.0
This is displayed at the bottom of the About page, eg. `v4.4.0-beta.1`
placeholder: v4.4.0-beta.1
validations:
required: true
- type: input
@@ -56,7 +56,7 @@ body:
label: Browser name and version
description: |
What browser are you using when getting this bug? Please specify the version as well.
placeholder: Firefox 131.0.0
placeholder: Firefox 139.0.0
validations:
required: true
- type: input
@@ -64,7 +64,7 @@ body:
label: Operating system
description: |
What OS are you running? Please specify the version as well.
placeholder: macOS 15.0.1
placeholder: macOS 15.5
validations:
required: true
- type: textarea

View File

@@ -48,8 +48,8 @@ body:
attributes:
label: Mastodon version
description: |
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
placeholder: v4.3.0
This is displayed at the bottom of the About page, eg. `v4.4.0-beta.1`
placeholder: v4.4.0-beta.1
validations:
required: false
- type: textarea
@@ -59,7 +59,7 @@ body:
Any additional technical details you may have, like logs or error traces
value: |
If this is happening on your own Mastodon server, please fill out those:
- Ruby version: (from `ruby --version`, eg. v3.4.1)
- Node.js version: (from `node --version`, eg. v20.18.0)
- Ruby version: (from `ruby --version`, eg. v3.4.4)
- Node.js version: (from `node --version`, eg. v22.16.0)
validations:
required: false

View File

@@ -49,7 +49,7 @@ body:
label: Mastodon version
description: |
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
placeholder: v4.3.0
placeholder: v4.4.0-beta.1
validations:
required: false
- type: textarea
@@ -59,9 +59,9 @@ body:
Details about your environment, like how Mastodon is deployed, if containers are used, version numbers, etc.
value: |
Please at least include those informations:
- Operating system: (eg. Ubuntu 22.04)
- Ruby version: (from `ruby --version`, eg. v3.4.1)
- Node.js version: (from `node --version`, eg. v20.18.0)
- Operating system: (eg. Ubuntu 24.04.2)
- Ruby version: (from `ruby --version`, eg. v3.4.4)
- Node.js version: (from `node --version`, eg. v22.16.0)
validations:
required: false
- type: textarea

View File

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

41
.github/workflows/chromatic.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: 'Chromatic'
on:
push:
branches-ignore:
- renovate/*
- stable-*
paths:
- 'package.json'
- 'yarn.lock'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.css'
- '**/*.scss'
- '.github/workflows/chromatic.yml'
jobs:
chromatic:
name: Run Chromatic
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Build Storybook
run: yarn build-storybook
- name: Run Chromatic
uses: chromaui/action@v12
with:
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
zip: true
storybookBuildDir: 'storybook-static'

View File

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

View File

@@ -332,6 +332,21 @@ jobs:
- name: Load database schema
run: './bin/rails db:create db:schema:load db:seed'
- name: Cache Playwright Chromium browser
id: playwright-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Chromium browser (with deps)
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn run playwright install --with-deps chromium
- name: Install Playwright Chromium browser deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: yarn run playwright install-deps chromium
- run: bin/rspec spec/system --tag streaming --tag js
- name: Archive logs

3
.gitignore vendored
View File

@@ -75,3 +75,6 @@ docker-compose.override.yml
# Ignore local-only rspec configuration
.rspec-local
*storybook.log
storybook-static

2
.nvmrc
View File

@@ -1 +1 @@
22.16
22.17

View File

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

View File

@@ -1,3 +1,6 @@
---
Naming/BlockForwarding:
EnforcedStyle: explicit
Naming/PredicateMethod:
Enabled: false

View File

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

View File

@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.75.8.
# using RuboCop version 1.77.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -28,26 +28,12 @@ Metrics/PerceivedComplexity:
Max: 27
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedVars.
# Configuration parameters: AllowedVars, DefaultToNil.
Style/FetchEnvVar:
Exclude:
- 'config/initializers/2_limited_federation_mode.rb'
- 'config/initializers/paperclip.rb'
- 'lib/tasks/repo.rake'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
Style/GuardClause:
Enabled: false
# Configuration parameters: AllowedMethods.
# AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter:
Exclude:
- 'app/lib/admin/system_check/message.rb'
- 'app/lib/request.rb'
- 'app/lib/webfinger.rb'
- 'app/services/block_domain_service.rb'
- 'app/services/fetch_resource_service.rb'
- 'app/workers/domain_block_worker.rb'
- 'app/workers/unfollow_follow_worker.rb'

31
.storybook/main.ts Normal file
View File

@@ -0,0 +1,31 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../app/javascript/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-docs',
'@storybook/addon-a11y',
'@storybook/addon-vitest',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
staticDirs: [
'./static',
// We need to manually specify the assets because of the symlink in public/sw.js
...[
'avatars',
'emoji',
'headers',
'sounds',
'badge.png',
'loading.gif',
'loading.png',
'oops.gif',
'oops.png',
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
],
};
export default config;

7
.storybook/manager.ts Normal file
View File

@@ -0,0 +1,7 @@
import { addons } from 'storybook/manager-api';
import theme from './storybook-theme';
addons.setConfig({
theme,
});

View File

@@ -0,0 +1,18 @@
<style>
/* Increase docs font size */
.sbdocs.sbdocs-content :where(p:not(.sb-anchor, .sb-unstyled, .sb-unstyled p)),
.sbdocs.sbdocs-content :where(li:not(.sb-anchor, .sb-unstyled, .sb-unstyled li)) {
font-size: 1.0666rem; /* 17px */
line-height: 1.585; /* 27px */
}
.sbdocs.sbdocs-content :where(p:not(.sb-anchor, .sb-unstyled, .sb-unstyled p)) code,
.sbdocs.sbdocs-content :where(li:not(.sb-anchor, .sb-unstyled, .sb-unstyled li)) code {
font-size: 0.875rem; /* ~15px */
}
/* Bring numbers back for ordered lists */
ol {
list-style: revert !important;
}
</style>

146
.storybook/preview.tsx Normal file
View File

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

View File

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

View File

@@ -0,0 +1,7 @@
// The addon package.json incorrectly exports types, so we need to override them here.
// See: https://github.com/storybookjs/storybook/blob/v9.0.4/code/addons/vitest/package.json#L70-L76
declare module '@storybook/addon-vitest/vitest-plugin' {
export * from '@storybook/addon-vitest/dist/vitest-plugin/index';
}
export {};

View File

@@ -0,0 +1,7 @@
import { create } from 'storybook/theming';
export default create({
base: 'light',
brandTitle: 'Mastodon Storybook',
brandImage: 'https://joinmastodon.org/logos/wordmark-black-text.svg',
});

View File

@@ -0,0 +1,8 @@
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';
import { setProjectAnnotations } from '@storybook/react-vite';
import * as projectAnnotations from './preview';
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);

View File

@@ -2,12 +2,12 @@
All notable changes to this project will be documented in this file.
## [4.4.0] - UNRELEASED
## [4.4.0] - 2025-07-08
### Added
- **Add “Followers you know” widget to user profiles and hover cards** (#34652, #34678, #34681, #34697, #34699, #34769, #34774 and #34914 by @diondiondion)
- **Add featured tab to profiles on web UI and rework pinned posts** (#34405, #34483, #34491, #34754, #34855, #34858, #34868, and #34869 by @ChaosExAnima, @ClearlyClaire, @Gargron, and @diondiondion)
- **Add featured tab to profiles on web UI and rework pinned posts** (#34405, #34483, #34491, #34754, #34855, #34858, #34868, #34869, #34927, #34995, #35056 and #34931 by @ChaosExAnima, @ClearlyClaire, @Gargron, and @diondiondion)
- Add endorsed accounts to featured tab in web UI (#34421 and #34568 by @Gargron)\
This also includes the following new REST API endpoints:
- `GET /api/v1/accounts/:id/endorsements`: https://docs.joinmastodon.org/methods/accounts/#endorsements
@@ -19,14 +19,14 @@ All notable changes to this project will be documented in this file.
- `POST /api/v1/tags/:id/unfeature`: https://docs.joinmastodon.org/methods/tags/#unfeature
- Add reminder when about to post without alt text in web UI (#33760 and #33784 by @Gargron)
- Add a warning in Web UI when composing a post when the selected and detected language are different (#33042, #33683, #33700, #33724, #33770, and #34193 by @ClearlyClaire and @Gargron)
- Add ability to reorder and translate server rules (#34637, #34737, #34494, #34756, and #34820 by @ChaosExAnima and @ClearlyClaire)\
- Add support for verifying and displaying remote quote posts (#34370, #34481, #34510, #34551, #34480, #34479, #34553, #34584, #34623, #34738, #34766, #34770, #34772, #34773, #34786, #34790, #34864, #34957, #34961, #35016, #35022, #35036, #34946, #34945 and #34958 by @ClearlyClaire and @diondiondion)\
Support for verifying remote quotes according to [FEP-044f](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md) and displaying them in the Web UI has been implemented.\
Quoting other people is not implemented yet, and it is currently not possible to mark your own posts as allowing quotes. However, a new “Who can quote” setting has been added to the “Posting defaults” section of the user settings. This setting allows you to set a default that will be used for new posts made on Mastodon 4.5 and newer, when quote posts will be fully implemented.\
In the REST API, quote posts are represented by a new `quote` attribute on `Status` and `StatusEdit` entities: https://docs.joinmastodon.org/entities/StatusEdit/#quote https://docs.joinmastodon.org/entities/Status/#quote
- Add ability to reorder and translate server rules (#34637, #34737, #34494, #34756, #34820, #34997, #35170, #35174 and #35174 by @ChaosExAnima and @ClearlyClaire)\
Rules are now shown in the users language, if a translation has been set.\
In the REST API, `Rule` entities now have a new `translations` attribute: https://docs.joinmastodon.org/entities/Rule/#translations
- Add emoji from Twemoji 15.1.0, including in the emoji picker/completion (#33395, #34321, #34620, and #34677 by @ChaosExAnima, @ClearlyClaire, @TheEssem, and @eramdam)
- Add experimental support for verifying and displaying remote quote posts (#34370, #34481, #34510, #34551, #34480, #34479, #34553, #34584, #34623, #34738, #34766, #34770, #34772, #34773, #34786, #34790, and #34864 by @ClearlyClaire and @diondiondion)\
Support for verifying remote quotes according to [FEP-044f](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md) and displaying them in the Web UI has been implemented. Such quotes are currently only processed if the `inbound_quotes` experimental feature is enabled (`EXPERIMENTAL_FEATURES=inbound_quotes`).\
Quoting other people is not implemented yet, and it is currently not possible to mark your own posts as allowing quotes. However, a new “Who can quote” setting has been added to the “Posting defaults” section of the user settings. This setting allows you to set a default that will be used for new posts made on Mastodon 4.5 and newer, when quote posts will be fully implemented.\
In the REST API, quote posts are represented by a new `quote` attribute on `Status` and `StatusEdit` entities: https://docs.joinmastodon.org/entities/StatusEdit/#quote https://docs.joinmastodon.org/entities/Status/#quote
- Add option to remove account from followers in web UI (#34488 by @Gargron)
- Add relationship tags to profiles and hover cards in web UI (#34467 and #34792 by @Gargron and @diondiondion)
- Add ability to open posts in a new tab by middle-clicking in web UI (#32988, #33106, #33419, and #34700 by @ClearlyClaire, @Gargron, and @tribela)
@@ -38,8 +38,11 @@ All notable changes to this project will be documented in this file.
Server administrators can now chose to opt in to transmit referrer information when following an external link. Only the domain name is transmitted, not the referrer path.
- Add double tap to zoom and swipe to dismiss to media modal in web UI (#34210 by @Gargron)
- Add link from Web UI for Hashtags to the Moderation UI (#31448 by @ThisIsMissEm)
- **Add terms of service** (#33055, #33233, #33230, #33703, #33699, #33994, #33993, #34105, #34122, #34200, and #34527 by @ClearlyClaire, @Gargron, @mjankowski, and @oneiros)\
Server administrators can now fill in Terms of Service, optionally using a provided template.
- **Add terms of service** (#33055, #33233, #33230, #33703, #33699, #33994, #33993, #34105, #34122, #34200, #34527, #35053, #35115, #35126, #35127 and #35233 by @ClearlyClaire, @Gargron, @mjankowski, and @oneiros)\
Server administrators can now fill in Terms of Service and notify their users of upcoming changes.
- Add optional bulk mailer settings (#35191 and #35203 by @oneiros)\
This adds the optional environment variables `BULK_SMTP_PORT`, `BULK_SMTP_SERVER`, `BULK_SMTP_LOGIN` and so on analogous to `SMTP_PORT`, `SMTP_SERVER`, `SMTP_LOGIN` and related SMTP configuration environment variables.\
When `BULK_SMTP_SERVER` is set, this group of variables is used instead of the regular ones for sending announcement notification emails and Terms of Service notification emails.
- **Add age verification on sign-up** (#34150, #34663, and #34636 by @ClearlyClaire and @Gargron)\
Server administrators now have a setting to set a minimum age requirement for creating a new server, asking users for their date of birth. The date of birth is checked against the minimum age requirement server-side but not stored.\
The following REST API changes have been made to accommodate this:
@@ -48,10 +51,12 @@ All notable changes to this project will be documented in this file.
- Add ability to dismiss alt text badge by tapping it in web UI (#33737 by @Gargron)
- Add loading indicator to timeline gap indicators in web UI (#33762 by @Gargron)
- Add interaction modal when trying to interact with a poll while logged out (#32609 by @ThisIsMissEm)
- **Add experimental FASP support** (#34031, #34415, and #34765 by @oneiros)\
- **Add experimental FASP support** (#34031, #34415, #34765, #34965, #34964, #34033, #35218, #35262 and #35263 by @oneiros)\
This is a first step towards supporting “Fediverse Auxiliary Service Providers” (https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications). This is mostly interesting to developers who would like to implement their own FASP, but also includes the capability to share data with a discovery provider (see https://www.fediscovery.org).
- Add ability for admins to send announcements to all users via email (#33928 and #34411 by @ClearlyClaire)\
This is meant for critical announcements only, as this will potentially send a lot of emails and cannot be opted out of by users.
- Add Server Moderation Notes (#31529 by @ThisIsMissEm)
- Add loading spinner to “Post” button when sending a post (#35153 by @diondiondion)
- Add option to use system scrollbar styling (#32117 by @vmstan)
- Add hover cards to follow suggestions (#33749 by @ClearlyClaire)
- Add `t` hotkey for post translations (#33441 by @ClearlyClaire)
@@ -59,8 +64,9 @@ All notable changes to this project will be documented in this file.
- Add dropdown menu with quick actions to lists of accounts in web UI (#34391, #34709, and #34767 by @Gargron, @diondiondion, and @mkljczk)
- Add support for displaying “year in review” notification in web UI (#32710, #32765, #32709, #32807, #32914, #33148, and #33882 by @Gargron and @mjankowski)\
Note that the notification is currently not generated automatically, and at the moment requires a manual undocumented administrator action.
- Add experimental support for receiving HTTP Message Signatures (RFC9421) (#34814 by @oneiros)\
- Add experimental support for receiving HTTP Message Signatures (RFC9421) (#34814, #35033, #35109 and #35278 by @oneiros)\
For now, this needs to be explicitly enabled through the `http_message_signatures` feature flag (`EXPERIMENTAL_FEATURES=http_message_signatures`). This currently only covers verifying such signatures (inbound HTTP requests), not issuing them (outbound HTTP requests).
- Add experimental Async Refreshes API (#34918 by @oneiros)
- Add experimental server-side feature to fetch remote replies (#32615, #34147, #34149, #34151, #34615, #34682, and #34702 by @ClearlyClaire and @sneakers-the-rat)\
This experimental feature causes the server to recursively fetch replies in background tasks whenever a user opens a remote post. This happens asynchronously and the client is currently not notified of the existence of new replies, which will thus only be displayed the next time this posts context gets requested.\
This feature needs to be explicitly enabled server-side by setting `FETCH_REPLIES_ENABLED` environment variable to `true`.
@@ -85,7 +91,7 @@ All notable changes to this project will be documented in this file.
- Add `og:locale` to expose status language in OpenGraph previews (#34012 by @ThisIsMissEm)
- Add `-skip-filled-timeline` option to `tootctl feed build` to skip half-filled feeds (#33844 by @ClearlyClaire)
- Add support for changing the base Docker registry with the `BASE_REGISTRY` `ARG` (#33712 by @wolfspyre)
- Add an optional metric exporter (#33734, #33840, #34172, #34192, 34223)\
- Add an optional metric exporter (#33734, #33840, #34172, #34192, #34223, and #35005 by @oneiros and @renchap)\
Optionally enable the `prometheus_exporter` ruby gem (see https://github.com/discourse/prometheus_exporter) to collect and expose metrics. See the documentation for all the details: https://docs.joinmastodon.org/admin/config/#prometheus
- Add `attribution_domains` attribute to `PATCH /api/v1/accounts/update_credentials` (#32730 by @c960657)\
This is documented at https://docs.joinmastodon.org/methods/accounts/#update_credentials
@@ -111,26 +117,31 @@ All notable changes to this project will be documented in this file.
### Changed
- Change design of navigation panel in Web UI, change layout on narrow screens (#34910, #34987, #35017, #34986, #35029, #35065, #35067, #35072, #35074, #35075, #35101, #35173, #35183, #35193 and #35225 by @ClearlyClaire, @Gargron, and @diondiondion)
- Change design of lists in web UI (#32881, #33054, and #33036 by @Gargron)
- Change design of edit media modal in web UI (#33516, #33702, #33725, #33725, #33771, and #34345 by @Gargron)
- Change design of audio player in web UI (#34520, #34740, and #34865 by @ClearlyClaire, @Gargron, and @diondiondion)
- Change design of audio player in web UI (#34520, #34740, #34865, #34929, #34933, and #35034 by @ClearlyClaire, @Gargron, and @diondiondion)
- Change design of interaction modal in web UI (#33278 by @Gargron)
- Change list timelines to reflect added and removed users retroactively (#32930 by @Gargron)
- Change account search to be more forgiving of spaces (#34455 by @Gargron)
- Change unfollow button label from “Mutual” to “Unfollow” in web UI (#34392 by @Gargron)
- Change “Specific people” to “Private mention” in menu in web UI (#33963 by @Gargron)
- Change "Explore" to "Trending" and remove explanation banners (#34985 by @Gargron)
- Change media attachments of moderated posts to not be accessible (#34872 by @Gargron)
Moderators will still be able to access them while they are kept, but they won't be accessible to the public in the meantime.
- Change language names in compose box language picker to be localized (#33402 by @c960657)
- Change onboarding flow in web UI (#32998, #33119, and #33471 by @ClearlyClaire and @Gargron)
- Change onboarding flow in web UI (#32998, #33119, #33471 and #34962 by @ClearlyClaire and @Gargron)
- Change Advanced Web UI to use the new main menu instead of the “Getting started” column (#35117 by @diondiondion)
- Change emoji categories in admin interface to be ordered by name (#33630 by @ShadowJonathan)
- Change design of rich text elements in web UI (#32633 by @Gargron)
- Change wording of “single choice” to “pick one” in poll authoring form (#32397 by @ThisIsMissEm)
- Change returned favorite and boost counts to use those provided by the remote server, if available (#32620, #34594, #34618, and #34619 by @ClearlyClaire and @sneakers-the-rat)
- Change label of favourite notifications on private mentions (#31659 by @ClearlyClaire)
- Change wording of "discard draft?" confirmation dialogs (#35192 by @diondiondion)
- Change `libvips` to be enabled by default in place of ImageMagick (#34741 and #34753 by @ClearlyClaire and @diondiondion)
- Change avatar and header size limits from 2MB to 8MB when using libvips (#33002 by @Gargron)
- Change search to use query params in web UI (#32949 and #33670 by @ClearlyClaire and @Gargron)
- Change build system from Webpack to Vite (#34454, #34450, #34758, #34768, #34813, #34808, #34837, and #34732 by @ChaosExAnima, @ClearlyClaire, @mjankowski, and @renchap)\
One known limitation is that themes main style file needs to have a very specific file name: `app/javascript/styles/:name.scss` where `:name` is the name of the theme in `config/themes.yml`
- Change build system from Webpack to Vite (#34454, #34450, #34758, #34768, #34813, #34808, #34837, #34732, #35007, #35035 and #35177 by @ChaosExAnima, @ClearlyClaire, @mjankowski, and @renchap)
- Change account creation API to forbid creation from user tokens (#34828 by @ThisIsMissEm)
- Change `/api/v2/instance` to be enabled without authentication when limited federation mode is enabled (#34576 by @ClearlyClaire)
- Change `DEFAULT_LOCALE` to not override unauthenticated users browser language (#34535 by @ClearlyClaire)\
@@ -198,22 +209,44 @@ All notable changes to this project will be documented in this file.
- Fix not being able to scroll dropdown on touch devices in web UI (#34873 by @Gargron)
- Fix inconsistent filtering of silenced accounts for other silenced accounts (#34863 by @ClearlyClaire)
- Fix update checker listing updates older or equal to current running version (#33906 by @ClearlyClaire)
- Fix clicking a status multiple times causing duplicate entries in browser history (#35118 by @ClearlyClaire)
- Fix Alt text button submitting form in moderation interface (#35147 by @ClearlyClaire)
- Fix Firefox sometimes not updating spellcheck language in textarea (#35148 by @ClearlyClaire)
- Fix `NoMethodError` in edge case of emoji cache handling (#34749 by @dariusk)
- Fix handling of inlined `featured` collections in ActivityPub actor objects (#34789 and #34811 by @ClearlyClaire)
- Fix long link names in admin sidebar being truncated (#34727 by @diondiondion)
- Fix admin dashboard crash on specific Elasticsearch connection errors (#34683 by @ClearlyClaire)
- Fix OIDC account creation failing for long display names (#34639 by @defnull)
- Fix use of the deprecated `/api/v1/instance` endpoint in the moderation interface (#34613 by @renchap)
- Fix inaccessible Clear search button (#35152 and #35281 by @diondiondion)
- Fix search operators sometimes getting lost (#35190 by @ClearlyClaire)
- Fix directory scroll position reset (#34560 by @przucidlo)
- Fix needlessly complex SVG paths for oEmbed and logo (#34538 by @edent)
- Fix avatar sizing with long account name in some UI elements (#34514 by @gomasy)
- Fix empty menu section in status dropdown (#34431 by @ClearlyClaire)
- Fix the delete suggestion button not working (#34396 and #34398 by @ClearlyClaire and @renchap)
- Fix popover/dialog backgrounds not being blurred on older Webkit browsers (#35220 by @diondiondion)
- Fix radio buttons not always being correctly centered (#34389 by @ChaosExAnima)
- Fix visual glitches with adding post filters (#34387 by @ChaosExAnima)
- Fix bugs with upload progress (#34325 by @ChaosExAnima)
- Fix being unable to hide controls in full screen video in web UI (#34308 by @Gargron)
- Fix extra space under left-indented vertical videos (#34313 by @ClearlyClaire)
- Fix glitchy iOS media attachment drag interactions (#35057 by @diondiondion)
- Fix zoomed images being blurry in Safari (#35052 by @diondiondion)
- Fix redundant focus stop within status component in Web UI and make focus style more noticeable (#35037, #35051, #35096, #35150 and #35251 by @diondiondion)
- Fix digits in media player time readout not having a consistent width (#35038 by @diondiondion)
- Fix wrong text color for Open in advanced web interface banner in high-contrast theme (#35032 by @diondiondion)
- Fix hover card for limited accounts not hiding information as expected (#35024 by @diondiondion)
- Fix some animations not respecting the reduced animation preferences (#35018 by @ChaosExAnima)
- Fix direction of media gallery arrows in RTL locales (#35014 by @diondiondion)
- Fix cramped layout of follower recommendations on small viewports (#34967 and #35023 by @diondiondion)
- Fix two composers being shown at the same time in some cases (#35006 by @ChaosExAnima)
- Fix handling of remote attachments with multiple media types (#34996 by @ClearlyClaire)
- Fix broken colors in some themed SVGs in web UI (#34988 by @Gargron)
- Fix wrong dimensions on blurhash previews of news articles in web UI (#34990 by @Gargron)
- Fix wrong styles on action bar in media modal in web UI (#34989 by @Gargron)
- Fix search column input not updating on param change (#34951 by @PGrayCS)
- Fix account note textarea being interactable before the relationship gets fetched (#34932 by @ClearlyClaire)
- Fix SASS deprecation notices (#34278 by @ChaosExAnima)
- Fix display of failed-to-load image attachments in web UI (#34217 by @Gargron)
- Fix duplicate REST API requests on submitting account personal note with ctrl+enter (#34213 by @ClearlyClaire)

View File

@@ -186,7 +186,7 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.16.1
ARG VIPS_VERSION=8.17.0
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download

View File

@@ -53,7 +53,7 @@ gem 'fastimage'
gem 'hiredis', '~> 0.6'
gem 'hiredis-client'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 5.2.0'
gem 'http', '~> 5.3.0'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.7.0', require: false
gem 'i18n'
@@ -110,8 +110,8 @@ group :opentelemetry do
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.26.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.27.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
@@ -137,7 +137,7 @@ group :test do
# Browser integration testing
gem 'capybara', '~> 3.39'
gem 'selenium-webdriver'
gem 'capybara-playwright-driver'
# Used to reset the database between system tests
gem 'database_cleaner-active_record'

View File

@@ -90,7 +90,9 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
android_key_attestation (0.3.0)
annotaterb (4.14.0)
annotaterb (4.16.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.3.2)
@@ -111,15 +113,15 @@ GEM
aws-eventstream (~> 1, >= 1.0.2)
azure-blob (0.5.8)
rexml
base64 (0.2.0)
base64 (0.3.0)
bcp47_spec (0.2.1)
bcrypt (3.1.20)
benchmark (0.4.0)
benchmark (0.4.1)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.1.9)
bigdecimal (3.2.2)
bindata (2.5.1)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
@@ -142,6 +144,10 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
capybara-playwright-driver (0.5.6)
addressable
capybara
playwright-ruby-client (>= 1.16.0)
case_transform (0.2)
activesupport
cbor (0.5.9.8)
@@ -174,7 +180,7 @@ GEM
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.4.1)
debug (1.10.0)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
debug_inspector (1.2.0)
@@ -218,6 +224,7 @@ GEM
mail (~> 2.7)
email_validator (2.2.4)
activemodel
erb (5.0.1)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
@@ -232,7 +239,7 @@ GEM
logger
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-httpclient (2.0.1)
faraday-httpclient (2.0.2)
httpclient (>= 2.2)
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
@@ -280,7 +287,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.62.0)
haml_lint (0.64.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@@ -297,9 +304,8 @@ GEM
redis-client (= 0.24.0)
hkdf (0.3.0)
htmlentities (4.3.4)
http (5.2.0)
http (5.3.1)
addressable (~> 2.8)
base64 (~> 0.1)
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0)
@@ -544,10 +550,10 @@ GEM
opentelemetry-instrumentation-excon (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-faraday (0.26.0)
opentelemetry-instrumentation-faraday (0.27.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http (0.24.0)
opentelemetry-instrumentation-http (0.25.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http_client (0.23.0)
@@ -604,6 +610,9 @@ GEM
pg (1.5.9)
pghero (3.7.0)
activerecord (>= 7.1)
playwright-ruby-client (1.52.0)
concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0)
pp (0.6.2)
prettyprint
premailer (1.27.0)
@@ -633,7 +642,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.15)
rack (3.1.16)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (3.0.0)
@@ -692,14 +701,15 @@ GEM
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rake (13.3.0)
rdf (3.3.2)
bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.7.0)
rdf (~> 3.3)
rdoc (6.13.1)
rdoc (6.14.1)
erb
psych (>= 4.0.0)
redcarpet (3.6.1)
redis (4.8.1)
@@ -727,17 +737,17 @@ GEM
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.3)
rspec-core (3.13.4)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.4)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-github (3.0.0)
rspec-core (~> 3.0)
rspec-mocks (3.13.4)
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (8.0.0)
rspec-rails (8.0.1)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
@@ -750,8 +760,8 @@ GEM
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
rspec-support (3.13.3)
rubocop (1.75.8)
rspec-support (3.13.4)
rubocop (1.77.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -759,10 +769,10 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-ast (>= 1.45.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.1)
rubocop-ast (1.45.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@@ -794,7 +804,7 @@ GEM
ruby-saml (1.18.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.3)
ruby-vips (2.2.4)
ffi (~> 1.12)
logger
rubyzip (2.4.1)
@@ -809,12 +819,6 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.4.1)
selenium-webdriver (4.33.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
sidekiq (7.3.9)
@@ -851,8 +855,8 @@ GEM
stoplight (4.1.1)
redlock (~> 1.0)
stringio (3.1.7)
strong_migrations (2.3.0)
activerecord (>= 7)
strong_migrations (2.4.0)
activerecord (>= 7.1)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
@@ -868,7 +872,7 @@ GEM
thor (1.3.2)
tilt (2.6.0)
timeout (0.4.3)
tpm-key_attestation (0.14.0)
tpm-key_attestation (0.14.1)
bindata (~> 2.4)
openssl (> 2.0)
openssl-signature_algorithm (~> 1.0)
@@ -911,7 +915,7 @@ GEM
zeitwerk (~> 2.2)
warden (1.2.9)
rack (>= 2.0.9)
webauthn (3.4.0)
webauthn (3.4.1)
android_key_attestation (~> 0.3.0)
bindata (~> 2.4)
cbor (~> 0.5.9)
@@ -928,7 +932,6 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
@@ -937,7 +940,7 @@ GEM
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.2)
zeitwerk (2.7.3)
PLATFORMS
ruby
@@ -956,6 +959,7 @@ DEPENDENCIES
browser
bundler-audit (~> 0.9)
capybara (~> 3.39)
capybara-playwright-driver
charlock_holmes (~> 0.7.7)
chewy (~> 7.3)
climate_control
@@ -987,7 +991,7 @@ DEPENDENCIES
hiredis (~> 0.6)
hiredis-client
htmlentities (~> 4.3)
http (~> 5.2.0)
http (~> 5.3.0)
http_accept_language (~> 2.1)
httplog (~> 1.7.0)
i18n
@@ -1026,8 +1030,8 @@ DEPENDENCIES
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-instrumentation-excon (~> 0.23.0)
opentelemetry-instrumentation-faraday (~> 0.26.0)
opentelemetry-instrumentation-http (~> 0.24.0)
opentelemetry-instrumentation-faraday (~> 0.27.0)
opentelemetry-instrumentation-http (~> 0.25.0)
opentelemetry-instrumentation-http_client (~> 0.23.0)
opentelemetry-instrumentation-net_http (~> 0.23.0)
opentelemetry-instrumentation-pg (~> 0.30.0)
@@ -1071,7 +1075,6 @@ DEPENDENCIES
rubyzip (~> 2.3)
sanitize (~> 7.0)
scenic (~> 1.7)
selenium-webdriver
shoulda-matchers
sidekiq (< 8)
sidekiq-bulk (~> 0.2.0)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,7 +92,7 @@ class Api::BaseController < ApplicationController
end
def disallow_unauthenticated_api_access?
ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.limited_federation_mode
ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.mastodon.limited_federation_mode
end
private

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
include AsyncRefreshesConcern
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show]
before_action :require_user!, only: [:show]
@@ -12,6 +14,8 @@ class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
@relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
add_async_refresh_header(account_home_feed.async_refresh, retry_seconds: 5)
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: @relationships,

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1Alpha::AsyncRefreshesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
def show
async_refresh = AsyncRefresh.find(params[:id])
if async_refresh
render json: async_refresh
else
not_found
end
end
end

View File

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

View File

@@ -2,11 +2,13 @@
class Api::V2::SuggestionsController < Api::BaseController
include Authorization
include AsyncRefreshesConcern
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!
before_action :set_suggestions
before_action :schedule_fasp_retrieval
def index
render json: @suggestions.get(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:offset].to_i), each_serializer: REST::SuggestionSerializer
@@ -22,4 +24,18 @@ class Api::V2::SuggestionsController < Api::BaseController
def set_suggestions
@suggestions = AccountSuggestions.new(current_account)
end
def schedule_fasp_retrieval
return unless Mastodon::Feature.fasp_enabled?
# Do not schedule a new retrieval if the request is a follow-up
# to an earlier retrieval
return if request.headers['Mastodon-Async-Refresh-Id'].present?
refresh_key = "fasp:follow_recommendation:#{current_account.id}"
return if AsyncRefresh.new(refresh_key).running?
add_async_refresh_header(AsyncRefresh.create(refresh_key))
Fasp::FollowRecommendationWorker.perform_async(current_account.id)
end
end

View File

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

View File

@@ -138,7 +138,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
set_locale { render :rules }
end
def is_flashing_format? # rubocop:disable Naming/PredicateName
def is_flashing_format? # rubocop:disable Naming/PredicatePrefix
if params[:action] == 'create'
false # Disable flash messages for sign-up
else

View File

@@ -12,18 +12,7 @@ class BackupsController < ApplicationController
BACKUP_LINK_TIMEOUT = 1.hour.freeze
def download
case Paperclip::Attachment.default_options[:storage]
when :s3, :azure
redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true
when :fog
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true
else
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
end
when :filesystem
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
end
redirect_to expiring_asset_url(@backup.dump, BACKUP_LINK_TIMEOUT), allow_other_host: true
end
private

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
module AsyncRefreshesConcern
private
def add_async_refresh_header(async_refresh, retry_seconds: 3)
return unless async_refresh.running?
response.headers['Mastodon-Async-Refresh'] = "id=\"#{async_refresh.id}\", retry=#{retry_seconds}"
end
end

View File

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

View File

@@ -12,7 +12,7 @@ module ThemingConcern
def current_skin
@current_skin ||= begin
skins = Themes.instance.skins_for(current_flavour)
[current_user&.setting_skin, Setting.skin, 'system', 'application'].find { |skin| skins.include?(skin) }
[current_user&.setting_skin, Setting.skin, 'system', 'default'].find { |skin| skins.include?(skin) }
end
end

View File

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

View File

@@ -9,6 +9,7 @@ class MediaProxyController < ApplicationController
skip_before_action :require_functional!
before_action :authenticate_user!, if: :limited_federation_mode?
before_action :set_media_attachment
rescue_from ActiveRecord::RecordInvalid, with: :not_found
rescue_from Mastodon::UnexpectedResponseError, with: :not_found
@@ -16,25 +17,36 @@ class MediaProxyController < ApplicationController
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
def show
with_redis_lock("media_download:#{params[:id]}") do
@media_attachment = MediaAttachment.remote.attached.find(params[:id])
authorize @media_attachment.status, :show?
redownload! if @media_attachment.needs_redownload? && !reject_media?
if @media_attachment.needs_redownload? && !reject_media?
with_redis_lock("media_download:#{params[:id]}") do
@media_attachment.reload # Reload once we have acquired a lock, in case the file was downloaded in the meantime
redownload! if @media_attachment.needs_redownload?
end
end
redirect_to full_asset_url(@media_attachment.file.url(version)), allow_other_host: true
if requires_file_streaming?
send_file(media_attachment_file.path, type: media_attachment_file.instance_read(:content_type), disposition: 'inline')
else
redirect_to media_attachment_file_path, allow_other_host: true
end
end
private
def set_media_attachment
@media_attachment = MediaAttachment.attached.find(params[:id])
authorize @media_attachment, :download?
end
def redownload!
@media_attachment.download_file!
@media_attachment.download_thumbnail!
@media_attachment.created_at = Time.now.utc
@media_attachment.save!
end
def version
if request.path.end_with?('/small')
def attachment_style
if @media_attachment.thumbnail.blank? && preview_requested?
:small
else
:original
@@ -42,6 +54,30 @@ class MediaProxyController < ApplicationController
end
def reject_media?
DomainBlock.reject_media?(@media_attachment.account.domain)
@media_attachment.account.remote? && DomainBlock.reject_media?(@media_attachment.account.domain)
end
def media_attachment_file_path
if @media_attachment.discarded?
expiring_asset_url(media_attachment_file, 10.minutes)
else
full_asset_url(media_attachment_file.url(attachment_style))
end
end
def media_attachment_file
if @media_attachment.thumbnail.present? && preview_requested?
@media_attachment.thumbnail
else
@media_attachment.file
end
end
def preview_requested?
request.path.end_with?('/small')
end
def requires_file_streaming?
Paperclip::Attachment.default_options[:storage] == :filesystem && @media_attachment.discarded?
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,10 +2,10 @@
module AuthorizedFetchHelper
def authorized_fetch_mode?
ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch && 'true' } == 'true' || Rails.configuration.x.limited_federation_mode
ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch && 'true' } == 'true' || Rails.configuration.x.mastodon.limited_federation_mode
end
def authorized_fetch_overridden?
ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.limited_federation_mode
ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.mastodon.limited_federation_mode
end
end

View File

@@ -18,6 +18,6 @@ module DomainControlHelper
end
def limited_federation_mode?
Rails.configuration.x.limited_federation_mode
Rails.configuration.x.mastodon.limited_federation_mode
end
end

View File

@@ -26,6 +26,8 @@ module JsonLdHelper
# The url attribute can be a string, an array of strings, or an array of objects.
# The objects could include a mimeType. Not-included mimeType means it's text/html.
def url_to_href(value, preferred_type = nil)
value = [value] if value.is_a?(Hash)
single_value = if value.is_a?(Array) && !value.first.is_a?(String)
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
elsif value.is_a?(Array)
@@ -41,6 +43,15 @@ module JsonLdHelper
end
end
def url_to_media_type(value, preferred_type = nil)
value = [value] if value.is_a?(Hash)
return unless value.is_a?(Array) && !value.first.is_a?(String)
single_value = value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
single_value['mediaType'] unless single_value.nil?
end
def as_array(value)
if value.nil?
[]

View File

@@ -11,9 +11,9 @@ module MediaComponentHelper
src: full_asset_url(video.file.url(:original)),
preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)),
alt: video.description,
lang: status.language,
blurhash: video.blurhash,
frameRate: meta.dig('original', 'frame_rate'),
inline: true,
aspectRatio: "#{meta.dig('original', 'width')} / #{meta.dig('original', 'height')}",
media: [
serialize_media_attachment(video),
@@ -34,6 +34,8 @@ module MediaComponentHelper
src: full_asset_url(audio.file.url(:original)),
poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url),
alt: audio.description,
lang: status.language,
blurhash: audio.blurhash,
backgroundColor: meta.dig('colors', 'background'),
foregroundColor: meta.dig('colors', 'foreground'),
accentColor: meta.dig('colors', 'accent'),

View File

@@ -20,6 +20,21 @@ module RoutingHelper
URI.join(asset_host, source).to_s
end
def expiring_asset_url(attachment, expires_in)
case Paperclip::Attachment.default_options[:storage]
when :s3, :azure
attachment.expiring_url(expires_in.to_i)
when :fog
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
attachment.expiring_url(expires_in.from_now)
else
full_asset_url(attachment.url)
end
when :filesystem
full_asset_url(attachment.url)
end
end
def asset_host
Rails.configuration.action_controller.asset_host || root_url
end

View File

@@ -6,13 +6,11 @@ module ThemeHelper
if theme == 'system'
''.html_safe.tap do |tags|
tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light.scss", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << vite_stylesheet_tag("skins/#{flavour}/application.scss", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light", type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << vite_stylesheet_tag("skins/#{flavour}/default", type: :virtual, media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
end
elsif theme == 'default'
vite_stylesheet_tag "skins/#{flavour}/application.scss", media: 'all', crossorigin: 'anonymous'
else
vite_stylesheet_tag "skins/#{flavour}/#{theme}.scss", media: 'all', crossorigin: 'anonymous'
vite_stylesheet_tag "skins/#{flavour}/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous'
end
end

View File

@@ -2,6 +2,7 @@ import { browserHistory } from 'flavours/glitch/components/router';
import { debounceWithDispatchAndArguments } from 'flavours/glitch/utils/debounce';
import api, { getLinks } from '../api';
import { me } from '../initial_state';
import {
followAccountSuccess, unfollowAccountSuccess,
@@ -12,6 +13,7 @@ import {
blockAccountSuccess, unblockAccountSuccess,
pinAccountSuccess, unpinAccountSuccess,
fetchRelationshipsSuccess,
fetchEndorsedAccounts,
} from './accounts_typed';
import { importFetchedAccount, importFetchedAccounts } from './importer';
@@ -634,6 +636,7 @@ export function pinAccount(id) {
api().post(`/api/v1/accounts/${id}/pin`).then(response => {
dispatch(pinAccountSuccess({ relationship: response.data }));
dispatch(fetchEndorsedAccounts({ accountId: me }));
}).catch(error => {
dispatch(pinAccountFail(error));
});
@@ -646,6 +649,7 @@ export function unpinAccount(id) {
api().post(`/api/v1/accounts/${id}/unpin`).then(response => {
dispatch(unpinAccountSuccess({ relationship: response.data }));
dispatch(fetchEndorsedAccounts({ accountId: me }));
}).catch(error => {
dispatch(unpinAccountFail(error));
});

View File

@@ -4,14 +4,12 @@ export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
export * from './lists_typed';
export const fetchList = id => (dispatch, getState) => {
if (getState().getIn(['lists', id])) {
return;
@@ -40,28 +38,6 @@ export const fetchListFail = (id, error) => ({
error,
});
export const fetchLists = () => (dispatch) => {
dispatch(fetchListsRequest());
api().get('/api/v1/lists')
.then(({ data }) => dispatch(fetchListsSuccess(data)))
.catch(err => dispatch(fetchListsFail(err)));
};
export const fetchListsRequest = () => ({
type: LISTS_FETCH_REQUEST,
});
export const fetchListsSuccess = lists => ({
type: LISTS_FETCH_SUCCESS,
lists,
});
export const fetchListsFail = error => ({
type: LISTS_FETCH_FAIL,
error,
});
export const deleteList = id => (dispatch) => {
dispatch(deleteListRequest(id));

View File

@@ -1,4 +1,4 @@
import { apiCreate, apiUpdate } from 'flavours/glitch/api/lists';
import { apiCreate, apiUpdate, apiGetLists } from 'flavours/glitch/api/lists';
import type { List } from 'flavours/glitch/models/list';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
@@ -11,3 +11,7 @@ export const updateList = createDataLoadingThunk(
'list/update',
(list: Partial<List>) => apiUpdate(list),
);
export const fetchLists = createDataLoadingThunk('lists/fetch', () =>
apiGetLists(),
);

View File

@@ -0,0 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
export const openNavigation = createAction('navigation/open');
export const closeNavigation = createAction('navigation/close');
export const toggleNavigation = createAction('navigation/toggle');

View File

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

View File

@@ -13,6 +13,8 @@ export const apiCreate = (list: Partial<ApiListJSON>) =>
export const apiUpdate = (list: Partial<ApiListJSON>) =>
apiRequestPut<ApiListJSON>(`v1/lists/${list.id}`, list);
export const apiGetLists = () => apiRequestGet<ApiListJSON[]>('v1/lists');
export const apiGetAccounts = (listId: string) =>
apiRequestGet<ApiAccountJSON[]>(`v1/lists/${listId}/accounts`, {
limit: 0,

View File

@@ -20,10 +20,11 @@ export const apiFeatureTag = (tagId: string) =>
export const apiUnfeatureTag = (tagId: string) =>
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfeature`);
export const apiGetFollowedTags = async (url?: string) => {
export const apiGetFollowedTags = async (url?: string, limit?: number) => {
const response = await api().request<ApiHashtagJSON[]>({
method: 'GET',
url: url ?? '/api/v1/followed_tags',
params: { limit },
});
return {

View File

@@ -11,6 +11,8 @@ import {
muteAccount,
unmuteAccount,
followAccountSuccess,
unpinAccount,
pinAccount,
} from 'flavours/glitch/actions/accounts';
import { showAlertForError } from 'flavours/glitch/actions/alerts';
import { openModal } from 'flavours/glitch/actions/modal';
@@ -63,14 +65,25 @@ const messages = defineMessages({
},
});
export const Account: React.FC<{
interface AccountProps {
size?: number;
id: string;
hidden?: boolean;
minimal?: boolean;
defaultAction?: 'block' | 'mute';
withBio?: boolean;
}> = ({ id, size = 46, hidden, minimal, defaultAction, withBio }) => {
withMenu?: boolean;
}
export const Account: React.FC<AccountProps> = ({
id,
size = 46,
hidden,
minimal,
defaultAction,
withBio,
withMenu = true,
}) => {
const intl = useIntl();
const { signedIn } = useIdentity();
const account = useAppSelector((state) => state.accounts.get(id));
@@ -120,8 +133,6 @@ export const Account: React.FC<{
},
];
} else if (defaultAction !== 'block') {
arr = [];
if (isRemote && accountUrl) {
arr.push({
text: intl.formatMessage(messages.openOriginalPage),
@@ -174,6 +185,25 @@ export const Account: React.FC<{
text: intl.formatMessage(messages.addToLists),
action: handleAddToLists,
});
if (id !== me && (relationship?.following || relationship?.requested)) {
const handleEndorseToggle = () => {
if (relationship.endorsed) {
dispatch(unpinAccount(id));
} else {
dispatch(pinAccount(id));
}
};
arr.push({
text: intl.formatMessage(
// Defined in features/account_timeline/components/account_header.tsx
relationship.endorsed
? { id: 'account.unendorse' }
: { id: 'account.endorse' },
),
action: handleEndorseToggle,
});
}
}
}
@@ -198,9 +228,10 @@ export const Account: React.FC<{
);
}
let button: React.ReactNode, dropdown: React.ReactNode;
let button: React.ReactNode;
let dropdown: React.ReactNode;
if (menu.length > 0) {
if (menu.length > 0 && withMenu) {
dropdown = (
<Dropdown
items={menu}
@@ -252,43 +283,69 @@ export const Account: React.FC<{
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Permalink
className='account__display-name'
title={account?.acct}
href={account?.url}
to={`/@${account?.acct}`}
data-hover-card-account={id}
>
<div className='account__avatar-wrapper'>
{account ? (
<Avatar account={account} size={size} />
<div
className={classNames('account', {
'account--minimal': minimal,
})}
>
<div
className={classNames('account__wrapper', {
'account__wrapper--with-bio': account && withBio,
})}
>
<div className='account__info-wrapper'>
<Permalink
className='account__display-name'
title={account?.acct}
href={account?.url}
to={`/@${account?.acct}`}
data-hover-card-account={id}
>
<div className='account__avatar-wrapper'>
{account ? (
<Avatar account={account} size={size} />
) : (
<Skeleton width={size} height={size} />
)}
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
{account ? (
<>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
/>{' '}
{verification} {muteTimeRemaining}
</>
) : (
<Skeleton width='7ch' />
)}
</div>
)}
</div>
</Permalink>
{account &&
withBio &&
(account.note.length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
/>
) : (
<Skeleton width={size} height={size} />
)}
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
{account ? (
<>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
/>{' '}
{verification} {muteTimeRemaining}
</>
) : (
<Skeleton width='7ch' />
)}
<div className='account__note account__note--missing'>
<FormattedMessage
id='account.no_bio'
defaultMessage='No description provided.'
/>
</div>
)}
</div>
</Permalink>
))}
</div>
{!minimal && (
<div className='account__relationship'>
@@ -297,22 +354,6 @@ export const Account: React.FC<{
</div>
)}
</div>
{account &&
withBio &&
(account.note.length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
/>
) : (
<div className='account__note account__note--missing'>
<FormattedMessage
id='account.no_bio'
defaultMessage='No description provided.'
/>
</div>
))}
</div>
);
};

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ interface Props {
withLink?: boolean;
counter?: number | string;
counterBorderColor?: string;
className?: string;
}
export const Avatar: React.FC<Props> = ({
@@ -27,6 +28,7 @@ export const Avatar: React.FC<Props> = ({
inline = false,
withLink = false,
style: styleFromParent,
className,
counter,
counterBorderColor,
}) => {
@@ -52,7 +54,7 @@ export const Avatar: React.FC<Props> = ({
const avatar = (
<div
className={classNames('account__avatar', {
className={classNames(className, 'account__avatar', {
'account__avatar--inline': inline,
'account__avatar--loading': loading,
})}

View File

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

View File

@@ -9,7 +9,8 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import UnfoldLessIcon from '@/material-icons/400-24px/unfold_less.svg?react';
import UnfoldMoreIcon from '@/material-icons/400-24px/unfold_more.svg?react';
import type { IconProp } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context';
@@ -17,7 +18,7 @@ import { useIdentity } from 'flavours/glitch/identity_context';
import { useAppHistory } from './router';
const messages = defineMessages({
export const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: {
@@ -238,7 +239,10 @@ export const ColumnHeader: React.FC<Props> = ({
onClick={handleToggleClick}
>
<i className='icon-with-badge'>
<Icon id='sliders' icon={SettingsIcon} />
<Icon
id='sliders'
icon={collapsed ? UnfoldMoreIcon : UnfoldLessIcon}
/>
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
</i>
</button>

View File

@@ -20,6 +20,6 @@ export const ContentWarning: React.FC<{
key={`icon-${icon}`}
/>
))}
<p dangerouslySetInnerHTML={{ __html: text }} />
<span dangerouslySetInnerHTML={{ __html: text }} />
</StatusBanner>
);

View File

@@ -12,15 +12,13 @@ export const FilterWarning: React.FC<{
onClick={onClick}
variant={BannerVariant.Filter}
>
<p>
<FormattedMessage
id='filter_warning.matches_filter'
defaultMessage='Matches filter “<span>{title}</span>”'
values={{
title,
span: (chunks) => <span className='filter-name'>{chunks}</span>,
}}
/>
</p>
<FormattedMessage
id='filter_warning.matches_filter'
defaultMessage='Matches filter “<span>{title}</span>”'
values={{
title,
span: (chunks) => <span className='filter-name'>{chunks}</span>,
}}
/>
</StatusBanner>
);

View File

@@ -20,6 +20,7 @@ import { Permalink } from 'flavours/glitch/components/permalink';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { useFetchFamiliarFollowers } from 'flavours/glitch/features/account_timeline/hooks/familiar_followers';
import { domain } from 'flavours/glitch/initial_state';
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
export const HoverCardAccount = forwardRef<
@@ -31,6 +32,11 @@ export const HoverCardAccount = forwardRef<
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const suspended = account?.suspended;
const hidden = useAppSelector((state) =>
accountId ? getAccountHidden(state, accountId) : undefined,
);
const isSuspendedOrHidden = Boolean(suspended || hidden);
const note = useAppSelector(
(state) =>
@@ -74,69 +80,95 @@ export const HoverCardAccount = forwardRef<
href={account.get('url')}
className='hover-card__name'
>
<Avatar account={account} size={46} />
<Avatar
account={isSuspendedOrHidden ? undefined : account}
size={46}
/>
<DisplayName account={account} localDomain={domain} />
</Permalink>
<div className='hover-card__text-row'>
<AccountBio
note={account.note_emojified}
className='hover-card__bio'
/>
<AccountFields fields={account.fields} limit={2} />
{note && note.length > 0 && (
<dl className='hover-card__note'>
<dt className='hover-card__note-label'>
<FormattedMessage
id='account.account_note_header'
defaultMessage='Personal note'
/>
</dt>
<dd>{note}</dd>
</dl>
)}
</div>
<div className='hover-card__numbers'>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
/>
{shouldDisplayFamiliarFollowers && (
<>
&middot;
<div className='hover-card__familiar-followers'>
<ShortNumber
value={familiarFollowers.length}
renderer={FollowersYouKnowCounter}
/>
<AvatarGroup compact>
{familiarFollowers.slice(0, 3).map((account) => (
<Avatar key={account.id} account={account} size={22} />
))}
</AvatarGroup>
</div>
</>
)}
{(isMutual || isFollower) && (
<>
&middot;
{isMutual ? (
<FormattedMessage
id='account.mutual'
defaultMessage='You follow each other'
/>
) : (
<FormattedMessage
id='account.follows_you'
defaultMessage='Follows you'
/>
{isSuspendedOrHidden ? (
<div className='hover-card__limited-account-note'>
{suspended ? (
<FormattedMessage
id='empty_column.account_suspended'
defaultMessage='Account suspended'
/>
) : (
<FormattedMessage
id='limited_account_hint.title'
defaultMessage='This profile has been hidden by the moderators of {domain}.'
values={{ domain }}
/>
)}
</div>
) : (
<>
<div className='hover-card__text-row'>
<AccountBio
note={account.note_emojified}
className='hover-card__bio'
/>
<AccountFields fields={account.fields} limit={2} />
{note && note.length > 0 && (
<dl className='hover-card__note'>
<dt className='hover-card__note-label'>
<FormattedMessage
id='account.account_note_header'
defaultMessage='Personal note'
/>
</dt>
<dd>{note}</dd>
</dl>
)}
</>
)}
</div>
</div>
<FollowButton accountId={accountId} />
<div className='hover-card__numbers'>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
/>
{shouldDisplayFamiliarFollowers && (
<>
&middot;
<div className='hover-card__familiar-followers'>
<ShortNumber
value={familiarFollowers.length}
renderer={FollowersYouKnowCounter}
/>
<AvatarGroup compact>
{familiarFollowers.slice(0, 3).map((account) => (
<Avatar
key={account.id}
account={account}
size={22}
/>
))}
</AvatarGroup>
</div>
</>
)}
{(isMutual || isFollower) && (
<>
&middot;
{isMutual ? (
<FormattedMessage
id='account.mutual'
defaultMessage='You follow each other'
/>
) : (
<FormattedMessage
id='account.follows_you'
defaultMessage='Follows you'
/>
)}
</>
)}
</div>
<FollowButton accountId={accountId} />
</>
)}
</>
) : (
<LoadingIndicator />

View File

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

View File

@@ -27,6 +27,7 @@ interface Props {
counter?: number;
href?: string;
ariaHidden?: boolean;
ariaControls?: string;
label?: string;
obfuscateCount?: boolean;
}
@@ -54,6 +55,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
overlay = false,
tabIndex = 0,
ariaHidden = false,
ariaControls,
label,
obfuscateCount,
},
@@ -158,6 +160,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
aria-label={title}
aria-expanded={expanded}
aria-hidden={ariaHidden}
aria-controls={ariaControls}
title={title}
className={classes}
onClick={handleClick}

View File

@@ -7,7 +7,7 @@ interface Props {
id: string;
icon: IconProp;
count: number;
issueBadge: boolean;
issueBadge?: boolean;
className: string;
}
export const IconWithBadge: React.FC<Props> = ({

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
import Trends from 'flavours/glitch/features/getting_started/containers/trends_container';
import { showTrends } from 'flavours/glitch/initial_state';
export const NavigationPortal: React.FC = () => (
<div className='navigation-panel__portal'>{showTrends && <Trends />}</div>
);

View File

@@ -14,7 +14,6 @@ import { fetchPoll, vote } from 'flavours/glitch/actions/polls';
import { Icon } from 'flavours/glitch/components/icon';
import emojify from 'flavours/glitch/features/emoji/emoji';
import { useIdentity } from 'flavours/glitch/identity_context';
import { reduceMotion } from 'flavours/glitch/initial_state';
import { makeEmojiMap } from 'flavours/glitch/models/custom_emoji';
import type * as Model from 'flavours/glitch/models/poll';
import type { Status } from 'flavours/glitch/models/status';
@@ -265,7 +264,6 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
to: {
width: `${percent}%`,
},
immediate: reduceMotion,
});
return (
@@ -320,7 +318,7 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
id='check'
icon={CheckIcon}
className='poll__voted__mark'
title={intl.formatMessage(messages.voted)}
aria-label={intl.formatMessage(messages.voted)}
/>
</span>
)}

View File

@@ -377,7 +377,11 @@ class Status extends ImmutablePureComponent {
if (newTab) {
window.open(path, '_blank', 'noopener');
} else {
history.push(path);
if (history.location.pathname.replace('/deck/', '/') === path) {
history.replace(path);
} else {
history.push(path);
}
}
};
@@ -735,10 +739,10 @@ class Status extends ImmutablePureComponent {
{...statusContentProps}
/>
{children}
{media}
{hashtagBar}
{children}
</>
)}

View File

@@ -1,3 +1,6 @@
import type { MouseEventHandler } from 'react';
import { useCallback, useRef, useId } from 'react';
import { FormattedMessage } from 'react-intl';
export enum BannerVariant {
@@ -5,38 +8,67 @@ export enum BannerVariant {
Filter = 'filter',
}
const stopPropagation: MouseEventHandler = (e) => {
e.stopPropagation();
};
export const StatusBanner: React.FC<{
children: React.ReactNode;
variant: BannerVariant;
expanded?: boolean;
onClick?: () => void;
}> = ({ children, variant, expanded, onClick }) => (
<label
className={
variant === BannerVariant.Warning
? 'content-warning'
: 'content-warning content-warning--filter'
}
>
{children}
}> = ({ children, variant, expanded, onClick }) => {
const descriptionId = useId();
<button className='link-button' onClick={onClick}>
{expanded ? (
<FormattedMessage
id='content_warning.hide'
defaultMessage='Hide post'
/>
) : variant === BannerVariant.Warning ? (
<FormattedMessage
id='content_warning.show_more'
defaultMessage='Show more'
/>
) : (
<FormattedMessage
id='content_warning.show'
defaultMessage='Show anyway'
/>
)}
</button>
</label>
);
const buttonRef = useRef<HTMLButtonElement>(null);
const forwardClick = useCallback<MouseEventHandler>((e) => {
if (
buttonRef.current &&
e.target !== buttonRef.current &&
!buttonRef.current.contains(e.target as Node)
) {
buttonRef.current.click();
buttonRef.current.focus();
}
}, []);
return (
// Element clicks are passed on to button
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
className={
variant === BannerVariant.Warning
? 'content-warning'
: 'content-warning content-warning--filter'
}
onClick={forwardClick}
onMouseUp={stopPropagation}
>
<p id={descriptionId}>{children}</p>
<button
ref={buttonRef}
className='link-button'
onClick={onClick}
aria-describedby={descriptionId}
>
{expanded ? (
<FormattedMessage
id='content_warning.hide'
defaultMessage='Hide post'
/>
) : variant === BannerVariant.Warning ? (
<FormattedMessage
id='content_warning.show_more'
defaultMessage='Show more'
/>
) : (
<FormattedMessage
id='content_warning.show'
defaultMessage='Show anyway'
/>
)}
</button>
</div>
);
};

View File

@@ -349,7 +349,7 @@ class StatusContent extends PureComponent {
if (this.props.onClick) {
return (
<>
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} tabIndex={0} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='status__content__text status__content__text--visible translate' lang={language} dangerouslySetInnerHTML={content} />
{poll}
@@ -361,7 +361,7 @@ class StatusContent extends PureComponent {
);
} else {
return (
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className={classNames} ref={this.setRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='status__content__text status__content__text--visible translate' lang={language} dangerouslySetInnerHTML={content} />
{poll}

View File

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

View File

@@ -1,3 +1,5 @@
import { useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
@@ -11,7 +13,11 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'
import { Icon } from 'flavours/glitch/components/icon';
import StatusContainer from 'flavours/glitch/containers/status_container';
import type { Status } from 'flavours/glitch/models/status';
import { useAppSelector } from 'flavours/glitch/store';
import type { RootState } from 'flavours/glitch/store';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { fetchStatus } from '../actions/statuses';
import { makeGetStatus } from '../selectors';
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
@@ -31,7 +37,7 @@ const QuoteWrapper: React.FC<{
);
};
const QuoteLink: React.FC<{
const NestedQuoteLink: React.FC<{
status: Status;
}> = ({ status }) => {
const accountId = status.get('account') as string;
@@ -64,6 +70,10 @@ const QuoteLink: React.FC<{
};
type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>;
type GetStatusSelector = (
state: RootState,
props: { id?: string | null; contextType?: string },
) => Status | null;
export const QuotedStatus: React.FC<{
quote: QuoteMap;
@@ -71,35 +81,59 @@ export const QuotedStatus: React.FC<{
variant?: 'full' | 'link';
nestingLevel?: number;
}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => {
const dispatch = useAppDispatch();
const quotedStatusId = quote.get('quoted_status');
const state = quote.get('state');
const quoteState = quote.get('state');
const status = useAppSelector((state) =>
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
);
useEffect(() => {
if (!status && quotedStatusId) {
dispatch(fetchStatus(quotedStatusId));
}
}, [status, quotedStatusId, dispatch]);
// In order to find out whether the quoted post should be completely hidden
// due to a matching filter, we run it through the selector used by `status_container`.
// If this returns null even though `status` exists, it's because it's filtered.
const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector;
const statusWithExtraData = useAppSelector((state) =>
getStatus(state, { id: quotedStatusId, contextType }),
);
const isFilteredAndHidden = status && statusWithExtraData === null;
let quoteError: React.ReactNode = null;
if (state === 'deleted') {
if (isFilteredAndHidden) {
quoteError = (
<FormattedMessage
id='status.quote_error.filtered'
defaultMessage='Hidden due to one of your filters'
/>
);
} else if (quoteState === 'deleted') {
quoteError = (
<FormattedMessage
id='status.quote_error.removed'
defaultMessage='This post was removed by its author.'
/>
);
} else if (state === 'unauthorized') {
} else if (quoteState === 'unauthorized') {
quoteError = (
<FormattedMessage
id='status.quote_error.unauthorized'
defaultMessage='This post cannot be displayed as you are not authorized to view it.'
/>
);
} else if (state === 'pending') {
} else if (quoteState === 'pending') {
quoteError = (
<FormattedMessage
id='status.quote_error.pending_approval'
defaultMessage='This post is pending approval from the original author.'
/>
);
} else if (state === 'rejected' || state === 'revoked') {
} else if (quoteState === 'rejected' || quoteState === 'revoked') {
quoteError = (
<FormattedMessage
id='status.quote_error.rejected'
@@ -120,7 +154,7 @@ export const QuotedStatus: React.FC<{
}
if (variant === 'link' && status) {
return <QuoteLink status={status} />;
return <NestedQuoteLink status={status} />;
}
const childQuote = status?.get('quote') as QuoteMap | undefined;

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ interface BaseRule {
interface Rule extends BaseRule {
id: string;
translations: Record<string, BaseRule>;
translations?: Record<string, BaseRule>;
}
export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
@@ -113,15 +113,23 @@ const rulesSelector = createSelector(
(rules, locale): Rule[] => {
return rules.map((rule) => {
const translations = rule.translations;
if (translations[locale]) {
rule.text = translations[locale].text;
rule.hint = translations[locale].hint;
// Handle cached responses from earlier versions
if (!translations) {
return rule;
}
const partialLocale = locale.split('-')[0];
if (partialLocale && translations[partialLocale]) {
rule.text = translations[partialLocale].text;
rule.hint = translations[partialLocale].hint;
}
if (translations[locale]) {
rule.text = translations[locale].text;
rule.hint = translations[locale].hint;
}
return rule;
});
},

View File

@@ -1,173 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { is } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
const messages = defineMessages({
placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
});
class InlineAlert extends PureComponent {
static propTypes = {
show: PropTypes.bool,
};
state = {
mountMessage: false,
};
static TRANSITION_DELAY = 200;
UNSAFE_componentWillReceiveProps (nextProps) {
if (!this.props.show && nextProps.show) {
this.setState({ mountMessage: true });
} else if (this.props.show && !nextProps.show) {
setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY);
}
}
render () {
const { show } = this.props;
const { mountMessage } = this.state;
return (
<span aria-live='polite' role='status' className='inline-alert' style={{ opacity: show ? 1 : 0 }}>
{mountMessage && <FormattedMessage id='generic.saved' defaultMessage='Saved' />}
</span>
);
}
}
class AccountNote extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
value: PropTypes.string,
onSave: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
value: null,
saving: false,
saved: false,
};
UNSAFE_componentWillMount () {
this._reset();
}
UNSAFE_componentWillReceiveProps (nextProps) {
const accountWillChange = !is(this.props.accountId, nextProps.accountId);
const newState = {};
if (accountWillChange && this._isDirty()) {
this._save(false);
}
if (accountWillChange || nextProps.value === this.state.value) {
newState.saving = false;
}
if (this.props.value !== nextProps.value) {
newState.value = nextProps.value;
}
this.setState(newState);
}
componentWillUnmount () {
if (this._isDirty()) {
this._save(false);
}
}
setTextareaRef = c => {
this.textarea = c;
};
handleChange = e => {
this.setState({ value: e.target.value, saving: false });
};
handleKeyDown = e => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (this.textarea) {
this.textarea.blur();
} else {
this._save();
}
} else if (e.keyCode === 27) {
e.preventDefault();
this._reset(() => {
if (this.textarea) {
this.textarea.blur();
}
});
}
};
handleBlur = () => {
if (this._isDirty()) {
this._save();
}
};
_save (showMessage = true) {
this.setState({ saving: true }, () => this.props.onSave(this.state.value));
if (showMessage) {
this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000));
}
}
_reset (callback) {
this.setState({ value: this.props.value }, callback);
}
_isDirty () {
return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value;
}
render () {
const { accountId, intl } = this.props;
const { value, saved } = this.state;
if (!accountId) {
return null;
}
return (
<div className='account__header__account-note'>
<label htmlFor={`account-note-${accountId}`}>
<FormattedMessage id='account.account_note_header' defaultMessage='Personal note' /> <InlineAlert show={saved} />
</label>
<Textarea
id={`account-note-${accountId}`}
className='account__header__account-note__content'
disabled={this.props.value === null || value === null}
placeholder={intl.formatMessage(messages.placeholder)}
value={value || ''}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleBlur}
ref={this.setTextareaRef}
/>
</div>
);
}
}
export default injectIntl(AccountNote);

View File

@@ -0,0 +1,131 @@
import type { ChangeEventHandler, KeyboardEventHandler } from 'react';
import { useState, useRef, useCallback, useId } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import Textarea from 'react-textarea-autosize';
import { submitAccountNote } from '@/flavours/glitch/actions/account_notes';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
const messages = defineMessages({
placeholder: {
id: 'account_note.placeholder',
defaultMessage: 'Click to add a note',
},
});
const AccountNoteUI: React.FC<{
initialValue: string | undefined;
onSubmit: (newNote: string) => void;
wasSaved: boolean;
}> = ({ initialValue, onSubmit, wasSaved }) => {
const intl = useIntl();
const uniqueId = useId();
const [value, setValue] = useState(initialValue ?? '');
const isLoading = initialValue === undefined;
const canSubmitOnBlurRef = useRef(true);
const handleChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
(e) => {
setValue(e.target.value);
},
[],
);
const handleKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
(e) => {
if (e.key === 'Escape') {
e.preventDefault();
setValue(initialValue ?? '');
canSubmitOnBlurRef.current = false;
e.currentTarget.blur();
} else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
onSubmit(value);
canSubmitOnBlurRef.current = false;
e.currentTarget.blur();
}
},
[initialValue, onSubmit, value],
);
const handleBlur = useCallback(() => {
if (initialValue !== value && canSubmitOnBlurRef.current) {
onSubmit(value);
}
canSubmitOnBlurRef.current = true;
}, [initialValue, onSubmit, value]);
return (
<div className='account__header__account-note'>
<label htmlFor={`account-note-${uniqueId}`}>
<FormattedMessage
id='account.account_note_header'
defaultMessage='Personal note'
/>{' '}
<span
aria-live='polite'
role='status'
className='inline-alert'
style={{ opacity: wasSaved ? 1 : 0 }}
>
{wasSaved && (
<FormattedMessage id='generic.saved' defaultMessage='Saved' />
)}
</span>
</label>
{isLoading ? (
<div className='account__header__account-note__loading-indicator-wrapper'>
<LoadingIndicator />
</div>
) : (
<Textarea
id={`account-note-${uniqueId}`}
className='account__header__account-note__content'
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
/>
)}
</div>
);
};
export const AccountNote: React.FC<{
accountId: string;
}> = ({ accountId }) => {
const dispatch = useAppDispatch();
const initialValue = useAppSelector((state) =>
state.relationships.get(accountId)?.get('note'),
);
const [wasSaved, setWasSaved] = useState(false);
const handleSubmit = useCallback(
(note: string) => {
setWasSaved(true);
void dispatch(submitAccountNote({ accountId, note }));
setTimeout(() => {
setWasSaved(false);
}, 2000);
},
[dispatch, accountId],
);
return (
<AccountNoteUI
key={`${accountId}-${initialValue}`}
initialValue={initialValue}
wasSaved={wasSaved}
onSubmit={handleSubmit}
/>
);
};

View File

@@ -1,19 +0,0 @@
import { connect } from 'react-redux';
import { submitAccountNote } from 'flavours/glitch/actions/account_notes';
import AccountNote from '../components/account_note';
const mapStateToProps = (state, { accountId }) => ({
value: state.relationships.getIn([accountId, 'note']),
});
const mapDispatchToProps = (dispatch, { accountId }) => ({
onSave (value) {
dispatch(submitAccountNote({ accountId: accountId, note: value }));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);

View File

@@ -42,8 +42,8 @@ import { FollowButton } from 'flavours/glitch/components/follow_button';
import { FormattedDateWrapper } from 'flavours/glitch/components/formatted_date';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { AccountNote } from 'flavours/glitch/features/account/components/account_note';
import { DomainPill } from 'flavours/glitch/features/account/components/domain_pill';
import AccountNoteContainer from 'flavours/glitch/features/account/containers/account_note_container';
import FollowRequestNoteContainer from 'flavours/glitch/features/account/containers/follow_request_note_container';
import { useLinks } from 'flavours/glitch/hooks/useLinks';
import { useIdentity } from 'flavours/glitch/identity_context';
@@ -422,7 +422,7 @@ export const AccountHeader: React.FC<{
return arr;
}
if (signedIn && account.id !== me && !account.suspended) {
if (signedIn && !account.suspended) {
arr.push({
text: intl.formatMessage(messages.mention, {
name: account.username,
@@ -446,37 +446,7 @@ export const AccountHeader: React.FC<{
arr.push(null);
}
if (account.id === me) {
arr.push({
text: intl.formatMessage(messages.edit_profile),
href: '/settings/profile',
});
arr.push({
text: intl.formatMessage(messages.preferences),
href: '/settings/preferences',
});
arr.push(null);
arr.push({
text: intl.formatMessage(messages.follow_requests),
to: '/follow_requests',
});
arr.push({
text: intl.formatMessage(messages.favourites),
to: '/favourites',
});
arr.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
arr.push({
text: intl.formatMessage(messages.followed_tags),
to: '/followed_tags',
});
arr.push(null);
arr.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
arr.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
arr.push({
text: intl.formatMessage(messages.domain_blocks),
to: '/domain_blocks',
});
} else if (signedIn) {
if (signedIn) {
if (relationship?.following) {
if (!relationship.muting) {
if (relationship.showing_reblogs) {
@@ -615,8 +585,7 @@ export const AccountHeader: React.FC<{
}
if (
(account.id !== me &&
(permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) ||
(permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS ||
(isRemote &&
(permissions & PERMISSION_MANAGE_FEDERATION) ===
PERMISSION_MANAGE_FEDERATION)
@@ -803,7 +772,7 @@ export const AccountHeader: React.FC<{
<Icon
id='lock'
icon={LockIcon}
title={intl.formatMessage(messages.account_locked)}
aria-label={intl.formatMessage(messages.account_locked)}
/>
);
}
@@ -884,12 +853,14 @@ export const AccountHeader: React.FC<{
<div className='account__header__tabs__buttons'>
{!hidden && bellBtn}
{!hidden && shareBtn}
<Dropdown
disabled={menu.length === 0}
items={menu}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
/>
{accountId !== me && (
<Dropdown
disabled={menu.length === 0}
items={menu}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
/>
)}
{!hidden && actionBtn}
</div>
</div>
@@ -927,7 +898,7 @@ export const AccountHeader: React.FC<{
onClickCapture={handleLinkClick}
>
{account.id !== me && signedIn && (
<AccountNoteContainer accountId={accountId} />
<AccountNote accountId={accountId} />
)}
{account.note.length > 0 && account.note !== '<p></p>' && (

View File

@@ -138,7 +138,7 @@ class AccountTimeline extends ImmutablePureComponent {
};
render () {
const { accountId, statusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
const { accountId, statusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl, params: { tagged } } = this.props;
if (isLoading && statusIds.isEmpty()) {
return (
@@ -173,8 +173,8 @@ class AccountTimeline extends ImmutablePureComponent {
<StatusList
prepend={
<>
<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />
{!forceEmptyState && <FeaturedCarousel accountId={this.props.accountId} />}
<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={tagged} />
{!forceEmptyState && <FeaturedCarousel accountId={this.props.accountId} tagged={tagged} />}
</>
}
alwaysPrepend

View File

@@ -27,7 +27,7 @@ import { Audio } from 'flavours/glitch/features/audio';
import { CharacterCounter } from 'flavours/glitch/features/compose/components/character_counter';
import { Tesseract as fetchTesseract } from 'flavours/glitch/features/ui/util/async-components';
import { Video, getPointerPosition } from 'flavours/glitch/features/video';
import { me, reduceMotion } from 'flavours/glitch/initial_state';
import { me } from 'flavours/glitch/initial_state';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { assetHost } from 'flavours/glitch/utils/config';
@@ -110,7 +110,7 @@ const Preview: React.FC<{
left: `${x * 100}%`,
top: `${y * 100}%`,
},
immediate: reduceMotion || draggingRef.current,
immediate: draggingRef.current,
});
const media = useAppSelector((state) =>
(

View File

@@ -17,12 +17,9 @@ import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { SpoilerButton } from 'flavours/glitch/components/spoiler_button';
import { formatTime, getPointerPosition } from 'flavours/glitch/features/video';
import { useAudioContext } from 'flavours/glitch/hooks/useAudioContext';
import { useAudioVisualizer } from 'flavours/glitch/hooks/useAudioVisualizer';
import {
displayMedia,
useBlurhash,
reduceMotion,
} from 'flavours/glitch/initial_state';
import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import { playerSettings } from 'flavours/glitch/settings';
const messages = defineMessages({
@@ -119,12 +116,17 @@ export const Audio: React.FC<{
const seekRef = useRef<HTMLDivElement>(null);
const volumeRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const [resumeAudio, suspendAudio, frequencyBands] = useAudioVisualizer(
audioRef,
3,
);
const accessibilityId = useId();
const { audioContextRef, sourceRef, gainNodeRef, playAudio, pauseAudio } =
useAudioContext({ audioElementRef: audioRef });
const frequencyBands = useAudioVisualizer({
audioContextRef,
sourceRef,
numBands: 3,
});
const [style, spring] = useSpring(() => ({
progress: '0%',
buffer: '0%',
@@ -152,22 +154,23 @@ export const Audio: React.FC<{
restoreVolume(audioRef.current);
setVolume(audioRef.current.volume);
setMuted(audioRef.current.muted);
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = audioRef.current.volume;
}
void spring.start({
volume: `${audioRef.current.volume * 100}%`,
immediate: reduceMotion,
});
}
},
[
spring,
setVolume,
setMuted,
deployPictureInPicture,
src,
poster,
backgroundColor,
accentColor,
foregroundColor,
deployPictureInPicture,
accentColor,
gainNodeRef,
spring,
],
);
@@ -178,7 +181,11 @@ export const Audio: React.FC<{
audioRef.current.volume = volume;
audioRef.current.muted = muted;
}, [volume, muted]);
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = muted ? 0 : volume;
}
}, [volume, muted, gainNodeRef]);
useEffect(() => {
if (typeof visible !== 'undefined') {
@@ -192,11 +199,10 @@ export const Audio: React.FC<{
}, [visible, sensitive]);
useEffect(() => {
if (!revealed && audioRef.current) {
audioRef.current.pause();
suspendAudio();
if (!revealed) {
pauseAudio();
}
}, [suspendAudio, revealed]);
}, [pauseAudio, revealed]);
useEffect(() => {
let nextFrame: ReturnType<typeof requestAnimationFrame>;
@@ -206,7 +212,6 @@ export const Audio: React.FC<{
if (audioRef.current && audioRef.current.duration > 0) {
void spring.start({
progress: `${(audioRef.current.currentTime / audioRef.current.duration) * 100}%`,
immediate: reduceMotion,
config: config.stiff,
});
}
@@ -228,13 +233,11 @@ export const Audio: React.FC<{
}
if (audioRef.current.paused) {
resumeAudio();
void audioRef.current.play();
playAudio();
} else {
audioRef.current.pause();
suspendAudio();
pauseAudio();
}
}, [resumeAudio, suspendAudio]);
}, [playAudio, pauseAudio]);
const handlePlay = useCallback(() => {
setPaused(false);
@@ -254,7 +257,6 @@ export const Audio: React.FC<{
if (lastTimeRange > -1) {
void spring.start({
buffer: `${Math.ceil(audioRef.current.buffered.end(lastTimeRange) / audioRef.current.duration) * 100}%`,
immediate: reduceMotion,
});
}
}, [spring]);
@@ -269,7 +271,6 @@ export const Audio: React.FC<{
void spring.start({
volume: `${audioRef.current.muted ? 0 : audioRef.current.volume * 100}%`,
immediate: reduceMotion,
});
persistVolume(audioRef.current.volume, audioRef.current.muted);
@@ -349,8 +350,7 @@ export const Audio: React.FC<{
document.removeEventListener('mouseup', handleSeekMouseUp, true);
setDragging(false);
resumeAudio();
void audioRef.current?.play();
playAudio();
};
const handleSeekMouseMove = (e: MouseEvent) => {
@@ -377,7 +377,7 @@ export const Audio: React.FC<{
e.preventDefault();
e.stopPropagation();
},
[setDragging, spring, resumeAudio],
[playAudio, spring],
);
const handleMouseEnter = useCallback(() => {
@@ -446,10 +446,9 @@ export const Audio: React.FC<{
const handleCanPlayThrough = useCallback(() => {
if (startPlaying) {
resumeAudio();
void audioRef.current?.play();
playAudio();
}
}, [startPlaying, resumeAudio]);
}, [startPlaying, playAudio]);
const seekBy = (time: number) => {
if (!audioRef.current) {
@@ -492,7 +491,7 @@ export const Audio: React.FC<{
return;
}
const newVolume = audioRef.current.volume + step;
const newVolume = Math.max(0, audioRef.current.volume + step);
if (!isNaN(newVolume)) {
audioRef.current.volume = newVolume;

View File

@@ -1,81 +0,0 @@
import { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
import { Dropdown } from 'flavours/glitch/components/dropdown_menu';
import { useAppDispatch } from 'flavours/glitch/store';
const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
preferences: {
id: 'navigation_bar.preferences',
defaultMessage: 'Preferences',
},
follow_requests: {
id: 'navigation_bar.follow_requests',
defaultMessage: 'Follow requests',
},
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: {
id: 'navigation_bar.followed_tags',
defaultMessage: 'Followed hashtags',
},
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: {
id: 'navigation_bar.domain_blocks',
defaultMessage: 'Blocked domains',
},
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
});
export const ActionBar: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const menu = useMemo(() => {
const handleLogoutClick = () => {
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} }));
};
return [
{
text: intl.formatMessage(messages.edit_profile),
href: '/settings/profile',
},
{
text: intl.formatMessage(messages.preferences),
href: '/settings/preferences',
},
null,
{
text: intl.formatMessage(messages.follow_requests),
to: '/follow_requests',
},
{ text: intl.formatMessage(messages.favourites), to: '/favourites' },
{ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' },
{ text: intl.formatMessage(messages.lists), to: '/lists' },
{
text: intl.formatMessage(messages.followed_tags),
to: '/followed_tags',
},
null,
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
{
text: intl.formatMessage(messages.domain_blocks),
to: '/domain_blocks',
},
{ text: intl.formatMessage(messages.filters), href: '/filters' },
null,
{ text: intl.formatMessage(messages.logout), action: handleLogoutClick },
];
}, [intl, dispatch]);
return <Dropdown items={menu} icon='bars' iconComponent={MoreHorizIcon} />;
};

View File

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

View File

@@ -2,34 +2,44 @@ import { useCallback } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { useSelector, useDispatch } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
import { Account } from 'flavours/glitch/components/account';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { me } from 'flavours/glitch/initial_state';
import { ActionBar } from './action_bar';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
});
export const NavigationBar = () => {
const dispatch = useDispatch();
export const NavigationBar: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
const isReplying = useAppSelector(
(state) => !!state.compose.get('in_reply_to'),
);
const handleCancelClick = useCallback(() => {
dispatch(cancelReplyCompose());
}, [dispatch]);
if (!me) {
return null;
}
return (
<div className='navigation-bar'>
<Account id={me} minimal />
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
{isReplying && (
<IconButton
title={intl.formatMessage(messages.cancel)}
icon=''
iconComponent={CloseIcon}
onClick={handleCancelClick}
/>
)}
</div>
);
};

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