diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx
deleted file mode 100644
index 122baafd6c..0000000000
--- a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import { Helmet } from 'react-helmet';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { debounce } from 'lodash';
-
-import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
-import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'mastodon/actions/bookmarks';
-import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
-import ColumnHeader from 'mastodon/components/column_header';
-import StatusList from 'mastodon/components/status_list';
-import Column from 'mastodon/features/ui/components/column';
-import { getStatusList } from 'mastodon/selectors';
-
-const messages = defineMessages({
- heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
-});
-
-const mapStateToProps = state => ({
- statusIds: getStatusList(state, 'bookmarks'),
- isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
- hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
-});
-
-class Bookmarks extends ImmutablePureComponent {
-
- static propTypes = {
- dispatch: PropTypes.func.isRequired,
- statusIds: ImmutablePropTypes.list.isRequired,
- intl: PropTypes.object.isRequired,
- columnId: PropTypes.string,
- multiColumn: PropTypes.bool,
- hasMore: PropTypes.bool,
- isLoading: PropTypes.bool,
- };
-
- UNSAFE_componentWillMount () {
- this.props.dispatch(fetchBookmarkedStatuses());
- }
-
- handlePin = () => {
- const { columnId, dispatch } = this.props;
-
- if (columnId) {
- dispatch(removeColumn(columnId));
- } else {
- dispatch(addColumn('BOOKMARKS', {}));
- }
- };
-
- handleMove = (dir) => {
- const { columnId, dispatch } = this.props;
- dispatch(moveColumn(columnId, dir));
- };
-
- handleHeaderClick = () => {
- this.column.scrollTop();
- };
-
- setRef = c => {
- this.column = c;
- };
-
- handleLoadMore = debounce(() => {
- this.props.dispatch(expandBookmarkedStatuses());
- }, 300, { leading: true });
-
- render () {
- const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
- const pinned = !!columnId;
-
- const emptyMessage = ;
-
- return (
-
-
-
-
-
-
- {intl.formatMessage(messages.heading)}
-
-
-
- );
- }
-
-}
-
-export default connect(mapStateToProps)(injectIntl(Bookmarks));
diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.tsx b/app/javascript/mastodon/features/bookmarked_statuses/index.tsx
new file mode 100644
index 0000000000..5d4574b05b
--- /dev/null
+++ b/app/javascript/mastodon/features/bookmarked_statuses/index.tsx
@@ -0,0 +1,116 @@
+import { useEffect, useRef, useCallback } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
+import {
+ fetchBookmarkedStatuses,
+ expandBookmarkedStatuses,
+} from 'mastodon/actions/bookmarks';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusList from 'mastodon/components/status_list';
+import { getStatusList } from 'mastodon/selectors';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const messages = defineMessages({
+ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
+});
+
+const Bookmarks: React.FC<{
+ columnId: string;
+ multiColumn: boolean;
+}> = ({ columnId, multiColumn }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const columnRef = useRef(null);
+ const statusIds = useAppSelector((state) =>
+ getStatusList(state, 'bookmarks'),
+ );
+ const isLoading = useAppSelector(
+ (state) =>
+ state.status_lists.getIn(['bookmarks', 'isLoading'], true) as boolean,
+ );
+ const hasMore = useAppSelector(
+ (state) => !!state.status_lists.getIn(['bookmarks', 'next']),
+ );
+
+ useEffect(() => {
+ dispatch(fetchBookmarkedStatuses());
+ }, [dispatch]);
+
+ const handlePin = useCallback(() => {
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ dispatch(addColumn('BOOKMARKS', {}));
+ }
+ }, [dispatch, columnId]);
+
+ const handleMove = useCallback(
+ (dir: number) => {
+ dispatch(moveColumn(columnId, dir));
+ },
+ [dispatch, columnId],
+ );
+
+ const handleHeaderClick = useCallback(() => {
+ columnRef.current?.scrollTop();
+ }, []);
+
+ const handleLoadMore = useCallback(() => {
+ dispatch(expandBookmarkedStatuses());
+ }, [dispatch]);
+
+ const pinned = !!columnId;
+
+ const emptyMessage = (
+
+ );
+
+ return (
+
+
+
+
+
+
+ {intl.formatMessage(messages.heading)}
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default Bookmarks;
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.jsx b/app/javascript/mastodon/features/favourited_statuses/index.jsx
deleted file mode 100644
index 9e0b982239..0000000000
--- a/app/javascript/mastodon/features/favourited_statuses/index.jsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import { Helmet } from 'react-helmet';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { debounce } from 'lodash';
-
-import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
-import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
-import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'mastodon/actions/favourites';
-import ColumnHeader from 'mastodon/components/column_header';
-import StatusList from 'mastodon/components/status_list';
-import Column from 'mastodon/features/ui/components/column';
-import { getStatusList } from 'mastodon/selectors';
-
-const messages = defineMessages({
- heading: { id: 'column.favourites', defaultMessage: 'Favorites' },
-});
-
-const mapStateToProps = state => ({
- statusIds: getStatusList(state, 'favourites'),
- isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
- hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
-});
-
-class Favourites extends ImmutablePureComponent {
-
- static propTypes = {
- dispatch: PropTypes.func.isRequired,
- statusIds: ImmutablePropTypes.list.isRequired,
- intl: PropTypes.object.isRequired,
- columnId: PropTypes.string,
- multiColumn: PropTypes.bool,
- hasMore: PropTypes.bool,
- isLoading: PropTypes.bool,
- };
-
- UNSAFE_componentWillMount () {
- this.props.dispatch(fetchFavouritedStatuses());
- }
-
- handlePin = () => {
- const { columnId, dispatch } = this.props;
-
- if (columnId) {
- dispatch(removeColumn(columnId));
- } else {
- dispatch(addColumn('FAVOURITES', {}));
- }
- };
-
- handleMove = (dir) => {
- const { columnId, dispatch } = this.props;
- dispatch(moveColumn(columnId, dir));
- };
-
- handleHeaderClick = () => {
- this.column.scrollTop();
- };
-
- setRef = c => {
- this.column = c;
- };
-
- handleLoadMore = debounce(() => {
- this.props.dispatch(expandFavouritedStatuses());
- }, 300, { leading: true });
-
- render () {
- const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
- const pinned = !!columnId;
-
- const emptyMessage = ;
-
- return (
-
-
-
-
-
-
- {intl.formatMessage(messages.heading)}
-
-
-
- );
- }
-
-}
-
-export default connect(mapStateToProps)(injectIntl(Favourites));
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.tsx b/app/javascript/mastodon/features/favourited_statuses/index.tsx
new file mode 100644
index 0000000000..908a8ae4a1
--- /dev/null
+++ b/app/javascript/mastodon/features/favourited_statuses/index.tsx
@@ -0,0 +1,116 @@
+import { useEffect, useRef, useCallback } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import {
+ fetchFavouritedStatuses,
+ expandFavouritedStatuses,
+} from 'mastodon/actions/favourites';
+import { Column } from 'mastodon/components/column';
+import type { ColumnRef } from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusList from 'mastodon/components/status_list';
+import { getStatusList } from 'mastodon/selectors';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const messages = defineMessages({
+ heading: { id: 'column.favourites', defaultMessage: 'Favorites' },
+});
+
+const Favourites: React.FC<{ columnId: string; multiColumn: boolean }> = ({
+ columnId,
+ multiColumn,
+}) => {
+ const dispatch = useAppDispatch();
+ const intl = useIntl();
+ const columnRef = useRef(null);
+ const statusIds = useAppSelector((state) =>
+ getStatusList(state, 'favourites'),
+ );
+ const isLoading = useAppSelector(
+ (state) =>
+ state.status_lists.getIn(['favourites', 'isLoading'], true) as boolean,
+ );
+ const hasMore = useAppSelector(
+ (state) => !!state.status_lists.getIn(['favourites', 'next']),
+ );
+
+ useEffect(() => {
+ dispatch(fetchFavouritedStatuses());
+ }, [dispatch]);
+
+ const handlePin = useCallback(() => {
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ dispatch(addColumn('FAVOURITES', {}));
+ }
+ }, [dispatch, columnId]);
+
+ const handleMove = useCallback(
+ (dir: number) => {
+ dispatch(moveColumn(columnId, dir));
+ },
+ [dispatch, columnId],
+ );
+
+ const handleHeaderClick = useCallback(() => {
+ columnRef.current?.scrollTop();
+ }, []);
+
+ const handleLoadMore = useCallback(() => {
+ dispatch(expandFavouritedStatuses());
+ }, [dispatch]);
+
+ const pinned = !!columnId;
+
+ const emptyMessage = (
+
+ );
+
+ return (
+
+
+
+
+
+
+ {intl.formatMessage(messages.heading)}
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default Favourites;
diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js
index 6cb6a937bb..c9d39130ee 100644
--- a/app/javascript/mastodon/reducers/status_lists.js
+++ b/app/javascript/mastodon/reducers/status_lists.js
@@ -96,6 +96,7 @@ const removeOneFromList = (state, listType, status) => {
return state.updateIn([listType, 'items'], (list) => list.delete(status.get('id')));
};
+/** @type {import('@reduxjs/toolkit').Reducer} */
export default function statusLists(state = initialState, action) {
switch(action.type) {
case FAVOURITED_STATUSES_FETCH_REQUEST:
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 5ccaba23fd..3119b285b2 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -6,6 +6,7 @@ import { me } from '../initial_state';
import { getFilters } from './filters';
export { makeGetAccount } from "./accounts";
+export { getStatusList } from "./statuses";
export const makeGetStatus = () => {
return createSelector(
@@ -77,7 +78,3 @@ export const makeGetReport = () => createSelector([
(_, base) => base,
(state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]),
], (base, targetAccount) => base.set('target_account', targetAccount));
-
-export const getStatusList = createSelector([
- (state, type) => state.getIn(['status_lists', type, 'items']),
-], (items) => items.toList());
diff --git a/app/javascript/mastodon/selectors/statuses.ts b/app/javascript/mastodon/selectors/statuses.ts
new file mode 100644
index 0000000000..4d045e924a
--- /dev/null
+++ b/app/javascript/mastodon/selectors/statuses.ts
@@ -0,0 +1,15 @@
+import { createSelector } from '@reduxjs/toolkit';
+import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
+
+import type { RootState } from 'mastodon/store';
+
+export const getStatusList = createSelector(
+ [
+ (
+ state: RootState,
+ type: 'favourites' | 'bookmarks' | 'pins' | 'trending',
+ ) =>
+ state.status_lists.getIn([type, 'items']) as ImmutableOrderedSet,
+ ],
+ (items) => items.toList(),
+);