diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js
deleted file mode 100644
index 1fc2e391e2..0000000000
--- a/app/javascript/mastodon/actions/boosts.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { openModal } from './modal';
-
-export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
-export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
-
-export function initBoostModal(props) {
-  return (dispatch, getState) => {
-    const default_privacy = getState().getIn(['compose', 'default_privacy']);
-
-    const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
-
-    dispatch({
-      type: BOOSTS_INIT_MODAL,
-      privacy,
-    });
-
-    dispatch(openModal({
-      modalType: 'BOOST',
-      modalProps: props,
-    }));
-  };
-}
-
-
-export function changeBoostPrivacy(privacy) {
-  return dispatch => {
-    dispatch({
-      type: BOOSTS_CHANGE_PRIVACY,
-      privacy,
-    });
-  };
-}
diff --git a/app/javascript/mastodon/components/visibility_icon.tsx b/app/javascript/mastodon/components/visibility_icon.tsx
index 753dc02737..3a310cbae2 100644
--- a/app/javascript/mastodon/components/visibility_icon.tsx
+++ b/app/javascript/mastodon/components/visibility_icon.tsx
@@ -4,11 +4,10 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
 import LockIcon from '@/material-icons/400-24px/lock.svg?react';
 import PublicIcon from '@/material-icons/400-24px/public.svg?react';
 import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
+import type { StatusVisibility } from 'mastodon/models/status';
 
 import { Icon } from './icon';
 
