diff --git a/app/soapbox/features/favourited_statuses/index.js b/app/soapbox/features/favourited_statuses/index.js
deleted file mode 100644
index b4ffb6a8a..000000000
--- a/app/soapbox/features/favourited_statuses/index.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import debounce from 'lodash/debounce';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-
-import { fetchAccount, fetchAccountByUsername } from 'soapbox/actions/accounts';
-import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from 'soapbox/actions/favourites';
-import MissingIndicator from 'soapbox/components/missing_indicator';
-import StatusList from 'soapbox/components/status_list';
-import { Spinner } from 'soapbox/components/ui';
-import { findAccountByUsername } from 'soapbox/selectors';
-import { getFeatures } from 'soapbox/utils/features';
-
-import Column from '../ui/components/column';
-
-const messages = defineMessages({
- heading: { id: 'column.favourited_statuses', defaultMessage: 'Liked posts' },
-});
-
-const mapStateToProps = (state, { params }) => {
- const username = params.username || '';
- const me = state.get('me');
- const meUsername = state.getIn(['accounts', me, 'username'], '');
-
- const isMyAccount = (username.toLowerCase() === meUsername.toLowerCase());
-
- const features = getFeatures(state.get('instance'));
-
- if (isMyAccount) {
- return {
- isMyAccount,
- statusIds: state.status_lists.get('favourites').items,
- isLoading: state.status_lists.get('favourites').isLoading,
- hasMore: !!state.status_lists.get('favourites').next,
- };
- }
-
- const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
-
- let accountId = -1;
- if (accountFetchError) {
- accountId = null;
- } else {
- const account = findAccountByUsername(state, username);
- accountId = account ? account.getIn(['id'], null) : -1;
- }
-
- const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
-
- return {
- isMyAccount,
- accountId,
- unavailable,
- username,
- isAccount: !!state.getIn(['accounts', accountId]),
- statusIds: state.status_lists.get(`favourites:${accountId}`)?.items || [],
- isLoading: state.status_lists.get(`favourites:${accountId}`)?.isLoading,
- hasMore: !!state.status_lists.get(`favourites:${accountId}`)?.next,
- };
-};
-
-export default @connect(mapStateToProps)
-@injectIntl
-class Favourites extends ImmutablePureComponent {
-
- static propTypes = {
- dispatch: PropTypes.func.isRequired,
- statusIds: ImmutablePropTypes.orderedSet.isRequired,
- intl: PropTypes.object.isRequired,
- hasMore: PropTypes.bool,
- isLoading: PropTypes.bool,
- isMyAccount: PropTypes.bool.isRequired,
- };
-
- componentDidMount() {
- const { accountId, isMyAccount, username } = this.props;
-
- if (isMyAccount)
- this.props.dispatch(fetchFavouritedStatuses());
- else {
- if (accountId && accountId !== -1) {
- this.props.dispatch(fetchAccount(accountId));
- this.props.dispatch(fetchAccountFavouritedStatuses(accountId));
- } else {
- this.props.dispatch(fetchAccountByUsername(username));
- }
- }
- }
-
- componentDidUpdate(prevProps) {
- const { accountId, isMyAccount } = this.props;
-
- if (!isMyAccount && accountId && accountId !== -1 && (accountId !== prevProps.accountId && accountId)) {
- this.props.dispatch(fetchAccount(accountId));
- this.props.dispatch(fetchAccountFavouritedStatuses(accountId));
- }
- }
-
- handleLoadMore = debounce(() => {
- const { accountId, isMyAccount } = this.props;
-
- if (isMyAccount) {
- this.props.dispatch(expandFavouritedStatuses());
- } else {
- this.props.dispatch(expandAccountFavouritedStatuses(accountId));
- }
- }, 300, { leading: true })
-
- render() {
- const { intl, statusIds, isLoading, hasMore, isMyAccount, isAccount, accountId, unavailable } = this.props;
-
- if (!isMyAccount && !isAccount && accountId !== -1) {
- return (
-
- );
- }
-
- if (accountId === -1) {
- return (
-
-
-
- );
- }
-
- if (unavailable) {
- return (
-
-
-
-
-
- );
- }
-
- const emptyMessage = isMyAccount
- ?
- : ;
-
- return (
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/favourited_statuses/index.tsx b/app/soapbox/features/favourited_statuses/index.tsx
new file mode 100644
index 000000000..1a8ba3bfa
--- /dev/null
+++ b/app/soapbox/features/favourited_statuses/index.tsx
@@ -0,0 +1,108 @@
+import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import debounce from 'lodash/debounce';
+import React, { useCallback, useEffect } from 'react';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+
+import { fetchAccount, fetchAccountByUsername } from 'soapbox/actions/accounts';
+import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from 'soapbox/actions/favourites';
+import MissingIndicator from 'soapbox/components/missing_indicator';
+import StatusList from 'soapbox/components/status_list';
+import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
+import { findAccountByUsername } from 'soapbox/selectors';
+
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+ heading: { id: 'column.favourited_statuses', defaultMessage: 'Liked posts' },
+});
+
+interface IFavourites {
+ params?: {
+ username?: string,
+ }
+}
+
+/** Timeline displaying a user's favourited statuses. */
+const Favourites: React.FC = (props) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const features = useFeatures();
+ const ownAccount = useOwnAccount();
+
+ const username = props.params?.username || '';
+ const account = useAppSelector(state => findAccountByUsername(state, username));
+ const isOwnAccount = username.toLowerCase() === ownAccount?.username?.toLowerCase();
+
+ const timelineKey = isOwnAccount ? 'favourites' : `favourites:${account?.id}`;
+ const statusIds = useAppSelector(state => state.status_lists.get(timelineKey)?.items || ImmutableOrderedSet());
+ const isLoading = useAppSelector(state => state.status_lists.get(timelineKey)?.isLoading === true);
+ const hasMore = useAppSelector(state => !!state.status_lists.get(timelineKey)?.next);
+
+ const isUnavailable = useAppSelector(state => {
+ const blockedBy = state.relationships.getIn([account?.id, 'blocked_by']) === true;
+ return isOwnAccount ? false : (blockedBy && !features.blockersVisible);
+ });
+
+ const handleLoadMore = useCallback(debounce(() => {
+ if (isOwnAccount) {
+ dispatch(expandFavouritedStatuses());
+ } else if (account) {
+ dispatch(expandAccountFavouritedStatuses(account.id));
+ }
+ }, 300, { leading: true }), [account?.id]);
+
+ useEffect(() => {
+ if (isOwnAccount)
+ dispatch(fetchFavouritedStatuses());
+ else {
+ if (account) {
+ dispatch(fetchAccount(account.id));
+ dispatch(fetchAccountFavouritedStatuses(account.id));
+ } else {
+ dispatch(fetchAccountByUsername(username));
+ }
+ }
+ }, []);
+
+ useEffect(() => {
+ if (account && !isOwnAccount) {
+ dispatch(fetchAccount(account.id));
+ dispatch(fetchAccountFavouritedStatuses(account.id));
+ }
+ }, [account?.id]);
+
+ if (isUnavailable) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (!account) {
+ return (
+
+ );
+ }
+
+ const emptyMessage = isOwnAccount
+ ?
+ : ;
+
+ return (
+
+
+
+ );
+};
+
+export default Favourites;
\ No newline at end of file