diff --git a/app/soapbox/features/account_timeline/index.js b/app/soapbox/features/account_timeline/index.js
deleted file mode 100644
index 907c54d68..000000000
--- a/app/soapbox/features/account_timeline/index.js
+++ /dev/null
@@ -1,174 +0,0 @@
-import { OrderedSet as ImmutableOrderedSet } from 'immutable';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
-
-import { fetchAccountByUsername } from 'soapbox/actions/accounts';
-import { fetchPatronAccount } from 'soapbox/actions/patron';
-import { getSettings } from 'soapbox/actions/settings';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'soapbox/actions/timelines';
-import MissingIndicator from 'soapbox/components/missing_indicator';
-import StatusList from 'soapbox/components/status_list';
-import { Card, CardBody, Spinner, Text } from 'soapbox/components/ui';
-import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
-import { getFeatures } from 'soapbox/utils/features';
-
-const makeMapStateToProps = () => {
- const getStatusIds = makeGetStatusIds();
-
- const mapStateToProps = (state, { params, withReplies = false }) => {
- const username = params.username || '';
- const me = state.get('me');
- const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
- const soapboxConfig = getSoapboxConfig(state);
- const features = getFeatures(state.get('instance'));
-
- let accountId = -1;
- let account = null;
- let accountUsername = username;
- let accountApId = null;
- if (accountFetchError) {
- accountId = null;
- } else {
- account = findAccountByUsername(state, username);
- accountId = account ? account.getIn(['id'], null) : -1;
- accountUsername = account ? account.getIn(['acct'], '') : '';
- accountApId = account ? account.get('url') : '';
- }
-
- const path = withReplies ? `${accountId}:with_replies` : accountId;
-
- const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
- const showPins = getSettings(state).getIn(['account_timeline', 'shows', 'pinned']) && !withReplies;
-
- return {
- accountId,
- unavailable,
- accountUsername,
- accountApId,
- isBlocked,
- account,
- isAccount: !!state.getIn(['accounts', accountId]),
- statusIds: getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }),
- featuredStatusIds: showPins ? getStatusIds(state, { type: `account:${accountId}:pinned`, prefix: 'account_timeline' }) : ImmutableOrderedSet(),
- isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
- hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
- me,
- patronEnabled: soapboxConfig.getIn(['extensions', 'patron', 'enabled']),
- };
- };
-
- return mapStateToProps;
-};
-
-export default @connect(makeMapStateToProps)
-@withRouter
-class AccountTimeline extends ImmutablePureComponent {
-
- static propTypes = {
- params: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
- statusIds: ImmutablePropTypes.orderedSet,
- featuredStatusIds: ImmutablePropTypes.orderedSet,
- isLoading: PropTypes.bool,
- hasMore: PropTypes.bool,
- withReplies: PropTypes.bool,
- isAccount: PropTypes.bool,
- unavailable: PropTypes.bool,
- };
-
- componentDidMount() {
- const { params: { username }, accountId, accountApId, withReplies, patronEnabled, history } = this.props;
-
- this.props.dispatch(fetchAccountByUsername(username, history));
-
- if (accountId && accountId !== -1) {
- if (!withReplies) {
- this.props.dispatch(expandAccountFeaturedTimeline(accountId));
- }
-
- if (patronEnabled && accountApId) {
- this.props.dispatch(fetchPatronAccount(accountApId));
- }
-
- this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
- }
- }
-
- componentDidUpdate(prevProps) {
- const { params: { username }, accountId, withReplies, accountApId, patronEnabled, history } = this.props;
-
- if (username && (username !== prevProps.params.username)) {
- this.props.dispatch(fetchAccountByUsername(username, history));
- }
-
- if (accountId && (accountId !== -1) && (accountId !== prevProps.accountId) || withReplies !== prevProps.withReplies) {
- if (!withReplies) {
- this.props.dispatch(expandAccountFeaturedTimeline(accountId));
- }
-
- if (patronEnabled && accountApId) {
- this.props.dispatch(fetchPatronAccount(accountApId));
- }
-
- this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
- }
- }
-
- handleLoadMore = maxId => {
- if (this.props.accountId && this.props.accountId !== -1) {
- this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
- }
- }
-
- render() {
- const { statusIds, featuredStatusIds, isLoading, hasMore, isBlocked, isAccount, accountId, unavailable, accountUsername } = this.props;
-
- if (!isAccount && accountId !== -1) {
- return (
-
- );
- }
-
- if (accountId === -1 || (!statusIds && isLoading)) {
- return (
-
- );
- }
-
- if (unavailable) {
- return (
-
-
-
- {isBlocked ? (
-
- ) : (
-
- )}
-
-
-
- );
- }
-
- return (
- }
- />
- );
- }
-
-}
diff --git a/app/soapbox/features/account_timeline/index.tsx b/app/soapbox/features/account_timeline/index.tsx
new file mode 100644
index 000000000..fc9110565
--- /dev/null
+++ b/app/soapbox/features/account_timeline/index.tsx
@@ -0,0 +1,108 @@
+import React, { useEffect } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { useHistory } from 'react-router-dom';
+
+import { fetchAccountByUsername } from 'soapbox/actions/accounts';
+import { fetchPatronAccount } from 'soapbox/actions/patron';
+import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'soapbox/actions/timelines';
+import MissingIndicator from 'soapbox/components/missing_indicator';
+import StatusList from 'soapbox/components/status_list';
+import { Card, CardBody, Spinner, Text } from 'soapbox/components/ui';
+import { useAppDispatch, useAppSelector, useFeatures, useSettings, useSoapboxConfig } from 'soapbox/hooks';
+import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
+
+const getStatusIds = makeGetStatusIds();
+
+interface IAccountTimeline {
+ params: {
+ username: string,
+ },
+ withReplies?: boolean,
+}
+
+const AccountTimeline: React.FC = ({ params, withReplies = false }) => {
+ const history = useHistory();
+ const dispatch = useAppDispatch();
+ const features = useFeatures();
+ const settings = useSettings();
+ const soapboxConfig = useSoapboxConfig();
+
+ const account = useAppSelector(state => findAccountByUsername(state, params.username));
+
+ const path = withReplies ? `${account?.id}:with_replies` : account?.id;
+ const showPins = settings.getIn(['account_timeline', 'shows', 'pinned']) === true && !withReplies;
+ const statusIds = useAppSelector(state => getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }));
+ const featuredStatusIds = useAppSelector(state => getStatusIds(state, { type: `account:${account?.id}:pinned`, prefix: 'account_timeline' }));
+
+ const isBlocked = useAppSelector(state => state.relationships.getIn([account?.id, 'blocked_by']) === true);
+ const unavailable = isBlocked && !features.blockersVisible;
+ const patronEnabled = soapboxConfig.getIn(['extensions', 'patron', 'enabled']) === true;
+ const isLoading = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'isLoading']) === true);
+ const hasMore = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'hasMore']) === true);
+
+ const accountUsername = account?.username || params.username;
+
+ useEffect(() => {
+ dispatch(fetchAccountByUsername(params.username, history));
+ }, [params.username]);
+
+ useEffect(() => {
+ if (account && !withReplies) {
+ dispatch(expandAccountFeaturedTimeline(account.id));
+ }
+ }, [account?.id, withReplies]);
+
+ useEffect(() => {
+ if (account && patronEnabled) {
+ dispatch(fetchPatronAccount(account.url));
+ }
+ }, [account?.url, patronEnabled]);
+
+ useEffect(() => {
+ if (account) {
+ dispatch(expandAccountTimeline(account.id, { withReplies }));
+ }
+ }, [account?.id]);
+
+ const handleLoadMore = (maxId: string) => {
+ if (account) {
+ dispatch(expandAccountTimeline(account.id, { maxId, withReplies }));
+ }
+ };
+
+ if (!account && isLoading) {
+ return ;
+ } else if (!account) {
+ return ;
+ }
+
+ if (unavailable) {
+ return (
+
+
+
+ {isBlocked ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+ }
+
+ return (
+ }
+ />
+ );
+};
+
+export default AccountTimeline;