diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js
index 3b66a0472..1baa252b0 100644
--- a/app/soapbox/actions/streaming.js
+++ b/app/soapbox/actions/streaming.js
@@ -82,7 +82,8 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
+export const connectRemoteStream = (instance, { onlyMedia } = {}) => connectTimelineStream(`remote${onlyMedia ? ':media' : ''}:${instance}`, `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`);
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
-export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
+export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
diff --git a/app/soapbox/actions/timelines.js b/app/soapbox/actions/timelines.js
index 67f6b19f8..7ce6320fd 100644
--- a/app/soapbox/actions/timelines.js
+++ b/app/soapbox/actions/timelines.js
@@ -166,6 +166,8 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandRemoteTimeline = (instance, { maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done);
+
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
diff --git a/app/soapbox/features/remote_timeline/components/column_settings.js b/app/soapbox/features/remote_timeline/components/column_settings.js
new file mode 100644
index 000000000..1d7bd7359
--- /dev/null
+++ b/app/soapbox/features/remote_timeline/components/column_settings.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+export default @injectIntl
+class ColumnSettings extends React.PureComponent {
+
+ static propTypes = {
+ settings: ImmutablePropTypes.map.isRequired,
+ onChange: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ };
+
+ render() {
+ const { settings, onChange } = this.props;
+
+ return (
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/remote_timeline/containers/column_settings_container.js b/app/soapbox/features/remote_timeline/containers/column_settings_container.js
new file mode 100644
index 000000000..ac001bcab
--- /dev/null
+++ b/app/soapbox/features/remote_timeline/containers/column_settings_container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import ColumnSettings from '../components/column_settings';
+import { getSettings, changeSetting } from '../../../actions/settings';
+
+const mapStateToProps = state => ({
+ settings: getSettings(state).get('public'),
+});
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onChange(key, checked) {
+ dispatch(changeSetting(['public', ...key], checked));
+ },
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/soapbox/features/remote_timeline/index.js b/app/soapbox/features/remote_timeline/index.js
new file mode 100644
index 000000000..b5d6cdb8f
--- /dev/null
+++ b/app/soapbox/features/remote_timeline/index.js
@@ -0,0 +1,101 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
+import StatusListContainer from '../ui/containers/status_list_container';
+import Column from '../../components/column';
+import ColumnSettingsContainer from './containers/column_settings_container';
+import HomeColumnHeader from '../../components/home_column_header';
+import { expandRemoteTimeline } from '../../actions/timelines';
+import { connectRemoteStream } from '../../actions/streaming';
+import { getSettings } from 'soapbox/actions/settings';
+
+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,
+ };
+};
+
+export default @connect(mapStateToProps)
+@injectIntl
+class RemoteTimeline extends React.PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ hasUnread: PropTypes.bool,
+ onlyMedia: PropTypes.bool,
+ timelineId: PropTypes.string,
+ instance: PropTypes.string.isRequired,
+ };
+
+ 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;
+ }
+ }
+
+ handleLoadMore = maxId => {
+ const { dispatch, onlyMedia, instance } = this.props;
+ dispatch(expandRemoteTimeline(instance, { maxId, onlyMedia }));
+ }
+
+ render() {
+ const { intl, hasUnread, onlyMedia, timelineId, instance } = this.props;
+
+ return (
+
+
+
+
+
+ }
+ />
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js
index 9f0085772..8b714fe7e 100644
--- a/app/soapbox/features/ui/index.js
+++ b/app/soapbox/features/ui/index.js
@@ -45,6 +45,7 @@ import {
// GettingStarted,
CommunityTimeline,
PublicTimeline,
+ RemoteTimeline,
AccountTimeline,
AccountGallery,
HomeTimeline,
@@ -208,6 +209,7 @@ class SwitchingColumnsArea extends React.PureComponent {
+
{/*
diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js
index 01838b156..3cb9e5142 100644
--- a/app/soapbox/features/ui/util/async-components.js
+++ b/app/soapbox/features/ui/util/async-components.js
@@ -18,6 +18,10 @@ export function PublicTimeline() {
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
}
+export function RemoteTimeline() {
+ return import(/* webpackChunkName: "features/remote_timeline" */'../../remote_timeline');
+}
+
export function CommunityTimeline() {
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
}