-type Visibility = 'public' | 'unlisted' | 'private' | 'direct';
-
 const messages = defineMessages({
   public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
   unlisted_short: {
@@ -25,7 +24,7 @@ const messages = defineMessages({
   },
 });
 
-export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
+export const VisibilityIcon: React.FC<{ visibility: StatusVisibility }> = ({
   visibility,
 }) => {
   const intl = useIntl();
diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx
index da93a16b08..c6842e8df8 100644
--- a/app/javascript/mastodon/containers/status_container.jsx
+++ b/app/javascript/mastodon/containers/status_container.jsx
@@ -8,7 +8,6 @@ import {
 } from '../actions/accounts';
 import { showAlertForError } from '../actions/alerts';
 import { initBlockModal } from '../actions/blocks';
-import { initBoostModal } from '../actions/boosts';
 import {
   replyCompose,
   mentionCompose,
@@ -107,7 +106,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
     if ((e && e.shiftKey) || !boostModal) {
       this.onModalReblog(status);
     } else {
-      dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
+      dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
     }
   },
 
diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js
index 4458fd7bc3..de450cd1ab 100644
--- a/app/javascript/mastodon/features/notifications/containers/notification_container.js
+++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js
@@ -1,6 +1,5 @@
 import { connect } from 'react-redux';
 
-import { initBoostModal } from '../../../actions/boosts';
 import { mentionCompose } from '../../../actions/compose';
 import {
   reblog,
@@ -8,6 +7,7 @@ import {
   unreblog,
   unfavourite,
 } from '../../../actions/interactions';
+import { openModal } from '../../../actions/modal';
 import {
   hideStatus,
   revealStatus,
@@ -49,7 +49,7 @@ const mapDispatchToProps = dispatch => ({
       if (e.shiftKey || !boostModal) {
         this.onModalReblog(status);
       } else {
-        dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
+        dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
       }
     }
   },
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
index a7d8356be7..7a163a8825 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
@@ -14,7 +14,6 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
 import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
 import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
 import StarIcon from '@/material-icons/400-24px/star.svg?react';
-import { initBoostModal } from 'mastodon/actions/boosts';
 import { replyCompose } from 'mastodon/actions/compose';
 import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
 import { openModal } from 'mastodon/actions/modal';
@@ -140,7 +139,7 @@ class Footer extends ImmutablePureComponent {
       } else if ((e && e.shiftKey) || !boostModal) {
         this._performReblog(status);
       } else {
-        dispatch(initBoostModal({ status, onReblog: this._performReblog }));
+        dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this._performReblog } }));
       }
     } else {
       dispatch(openModal({
diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
index 3e1f8d4d29..1c650f544f 100644
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
 
 import { showAlertForError } from '../../../actions/alerts';
 import { initBlockModal } from '../../../actions/blocks';
-import { initBoostModal } from '../../../actions/boosts';
 import {
   replyCompose,
   mentionCompose,
@@ -85,7 +84,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
       if (e.shiftKey || !boostModal) {
         this.onModalReblog(status);
       } else {
-        dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
+        dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
       }
     }
   },
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index 44db9d9c3f..3914759725 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -27,7 +27,6 @@ import {
   unmuteAccount,
 } from '../../actions/accounts';
 import { initBlockModal } from '../../actions/blocks';
-import { initBoostModal } from '../../actions/boosts';
 import {
   replyCompose,
   mentionCompose,
@@ -317,7 +316,7 @@ class Status extends ImmutablePureComponent {
         if ((e && e.shiftKey) || !boostModal) {
           this.handleModalReblog(status);
         } else {
-          dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
+          dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.handleModalReblog } }));
         }
       }
     } else {
diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.jsx b/app/javascript/mastodon/features/ui/components/boost_modal.jsx
deleted file mode 100644
index 3b3e1e3f97..0000000000
--- a/app/javascript/mastodon/features/ui/components/boost_modal.jsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import classNames from 'classnames';
-import { withRouter } from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
-import { changeBoostPrivacy } from 'mastodon/actions/boosts';
-import AttachmentList from 'mastodon/components/attachment_list';
-import { Icon }  from 'mastodon/components/icon';
-import { VisibilityIcon } from 'mastodon/components/visibility_icon';
-import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
-import { WithRouterPropTypes } from 'mastodon/utils/react_router';
-
-import { Avatar } from '../../../components/avatar';
-import { Button } from '../../../components/button';
-import { DisplayName } from '../../../components/display_name';
-import { RelativeTimestamp } from '../../../components/relative_timestamp';
-import StatusContent from '../../../components/status_content';
-
-const messages = defineMessages({
-  cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
-  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
-});
-
-const mapStateToProps = state => {
-  return {
-    privacy: state.getIn(['boosts', 'new', 'privacy']),
-  };
-};
-
-const mapDispatchToProps = dispatch => {
-  return {
-    onChangeBoostPrivacy(value) {
-      dispatch(changeBoostPrivacy(value));
-    },
-  };
-};
-
-class BoostModal extends ImmutablePureComponent {
-  static propTypes = {
-    status: ImmutablePropTypes.map.isRequired,
-    onReblog: PropTypes.func.isRequired,
-    onClose: PropTypes.func.isRequired,
-    onChangeBoostPrivacy: PropTypes.func.isRequired,
-    privacy: PropTypes.string.isRequired,
-    intl: PropTypes.object.isRequired,
-    ...WithRouterPropTypes,
-  };
-
-  handleReblog = () => {
-    this.props.onReblog(this.props.status, this.props.privacy);
-    this.props.onClose();
-  };
-
-  handleAccountClick = (e) => {
-    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      this.props.onClose();
-      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
-    }
-  };
-
-  _findContainer = () => {
-    return document.getElementsByClassName('modal-root__container')[0];
-  };
-
-  render () {
-    const { status, privacy, intl } = this.props;
-    const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
-
-    return (
-      <div className='modal-root__modal boost-modal'>
-        <div className='boost-modal__container'>
-          <div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
-            <div className='status__info'>
-              <a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
-                <span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
-                <RelativeTimestamp timestamp={status.get('created_at')} />
-              </a>
-
-              <a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name'>
-                <div className='status__avatar'>
-                  <Avatar account={status.get('account')} size={48} />
-                </div>
-
-                <DisplayName account={status.get('account')} />
-              </a>
-            </div>
-
-            <StatusContent status={status} />
-
-            {status.get('media_attachments').size > 0 && (
-              <AttachmentList
-                compact
-                media={status.get('media_attachments')}
-              />
-            )}
-          </div>
-        </div>
-
-        <div className='boost-modal__action-bar'>
-          <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' icon={RepeatIcon} /></span> }} /></div>
-          {status.get('visibility') !== 'private' && !status.get('reblogged') && (
-            <PrivacyDropdown
-              noDirect
-              value={privacy}
-              container={this._findContainer}
-              onChange={this.props.onChangeBoostPrivacy}
-            />
-          )}
-          <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} autoFocus />
-        </div>
-      </div>
-    );
-  }
-
-}
-
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal)));
diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.tsx b/app/javascript/mastodon/features/ui/components/boost_modal.tsx
new file mode 100644
index 0000000000..40b0c81833
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/boost_modal.tsx
@@ -0,0 +1,162 @@
+import type { MouseEventHandler } from 'react';
+import { useCallback, useState } from 'react';
+
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import classNames from 'classnames';
+import { useHistory } from 'react-router';
+
+import type Immutable from 'immutable';
+
+import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
+import AttachmentList from 'mastodon/components/attachment_list';
+import { Icon } from 'mastodon/components/icon';
+import { VisibilityIcon } from 'mastodon/components/visibility_icon';
+import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
+import type { Account } from 'mastodon/models/account';
+import type { Status, StatusVisibility } from 'mastodon/models/status';
+import { useAppSelector } from 'mastodon/store';
+
+import { Avatar } from '../../../components/avatar';
+import { Button } from '../../../components/button';
+import { DisplayName } from '../../../components/display_name';
+import { RelativeTimestamp } from '../../../components/relative_timestamp';
+import StatusContent from '../../../components/status_content';
+
+const messages = defineMessages({
+  cancel_reblog: {
+    id: 'status.cancel_reblog_private',
+    defaultMessage: 'Unboost',
+  },
+  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
+});
+
+export const BoostModal: React.FC<{
+  status: Status;
+  onClose: () => void;
+  onReblog: (status: Status, privacy: StatusVisibility) => void;
+}> = ({ status, onReblog, onClose }) => {
+  const intl = useIntl();
+  const history = useHistory();
+
+  const default_privacy = useAppSelector(
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
+    (state) => state.compose.get('default_privacy') as StatusVisibility,
+  );
+
+  const account = status.get('account') as Account;
+  const statusVisibility = status.get('visibility') as StatusVisibility;
+
+  const [privacy, setPrivacy] = useState<StatusVisibility>(
+    statusVisibility === 'private' ? 'private' : default_privacy,
+  );
+
+  const onPrivacyChange = useCallback((value: StatusVisibility) => {
+    setPrivacy(value);
+  }, []);
+
+  const handleReblog = useCallback(() => {
+    onReblog(status, privacy);
+    onClose();
+  }, [onClose, onReblog, status, privacy]);
+
+  const handleAccountClick = useCallback<MouseEventHandler>(
+    (e) => {
+      if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+        e.preventDefault();
+        onClose();
+        history.push(`/@${account.acct}`);
+      }
+    },
+    [history, onClose, account],
+  );
+
+  const buttonText = status.get('reblogged')
+    ? messages.cancel_reblog
+    : messages.reblog;
+
+  const findContainer = useCallback(
+    () => document.getElementsByClassName('modal-root__container')[0],
+    [],
+  );
+
+  return (
+    <div className='modal-root__modal boost-modal'>
+      <div className='boost-modal__container'>
+        <div
+          className={classNames(
+            'status',
+            `status-${statusVisibility}`,
+            'light',
+          )}
+        >
+          <div className='status__info'>
+            <a
+              href={`/@${account.acct}/${status.get('id') as string}`}
+              className='status__relative-time'
+              target='_blank'
+              rel='noopener noreferrer'
+            >
+              <span className='status__visibility-icon'>
+                <VisibilityIcon visibility={statusVisibility} />
+              </span>
+              <RelativeTimestamp
+                timestamp={status.get('created_at') as string}
+              />
+            </a>
+
+            <a
+              onClick={handleAccountClick}
+              href={`/@${account.acct}`}
+              className='status__display-name'
+            >
+              <div className='status__avatar'>
+                <Avatar account={account} size={48} />
+              </div>
+
+              <DisplayName account={account} />
+            </a>
+          </div>
+
+          {/* @ts-expect-error Expected until StatusContent is typed */}
+          <StatusContent status={status} />
+
+          {(status.get('media_attachments') as Immutable.List<unknown>).size >
+            0 && (
+            <AttachmentList compact media={status.get('media_attachments')} />
+          )}
+        </div>
+      </div>
+
+      <div className='boost-modal__action-bar'>
+        <div>
+          <FormattedMessage
+            id='boost_modal.combo'
+            defaultMessage='You can press {combo} to skip this next time'
+            values={{
+              combo: (
+                <span>
+                  Shift + <Icon id='retweet' icon={RepeatIcon} />
+                </span>
+              ),
+            }}
+          />
+        </div>
+        {statusVisibility !== 'private' && !status.get('reblogged') && (
+          <PrivacyDropdown
+            noDirect
+            value={privacy}
+            container={findContainer}
+            onChange={onPrivacyChange}
+          />
+        )}
+        <Button
+          text={intl.formatMessage(buttonText)}
+          onClick={handleReblog}
+          // eslint-disable-next-line jsx-a11y/no-autofocus
+          autoFocus
+        />
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx
index 97d7706da4..404b53c742 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.jsx
+++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx
@@ -24,7 +24,7 @@ import BundleContainer from '../containers/bundle_container';
 
 import ActionsModal from './actions_modal';
 import AudioModal from './audio_modal';
-import BoostModal from './boost_modal';
+import { BoostModal } from './boost_modal';
 import BundleModalError from './bundle_modal_error';
 import ConfirmationModal from './confirmation_modal';
 import FocalPointModal from './focal_point_modal';
diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts
new file mode 100644
index 0000000000..83e9f6b885
--- /dev/null
+++ b/app/javascript/mastodon/models/status.ts
@@ -0,0 +1,4 @@
+export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct';
+
+// Temporary until we type it correctly
+export type Status = Immutable.Map<string, unknown>;
diff --git a/app/javascript/mastodon/reducers/boosts.js b/app/javascript/mastodon/reducers/boosts.js
deleted file mode 100644
index d0d825057c..0000000000
--- a/app/javascript/mastodon/reducers/boosts.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import Immutable from 'immutable';
-
-import {
-  BOOSTS_INIT_MODAL,
-  BOOSTS_CHANGE_PRIVACY,
-} from 'mastodon/actions/boosts';
-
-const initialState = Immutable.Map({
-  new: Immutable.Map({
-    privacy: 'public',
-  }),
-});
-
-export default function mutes(state = initialState, action) {
-  switch (action.type) {
-  case BOOSTS_INIT_MODAL:
-    return state.withMutations((state) => {
-      state.setIn(['new', 'privacy'], action.privacy);
-    });
-  case BOOSTS_CHANGE_PRIVACY:
-    return state.setIn(['new', 'privacy'], action.privacy);
-  default:
-    return state;
-  }
-}
diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts
index db5e68a70c..6296ef2026 100644
--- a/app/javascript/mastodon/reducers/index.ts
+++ b/app/javascript/mastodon/reducers/index.ts
@@ -7,7 +7,6 @@ import { accountsReducer } from './accounts';
 import accounts_map from './accounts_map';
 import alerts from './alerts';
 import announcements from './announcements';
-import boosts from './boosts';
 import compose from './compose';
 import contexts from './contexts';
 import conversations from './conversations';
@@ -60,7 +59,6 @@ const reducers = {
   relationships: relationshipsReducer,
   settings,
   push_notifications,
-  boosts,
   server,
   contexts,
   compose,