diff --git a/app/soapbox/features/remote_timeline/index.js b/app/soapbox/features/remote_timeline/index.js
deleted file mode 100644
index a207498c6..000000000
--- a/app/soapbox/features/remote_timeline/index.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
-
-import { getSettings } from 'soapbox/actions/settings';
-import IconButton from 'soapbox/components/icon_button';
-import Column from 'soapbox/features/ui/components/column';
-
-import { connectRemoteStream } from '../../actions/streaming';
-import { expandRemoteTimeline } from '../../actions/timelines';
-import StatusListContainer from '../ui/containers/status_list_container';
-
-import PinnedHostsPicker from './components/pinned_hosts_picker';
-
-const messages = defineMessages({
- title: { id: 'column.remote', defaultMessage: 'Federated timeline' },
-});
-
-const mapStateToProps = (state, props) => {
- const instance = props.params.instance;
- const settings = getSettings(state);
- const onlyMedia = settings.getIn(['remote', 'other', 'onlyMedia']);
-
- const timelineId = 'remote';
-
- return {
- timelineId,
- onlyMedia,
- hasUnread: state.getIn(['timelines', `${timelineId}${onlyMedia ? ':media' : ''}:${instance}`, 'unread']) > 0,
- instance,
- pinned: settings.getIn(['remote_timeline', 'pinnedHosts']).includes(instance),
- };
-};
-
-export default @connect(mapStateToProps)
-@injectIntl
-@withRouter
-class RemoteTimeline extends React.PureComponent {
-
- static propTypes = {
- dispatch: PropTypes.func.isRequired,
- intl: PropTypes.object.isRequired,
- hasUnread: PropTypes.bool,
- onlyMedia: PropTypes.bool,
- timelineId: PropTypes.string,
- instance: PropTypes.string.isRequired,
- pinned: PropTypes.bool,
- history: PropTypes.object,
- };
-
- componentDidMount() {
- const { dispatch, onlyMedia, instance } = this.props;
- dispatch(expandRemoteTimeline(instance, { onlyMedia }));
- this.disconnect = dispatch(connectRemoteStream(instance, { onlyMedia }));
- }
-
- componentDidUpdate(prevProps) {
- if (prevProps.onlyMedia !== this.props.onlyMedia) {
- const { dispatch, onlyMedia, instance } = this.props;
- this.disconnect();
-
- dispatch(expandRemoteTimeline(instance, { onlyMedia }));
- this.disconnect = dispatch(connectRemoteStream(instance, { onlyMedia }));
- }
- }
-
- componentWillUnmount() {
- if (this.disconnect) {
- this.disconnect();
- this.disconnect = null;
- }
- }
-
- handleCloseClick = e => {
- this.props.history.push('/timeline/fediverse');
- }
-
- handleLoadMore = maxId => {
- const { dispatch, onlyMedia, instance } = this.props;
- dispatch(expandRemoteTimeline(instance, { maxId, onlyMedia }));
- }
-
- render() {
- const { intl, onlyMedia, timelineId, instance, pinned } = this.props;
-
- return (
-
-
- {!pinned &&
-
-
-
}
-
- }
- divideType='space'
- />
-
- );
- }
-
-}
diff --git a/app/soapbox/features/remote_timeline/index.tsx b/app/soapbox/features/remote_timeline/index.tsx
new file mode 100644
index 000000000..d3560f4a6
--- /dev/null
+++ b/app/soapbox/features/remote_timeline/index.tsx
@@ -0,0 +1,94 @@
+import React, { useEffect, useRef } from 'react';
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+import { useHistory } from 'react-router-dom';
+
+import IconButton from 'soapbox/components/icon_button';
+import Column from 'soapbox/features/ui/components/column';
+import { useAppDispatch, useSettings } from 'soapbox/hooks';
+
+import { connectRemoteStream } from '../../actions/streaming';
+import { expandRemoteTimeline } from '../../actions/timelines';
+import StatusListContainer from '../ui/containers/status_list_container';
+
+import PinnedHostsPicker from './components/pinned_hosts_picker';
+
+const messages = defineMessages({
+ title: { id: 'column.remote', defaultMessage: 'Federated timeline' },
+});
+
+interface IRemoteTimeline {
+ params?: {
+ instance?: string,
+ }
+}
+
+/** View statuses from a remote instance. */
+const RemoteTimeline: React.FC = ({ params }) => {
+ const intl = useIntl();
+ const history = useHistory();
+ const dispatch = useAppDispatch();
+
+ const instance = params?.instance;
+ const settings = useSettings();
+
+ const stream = useRef(null);
+
+ const timelineId = 'remote';
+ const onlyMedia = !!settings.getIn(['remote', 'other', 'onlyMedia']);
+
+ const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(instance);
+
+ const disconnect = () => {
+ if (stream.current) {
+ stream.current();
+ }
+ };
+
+ useEffect(() => {
+ disconnect();
+ dispatch(expandRemoteTimeline(instance, { onlyMedia, maxId: undefined }));
+ stream.current = dispatch(connectRemoteStream(instance, { onlyMedia }));
+
+ return () => {
+ disconnect();
+ stream.current = null;
+ };
+ }, [onlyMedia]);
+
+ const handleCloseClick: React.MouseEventHandler = () => {
+ history.push('/timeline/fediverse');
+ };
+
+ const handleLoadMore = (maxId: string) => {
+ dispatch(expandRemoteTimeline(instance, { maxId, onlyMedia }));
+ };
+
+ return (
+
+ {instance && }
+ {!pinned &&
+
+
+
}
+
+ }
+ divideType='space'
+ />
+
+ );
+};
+
+export default RemoteTimeline;