From a0e237a96fca2774d3c9ed43063a45e395bb7f40 Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Mon, 8 Jan 2024 11:57:40 +0100
Subject: [PATCH] Upgrade Redux packages (#28585)

---
 .eslintrc.js                                  |  11 +-
 .../emoji_picker_dropdown_container.js        |   3 +-
 .../containers/language_dropdown_container.js |   3 +-
 .../containers/announcements_container.js     |   3 +-
 .../mastodon/features/home_timeline/index.jsx |   2 +-
 .../mastodon/features/list_adder/index.jsx    |   2 +-
 .../mastodon/features/lists/index.jsx         |   2 +-
 .../mastodon/features/notifications/index.jsx |   2 +-
 .../mastodon/features/report/comment.jsx      |   2 +-
 .../mastodon/features/status/index.jsx        |   2 +-
 .../subscribed_languages_modal/index.jsx      |   2 +-
 .../features/ui/components/list_panel.jsx     |   2 +-
 .../ui/containers/status_list_container.js    |   2 +-
 app/javascript/mastodon/reducers/accounts.ts  |   3 +-
 app/javascript/mastodon/reducers/modal.ts     |   3 +-
 .../mastodon/reducers/relationships.ts        |   5 +-
 app/javascript/mastodon/selectors/accounts.ts |   2 +-
 app/javascript/mastodon/selectors/index.js    |   2 +-
 .../mastodon/store/middlewares/errors.ts      |  32 ++++--
 .../mastodon/store/middlewares/loading_bar.ts |  24 +++-
 .../mastodon/store/middlewares/sounds.ts      |  35 ++++--
 .../mastodon/store/typed_functions.ts         |   6 +-
 config/webpack/rules/babel.js                 |   8 +-
 package.json                                  |   9 +-
 yarn.lock                                     | 104 +++++++++---------
 25 files changed, 167 insertions(+), 104 deletions(-)

diff --git a/.eslintrc.js b/.eslintrc.js
index e2d16a54a8..1b36bcee25 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -245,7 +245,7 @@ module.exports = defineConfig({
           },
           // Immutable / Redux / data store
           {
-            pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}',
+            pattern: '{immutable,@reduxjs/toolkit,react-redux,react-immutable-proptypes,react-immutable-pure-component}',
             group: 'external',
             position: 'before',
           },
@@ -353,7 +353,14 @@ module.exports = defineConfig({
         '@typescript-eslint/consistent-type-exports': 'error',
         '@typescript-eslint/consistent-type-imports': 'error',
         "@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }],
-
+        "@typescript-eslint/no-restricted-imports": [
+          "warn",
+          {
+            "name": "react-redux",
+            "importNames": ["useSelector", "useDispatch"],
+            "message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
+          }
+        ],
         'jsdoc/require-jsdoc': 'off',
 
         // Those rules set stricter rules for TS files
diff --git a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
index a0e50029df..8cf906b20a 100644
--- a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
@@ -1,6 +1,7 @@
+import { createSelector } from '@reduxjs/toolkit';
 import { Map as ImmutableMap } from 'immutable';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
+
 
 import { useEmoji } from '../../../actions/emojis';
 import { changeSetting } from '../../../actions/settings';
diff --git a/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js
index e1e2f04024..ba4b5f05a5 100644
--- a/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js
@@ -1,6 +1,7 @@
+import { createSelector } from '@reduxjs/toolkit';
 import { Map as ImmutableMap } from 'immutable';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
+
 
 import { changeComposeLanguage } from 'mastodon/actions/compose';
 import { useLanguage } from 'mastodon/actions/languages';
diff --git a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js
index c33e624324..3bb1b8e8d1 100644
--- a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js
+++ b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js
@@ -1,6 +1,7 @@
+import { createSelector } from '@reduxjs/toolkit';
 import { Map as ImmutableMap } from 'immutable';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
+
 
 import { addReaction, removeReaction, dismissAnnouncement } from 'mastodon/actions/announcements';
 
diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx
index 7c9c7a4e52..613eb4b896 100644
--- a/app/javascript/mastodon/features/home_timeline/index.jsx
+++ b/app/javascript/mastodon/features/home_timeline/index.jsx
@@ -6,9 +6,9 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import classNames from 'classnames';
 import { Helmet } from 'react-helmet';
 
+import { createSelector } from '@reduxjs/toolkit';
 import { List as ImmutableList } from 'immutable';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import { ReactComponent as CampaignIcon } from '@material-symbols/svg-600/outlined/campaign.svg';
 import { ReactComponent as HomeIcon } from '@material-symbols/svg-600/outlined/home-fill.svg';
diff --git a/app/javascript/mastodon/features/list_adder/index.jsx b/app/javascript/mastodon/features/list_adder/index.jsx
index 1ba9972e00..4e7bd46bdf 100644
--- a/app/javascript/mastodon/features/list_adder/index.jsx
+++ b/app/javascript/mastodon/features/list_adder/index.jsx
@@ -2,10 +2,10 @@ import PropTypes from 'prop-types';
 
 import { injectIntl } from 'react-intl';
 
+import { createSelector } from '@reduxjs/toolkit';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import { setupListAdder, resetListAdder } from '../../actions/lists';
 import NewListForm from '../lists/components/new_list_form';
diff --git a/app/javascript/mastodon/features/lists/index.jsx b/app/javascript/mastodon/features/lists/index.jsx
index 58e85b4d28..9014394351 100644
--- a/app/javascript/mastodon/features/lists/index.jsx
+++ b/app/javascript/mastodon/features/lists/index.jsx
@@ -4,10 +4,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
 
+import { createSelector } from '@reduxjs/toolkit';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg';
 
diff --git a/app/javascript/mastodon/features/notifications/index.jsx b/app/javascript/mastodon/features/notifications/index.jsx
index 379932b7b7..762c96ccca 100644
--- a/app/javascript/mastodon/features/notifications/index.jsx
+++ b/app/javascript/mastodon/features/notifications/index.jsx
@@ -5,10 +5,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
 
+import { createSelector } from '@reduxjs/toolkit';
 import { List as ImmutableList } from 'immutable';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import { ReactComponent as DoneAllIcon } from '@material-symbols/svg-600/outlined/done_all.svg';
 import { ReactComponent as NotificationsIcon } from '@material-symbols/svg-600/outlined/notifications-fill.svg';
diff --git a/app/javascript/mastodon/features/report/comment.jsx b/app/javascript/mastodon/features/report/comment.jsx
index ec59746923..b80c14fcb9 100644
--- a/app/javascript/mastodon/features/report/comment.jsx
+++ b/app/javascript/mastodon/features/report/comment.jsx
@@ -3,10 +3,10 @@ import { useCallback, useEffect, useRef } from 'react';
 
 import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
 
+import { createSelector } from '@reduxjs/toolkit';
 import { OrderedSet, List as ImmutableList } from 'immutable';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { shallowEqual } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import Toggle from 'react-toggle';
 
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index c581255c99..3832489764 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -6,11 +6,11 @@ import classNames from 'classnames';
 import { Helmet } from 'react-helmet';
 import { withRouter } from 'react-router-dom';
 
+import { createSelector } from '@reduxjs/toolkit';
 import Immutable from 'immutable';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg';
 import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
diff --git a/app/javascript/mastodon/features/subscribed_languages_modal/index.jsx b/app/javascript/mastodon/features/subscribed_languages_modal/index.jsx
index 147e11d1be..ac34f7986c 100644
--- a/app/javascript/mastodon/features/subscribed_languages_modal/index.jsx
+++ b/app/javascript/mastodon/features/subscribed_languages_modal/index.jsx
@@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
 
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 
+import { createSelector } from '@reduxjs/toolkit';
 import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
 
diff --git a/app/javascript/mastodon/features/ui/components/list_panel.jsx b/app/javascript/mastodon/features/ui/components/list_panel.jsx
index 8dbd28f094..ff600730a0 100644
--- a/app/javascript/mastodon/features/ui/components/list_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/list_panel.jsx
@@ -1,9 +1,9 @@
 import PropTypes from 'prop-types';
 
+import { createSelector } from '@reduxjs/toolkit';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg';
 
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js
index 36a8f58f8b..3e7ae2add0 100644
--- a/app/javascript/mastodon/features/ui/containers/status_list_container.js
+++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js
@@ -1,6 +1,6 @@
+import { createSelector } from '@reduxjs/toolkit';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
 
 import { debounce } from 'lodash';
 
diff --git a/app/javascript/mastodon/reducers/accounts.ts b/app/javascript/mastodon/reducers/accounts.ts
index f7270eb60a..e6f07340c0 100644
--- a/app/javascript/mastodon/reducers/accounts.ts
+++ b/app/javascript/mastodon/reducers/accounts.ts
@@ -1,7 +1,6 @@
+import type { Reducer } from '@reduxjs/toolkit';
 import { Map as ImmutableMap } from 'immutable';
 
-import type { Reducer } from 'redux';
-
 import {
   followAccountSuccess,
   unfollowAccountSuccess,
diff --git a/app/javascript/mastodon/reducers/modal.ts b/app/javascript/mastodon/reducers/modal.ts
index 73a2afb916..368f26542c 100644
--- a/app/javascript/mastodon/reducers/modal.ts
+++ b/app/javascript/mastodon/reducers/modal.ts
@@ -1,6 +1,5 @@
-import { Record as ImmutableRecord, Stack } from 'immutable';
-
 import type { Reducer } from '@reduxjs/toolkit';
+import { Record as ImmutableRecord, Stack } from 'immutable';
 
 import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
 import type { ModalType } from '../actions/modal';
diff --git a/app/javascript/mastodon/reducers/relationships.ts b/app/javascript/mastodon/reducers/relationships.ts
index 2ba61839c7..dcca11b203 100644
--- a/app/javascript/mastodon/reducers/relationships.ts
+++ b/app/javascript/mastodon/reducers/relationships.ts
@@ -1,7 +1,6 @@
-import { Map as ImmutableMap } from 'immutable';
-
 import { isFulfilled } from '@reduxjs/toolkit';
-import type { Reducer } from 'redux';
+import type { Reducer } from '@reduxjs/toolkit';
+import { Map as ImmutableMap } from 'immutable';
 
 import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
 import type { Account } from 'mastodon/models/account';
diff --git a/app/javascript/mastodon/selectors/accounts.ts b/app/javascript/mastodon/selectors/accounts.ts
index 66193136c4..cee3a87bca 100644
--- a/app/javascript/mastodon/selectors/accounts.ts
+++ b/app/javascript/mastodon/selectors/accounts.ts
@@ -1,5 +1,5 @@
+import { createSelector } from '@reduxjs/toolkit';
 import { Record as ImmutableRecord } from 'immutable';
-import { createSelector } from 'reselect';
 
 import { accountDefaultValues } from 'mastodon/models/account';
 import type { Account, AccountShape } from 'mastodon/models/account';
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 8a07ba774d..b1c60403e9 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -1,5 +1,5 @@
+import { createSelector } from '@reduxjs/toolkit';
 import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
-import { createSelector } from 'reselect';
 
 import { toServerSideType } from 'mastodon/utils/filters';
 
diff --git a/app/javascript/mastodon/store/middlewares/errors.ts b/app/javascript/mastodon/store/middlewares/errors.ts
index 9f28f5ff53..e11aa78178 100644
--- a/app/javascript/mastodon/store/middlewares/errors.ts
+++ b/app/javascript/mastodon/store/middlewares/errors.ts
@@ -1,20 +1,34 @@
-import type { AnyAction, Middleware } from 'redux';
+import { isAction } from '@reduxjs/toolkit';
+import type { Action, Middleware } from '@reduxjs/toolkit';
 
 import type { RootState } from '..';
 import { showAlertForError } from '../../actions/alerts';
 
 const defaultFailSuffix = 'FAIL';
+const isFailedAction = new RegExp(`${defaultFailSuffix}$`, 'g');
 
-export const errorsMiddleware: Middleware<unknown, RootState> =
+interface ActionWithMaybeAlertParams extends Action {
+  skipAlert?: boolean;
+  skipNotFound?: boolean;
+  error?: unknown;
+}
+
+function isActionWithmaybeAlertParams(
+  action: unknown,
+): action is ActionWithMaybeAlertParams {
+  return isAction(action);
+}
+
+export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
   ({ dispatch }) =>
   (next) =>
-  (action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => {
-    if (action.type && !action.skipAlert) {
-      const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
-
-      if (typeof action.type === 'string' && action.type.match(isFail)) {
-        dispatch(showAlertForError(action.error, action.skipNotFound));
-      }
+  (action) => {
+    if (
+      isActionWithmaybeAlertParams(action) &&
+      !action.skipAlert &&
+      action.type.match(isFailedAction)
+    ) {
+      dispatch(showAlertForError(action.error, action.skipNotFound));
     }
 
     return next(action);
diff --git a/app/javascript/mastodon/store/middlewares/loading_bar.ts b/app/javascript/mastodon/store/middlewares/loading_bar.ts
index 83056ee49f..d259be899b 100644
--- a/app/javascript/mastodon/store/middlewares/loading_bar.ts
+++ b/app/javascript/mastodon/store/middlewares/loading_bar.ts
@@ -3,9 +3,11 @@ import {
   isPending as isThunkActionPending,
   isFulfilled as isThunkActionFulfilled,
   isRejected as isThunkActionRejected,
+  isAction,
 } from '@reduxjs/toolkit';
+import type { Middleware, UnknownAction } from '@reduxjs/toolkit';
+
 import { showLoading, hideLoading } from 'react-redux-loading-bar';
-import type { AnyAction, Middleware } from 'redux';
 
 import type { RootState } from '..';
 
@@ -19,14 +21,28 @@ const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
   'REJECTED',
 ];
 
+interface ActionWithSkipLoading extends UnknownAction {
+  skipLoading: boolean;
+}
+
+function isActionWithSkipLoading(
+  action: unknown,
+): action is ActionWithSkipLoading {
+  return (
+    isAction(action) &&
+    'skipLoading' in action &&
+    typeof action.skipLoading === 'boolean'
+  );
+}
+
 export const loadingBarMiddleware = (
   config: Config = {},
-): Middleware<unknown, RootState> => {
+): Middleware<{ skipLoading?: boolean }, RootState> => {
   const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes;
 
   return ({ dispatch }) =>
     (next) =>
-    (action: AnyAction) => {
+    (action) => {
       let isPending = false;
       let isFulfilled = false;
       let isRejected = false;
@@ -39,7 +55,7 @@ export const loadingBarMiddleware = (
         else if (isThunkActionFulfilled(action)) isFulfilled = true;
         else if (isThunkActionRejected(action)) isRejected = true;
       } else if (
-        action.type &&
+        isActionWithSkipLoading(action) &&
         !action.skipLoading &&
         typeof action.type === 'string'
       ) {
diff --git a/app/javascript/mastodon/store/middlewares/sounds.ts b/app/javascript/mastodon/store/middlewares/sounds.ts
index 09ade7d753..51839f427a 100644
--- a/app/javascript/mastodon/store/middlewares/sounds.ts
+++ b/app/javascript/mastodon/store/middlewares/sounds.ts
@@ -1,4 +1,5 @@
-import type { Middleware, AnyAction } from 'redux';
+import { isAction } from '@reduxjs/toolkit';
+import type { Middleware, UnknownAction } from '@reduxjs/toolkit';
 
 import ready from 'mastodon/ready';
 import { assetHost } from 'mastodon/utils/config';
@@ -10,6 +11,21 @@ interface AudioSource {
   type: string;
 }
 
+interface ActionWithMetaSound extends UnknownAction {
+  meta: { sound: string };
+}
+
+function isActionWithMetaSound(action: unknown): action is ActionWithMetaSound {
+  return (
+    isAction(action) &&
+    'meta' in action &&
+    typeof action.meta === 'object' &&
+    !!action.meta &&
+    'sound' in action.meta &&
+    typeof action.meta.sound === 'string'
+  );
+}
+
 const createAudio = (sources: AudioSource[]) => {
   const audio = new Audio();
   sources.forEach(({ type, src }) => {
@@ -34,7 +50,10 @@ const play = (audio: HTMLAudioElement) => {
   void audio.play();
 };
 
-export const soundsMiddleware = (): Middleware<unknown, RootState> => {
+export const soundsMiddleware = (): Middleware<
+  Record<string, never>,
+  RootState
+> => {
   const soundCache: Record<string, HTMLAudioElement> = {};
 
   void ready(() => {
@@ -50,15 +69,15 @@ export const soundsMiddleware = (): Middleware<unknown, RootState> => {
     ]);
   });
 
-  return () =>
-    (next) =>
-    (action: AnyAction & { meta?: { sound?: string } }) => {
-      const sound = action.meta?.sound;
+  return () => (next) => (action) => {
+    if (isActionWithMetaSound(action)) {
+      const sound = action.meta.sound;
 
       if (sound && Object.hasOwn(soundCache, sound)) {
         play(soundCache[sound]);
       }
+    }
 
-      return next(action);
-    };
+    return next(action);
+  };
 };
diff --git a/app/javascript/mastodon/store/typed_functions.ts b/app/javascript/mastodon/store/typed_functions.ts
index f1e71385a8..46a10b8b47 100644
--- a/app/javascript/mastodon/store/typed_functions.ts
+++ b/app/javascript/mastodon/store/typed_functions.ts
@@ -1,7 +1,7 @@
-import type { TypedUseSelectorHook } from 'react-redux';
-import { useDispatch, useSelector } from 'react-redux';
-
 import { createAsyncThunk } from '@reduxjs/toolkit';
+import type { TypedUseSelectorHook } from 'react-redux';
+// eslint-disable-next-line @typescript-eslint/no-restricted-imports
+import { useDispatch, useSelector } from 'react-redux';
 
 import type { AppDispatch, RootState } from './store';
 
diff --git a/config/webpack/rules/babel.js b/config/webpack/rules/babel.js
index 2811a1674d..c7bf886e79 100644
--- a/config/webpack/rules/babel.js
+++ b/config/webpack/rules/babel.js
@@ -7,8 +7,14 @@ module.exports = {
   include: [
     settings.source_path,
     ...settings.resolved_paths,
+    'node_modules/@reduxjs'
   ].map(p => resolve(p)),
-  exclude: /node_modules/,
+  exclude: function(modulePath) {
+    return (
+      /node_modules/.test(modulePath) &&
+      !/@reduxjs/.test(modulePath)
+    );
+  },
   use: [
     {
       loader: 'babel-loader',
diff --git a/package.json b/package.json
index a5b6a228ca..16ed4decaa 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
     "@github/webauthn-json": "^2.1.1",
     "@material-symbols/svg-600": "^0.14.0",
     "@rails/ujs": "^7.1.1",
-    "@reduxjs/toolkit": "^1.9.5",
+    "@reduxjs/toolkit": "^2.0.1",
     "@svgr/webpack": "^5.5.0",
     "arrow-key-navigation": "^1.2.0",
     "async-mutex": "^0.4.0",
@@ -106,8 +106,8 @@
     "react-motion": "^0.5.2",
     "react-notification": "^6.8.5",
     "react-overlays": "^5.2.1",
-    "react-redux": "^8.0.4",
-    "react-redux-loading-bar": "^5.0.4",
+    "react-redux": "^9.0.4",
+    "react-redux-loading-bar": "^5.0.8",
     "react-router": "^5.3.4",
     "react-router-dom": "^5.3.4",
     "react-router-scroll-4": "^1.0.0-beta.1",
@@ -116,12 +116,9 @@
     "react-swipeable-views": "^0.14.0",
     "react-textarea-autosize": "^8.4.1",
     "react-toggle": "^4.1.3",
-    "redux": "^4.2.1",
     "redux-immutable": "^4.0.0",
-    "redux-thunk": "^2.4.2",
     "regenerator-runtime": "^0.14.0",
     "requestidlecallback": "^0.3.0",
-    "reselect": "^4.1.8",
     "rimraf": "^5.0.1",
     "sass": "^1.62.1",
     "sass-loader": "^10.2.0",
diff --git a/yarn.lock b/yarn.lock
index 70d98332ad..71ed44a446 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1483,7 +1483,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
+"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
   version: 7.23.7
   resolution: "@babel/runtime@npm:7.23.7"
   dependencies:
@@ -2302,7 +2302,7 @@ __metadata:
     "@github/webauthn-json": "npm:^2.1.1"
     "@material-symbols/svg-600": "npm:^0.14.0"
     "@rails/ujs": "npm:^7.1.1"
-    "@reduxjs/toolkit": "npm:^1.9.5"
+    "@reduxjs/toolkit": "npm:^2.0.1"
     "@svgr/webpack": "npm:^5.5.0"
     "@testing-library/jest-dom": "npm:^6.0.0"
     "@testing-library/react": "npm:^14.0.0"
@@ -2410,8 +2410,8 @@ __metadata:
     react-motion: "npm:^0.5.2"
     react-notification: "npm:^6.8.5"
     react-overlays: "npm:^5.2.1"
-    react-redux: "npm:^8.0.4"
-    react-redux-loading-bar: "npm:^5.0.4"
+    react-redux: "npm:^9.0.4"
+    react-redux-loading-bar: "npm:^5.0.8"
     react-router: "npm:^5.3.4"
     react-router-dom: "npm:^5.3.4"
     react-router-scroll-4: "npm:^1.0.0-beta.1"
@@ -2421,12 +2421,9 @@ __metadata:
     react-test-renderer: "npm:^18.2.0"
     react-textarea-autosize: "npm:^8.4.1"
     react-toggle: "npm:^4.1.3"
-    redux: "npm:^4.2.1"
     redux-immutable: "npm:^4.0.0"
-    redux-thunk: "npm:^2.4.2"
     regenerator-runtime: "npm:^0.14.0"
     requestidlecallback: "npm:^0.3.0"
-    reselect: "npm:^4.1.8"
     rimraf: "npm:^5.0.1"
     sass: "npm:^1.62.1"
     sass-loader: "npm:^10.2.0"
@@ -2622,23 +2619,23 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@reduxjs/toolkit@npm:^1.9.5":
-  version: 1.9.7
-  resolution: "@reduxjs/toolkit@npm:1.9.7"
+"@reduxjs/toolkit@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "@reduxjs/toolkit@npm:2.0.1"
   dependencies:
-    immer: "npm:^9.0.21"
-    redux: "npm:^4.2.1"
-    redux-thunk: "npm:^2.4.2"
-    reselect: "npm:^4.1.8"
+    immer: "npm:^10.0.3"
+    redux: "npm:^5.0.0"
+    redux-thunk: "npm:^3.1.0"
+    reselect: "npm:^5.0.1"
   peerDependencies:
     react: ^16.9.0 || ^17.0.0 || ^18
-    react-redux: ^7.2.1 || ^8.0.2
+    react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
   peerDependenciesMeta:
     react:
       optional: true
     react-redux:
       optional: true
-  checksum: fa0aa4b7c6973ac87ce0ac7e45faa02c73b66c4ee0bc950d178494539a42a1bb908d109297102458b7ea14d5e7dae356e7a7ce9a1b9849b0e8451e6dd70fca9c
+  checksum: 161b9b8e11d9688890ab97b604a4c10c0d41b1369425a5fa821586932db4cd5a391d15799732b3612e6120a6336458ff577ff254219315c05ecd68da5d15fd79
   languageName: node
   linkType: hard
 
@@ -9128,10 +9125,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"immer@npm:^9.0.21":
-  version: 9.0.21
-  resolution: "immer@npm:9.0.21"
-  checksum: 03ea3ed5d4d72e8bd428df4a38ad7e483ea8308e9a113d3b42e0ea2cc0cc38340eb0a6aca69592abbbf047c685dbda04e3d34bf2ff438ab57339ed0a34cc0a05
+"immer@npm:^10.0.3":
+  version: 10.0.3
+  resolution: "immer@npm:10.0.3"
+  checksum: 282a4f8479a40f7d12b2b3243c095e3e892bf99058e2ffcdd6b8e9fd143e6a90f2717ab9b6c8b97c927ffb8054465c8f647056f41660dbfd672e240cf1063503
   languageName: node
   linkType: hard
 
@@ -13104,7 +13101,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.15, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
+"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
+  version: 6.0.13
+  resolution: "postcss-selector-parser@npm:6.0.13"
+  dependencies:
+    cssesc: "npm:^3.0.0"
+    util-deprecate: "npm:^1.0.2"
+  checksum: 51f099b27f7c7198ea1826470ef0adfa58b3bd3f59b390fda123baa0134880a5fa9720137b6009c4c1373357b144f700b0edac73335d0067422063129371444e
+  languageName: node
+  linkType: hard
+
+"postcss-selector-parser@npm:^6.0.15":
   version: 6.0.15
   resolution: "postcss-selector-parser@npm:6.0.15"
   dependencies:
@@ -13693,7 +13700,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-redux-loading-bar@npm:^5.0.4":
+"react-redux-loading-bar@npm:^5.0.8":
   version: 5.0.8
   resolution: "react-redux-loading-bar@npm:5.0.8"
   dependencies:
@@ -13708,35 +13715,25 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-redux@npm:^8.0.4":
-  version: 8.1.3
-  resolution: "react-redux@npm:8.1.3"
+"react-redux@npm:^9.0.4":
+  version: 9.0.4
+  resolution: "react-redux@npm:9.0.4"
   dependencies:
-    "@babel/runtime": "npm:^7.12.1"
-    "@types/hoist-non-react-statics": "npm:^3.3.1"
     "@types/use-sync-external-store": "npm:^0.0.3"
-    hoist-non-react-statics: "npm:^3.3.2"
-    react-is: "npm:^18.0.0"
     use-sync-external-store: "npm:^1.0.0"
   peerDependencies:
-    "@types/react": ^16.8 || ^17.0 || ^18.0
-    "@types/react-dom": ^16.8 || ^17.0 || ^18.0
-    react: ^16.8 || ^17.0 || ^18.0
-    react-dom: ^16.8 || ^17.0 || ^18.0
-    react-native: ">=0.59"
-    redux: ^4 || ^5.0.0-beta.0
+    "@types/react": ^18.2.25
+    react: ^18.0
+    react-native: ">=0.69"
+    redux: ^5.0.0
   peerDependenciesMeta:
     "@types/react":
       optional: true
-    "@types/react-dom":
-      optional: true
-    react-dom:
-      optional: true
     react-native:
       optional: true
     redux:
       optional: true
-  checksum: 64c8be2765568dc66a3c442a41dd0ed74fe048d5ceb7a4fe72e5bac3d3687996a7115f57b5156af7406521087065a0e60f9194318c8ca99c55e9ce48558980ce
+  checksum: 23af10014b129aeb051de729bde01de21175170b860deefb7ad83483feab5816253f770a4cea93333fc22a53ac9ac699b27f5c3705c388dab53dbcb2906a571a
   languageName: node
   linkType: hard
 
@@ -14039,16 +14036,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"redux-thunk@npm:^2.4.2":
-  version: 2.4.2
-  resolution: "redux-thunk@npm:2.4.2"
+"redux-thunk@npm:^3.1.0":
+  version: 3.1.0
+  resolution: "redux-thunk@npm:3.1.0"
   peerDependencies:
-    redux: ^4
-  checksum: e202d6ef7dfa7df08ed24cb221aa89d6c84dbaa7d65fe90dbd8e826d0c10d801f48388f9a7598a4fd970ecbc93d335014570a61ca7bc8bf569eab5de77b31a3c
+    redux: ^5.0.0
+  checksum: 21557f6a30e1b2e3e470933247e51749be7f1d5a9620069a3125778675ce4d178d84bdee3e2a0903427a5c429e3aeec6d4df57897faf93eb83455bc1ef7b66fd
   languageName: node
   linkType: hard
 
-"redux@npm:^4.0.0, redux@npm:^4.2.1":
+"redux@npm:^4.0.0":
   version: 4.2.1
   resolution: "redux@npm:4.2.1"
   dependencies:
@@ -14057,6 +14054,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"redux@npm:^5.0.0":
+  version: 5.0.1
+  resolution: "redux@npm:5.0.1"
+  checksum: b10c28357194f38e7d53b760ed5e64faa317cc63de1fb95bc5d9e127fab956392344368c357b8e7a9bedb0c35b111e7efa522210cfdc3b3c75e5074718e9069c
+  languageName: node
+  linkType: hard
+
 "reflect.getprototypeof@npm:^1.0.4":
   version: 1.0.4
   resolution: "reflect.getprototypeof@npm:1.0.4"
@@ -14226,10 +14230,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"reselect@npm:^4.1.8":
-  version: 4.1.8
-  resolution: "reselect@npm:4.1.8"
-  checksum: 06a305a504affcbb67dd0561ddc8306b35796199c7e15b38934c80606938a021eadcf68cfd58e7bb5e17786601c37602a3362a4665c7bf0a96c1041ceee9d0b7
+"reselect@npm:^5.0.1":
+  version: 5.0.1
+  resolution: "reselect@npm:5.0.1"
+  checksum: 0724b4555cd6411849de334a75177780f127af849eb71c4b709966d07ade8090d125c0c926dc6cf936866d23ebadda6aad1da93cd8340525323b889f25d56d51
   languageName: node
   linkType: hard