Make the streaming API also handle websockets (because trying to get the browser EventSource interface to

work flawlessly was a nightmare). WARNING: This commit makes the web UI connect to the streaming API instead
of ActionCable like before. This means that if you are upgrading, you should set that up beforehand.
This commit is contained in:
Eugen Rochko 2017-02-04 00:34:31 +01:00
parent 8c0bc1309f
commit ccb8ac8573
14 changed files with 310 additions and 132 deletions

View file

@ -13,4 +13,3 @@
//= require jquery
//= require jquery_ujs
//= require components
//= require cable

View file

@ -1,12 +0,0 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the rails generate channel command.
//
//= require action_cable
//= require_self
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

View file

@ -43,6 +43,7 @@ import hu from 'react-intl/locale-data/hu';
import uk from 'react-intl/locale-data/uk';
import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store';
import createStream from '../stream';
const store = configureStore();
@ -60,28 +61,27 @@ const Mastodon = React.createClass({
locale: React.PropTypes.string.isRequired
},
componentWillMount() {
const { locale } = this.props;
componentDidMount() {
const { locale } = this.props;
const accessToken = store.getState().getIn(['meta', 'access_token']);
if (typeof App !== 'undefined') {
this.subscription = App.cable.subscriptions.create('TimelineChannel', {
this.subscription = createStream(accessToken, 'user', {
received (data) {
switch(data.event) {
case 'update':
store.dispatch(updateTimeline('home', JSON.parse(data.payload)));
break;
case 'delete':
store.dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale));
break;
}
received (data) {
switch(data.event) {
case 'update':
store.dispatch(updateTimeline('home', JSON.parse(data.payload)));
break;
case 'delete':
store.dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale));
break;
}
}
});
}
});
// Desktop notifications
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
@ -91,7 +91,8 @@ const Mastodon = React.createClass({
componentWillUnmount () {
if (typeof this.subscription !== 'undefined') {
this.subscription.unsubscribe();
this.subscription.close();
this.subscription = null;
}
},

View file

@ -8,45 +8,49 @@ import {
deleteFromTimelines
} from '../../actions/timelines';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import createStream from '../../stream';
const mapStateToProps = state => ({
accessToken: state.getIn(['meta', 'access_token'])
});
const HashtagTimeline = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired
dispatch: React.PropTypes.func.isRequired,
accessToken: React.PropTypes.string.isRequired
},
mixins: [PureRenderMixin],
_subscribe (dispatch, id) {
if (typeof App !== 'undefined') {
this.subscription = App.cable.subscriptions.create({
channel: 'HashtagChannel',
tag: id
}, {
const { accessToken } = this.props;
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('tag', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
this.subscription = createStream(accessToken, `hashtag&tag=${id}`, {
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('tag', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
}
});
}
});
},
_unsubscribe () {
if (typeof this.subscription !== 'undefined') {
this.subscription.unsubscribe();
this.subscription.close();
this.subscription = null;
}
},
componentWillMount () {
componentDidMount () {
const { dispatch } = this.props;
const { id } = this.props.params;
@ -79,4 +83,4 @@ const HashtagTimeline = React.createClass({
});
export default connect()(HashtagTimeline);
export default connect(mapStateToProps)(HashtagTimeline);

View file

@ -9,46 +9,51 @@ import {
} from '../../actions/timelines';
import { defineMessages, injectIntl } from 'react-intl';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import createStream from '../../stream';
const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Public' }
});
const mapStateToProps = state => ({
accessToken: state.getIn(['meta', 'access_token'])
});
const PublicTimeline = React.createClass({
propTypes: {
dispatch: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
intl: React.PropTypes.object.isRequired,
accessToken: React.PropTypes.string.isRequired
},
mixins: [PureRenderMixin],
componentWillMount () {
const { dispatch } = this.props;
componentDidMount () {
const { dispatch, accessToken } = this.props;
dispatch(refreshTimeline('public'));
if (typeof App !== 'undefined') {
this.subscription = App.cable.subscriptions.create('PublicChannel', {
this.subscription = createStream(accessToken, 'public', {
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('public', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('public', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
}
});
}
});
},
componentWillUnmount () {
if (typeof this.subscription !== 'undefined') {
this.subscription.unsubscribe();
this.subscription.close();
this.subscription = null;
}
},
@ -65,4 +70,4 @@ const PublicTimeline = React.createClass({
});
export default connect()(injectIntl(PublicTimeline));
export default connect(mapStateToProps)(injectIntl(PublicTimeline));

View file

@ -0,0 +1,21 @@
import WebSocketClient from 'websocket.js';
const createWebSocketURL = (url) => {
const a = document.createElement('a');
a.href = url;
a.href = a.href;
a.protocol = a.protocol.replace('http', 'ws');
return a.href;
};
export default function getStream(accessToken, stream, { connected, received, disconnected }) {
const ws = new WebSocketClient(`${createWebSocketURL(STREAMING_API_BASE_URL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
ws.onopen = connected;
ws.onmessage = e => received(JSON.parse(e.data));
ws.onclose = disconnected;
return ws;
};

View file

@ -1,5 +1,6 @@
- content_for :header_tags do
:javascript
window.STREAMING_API_BASE_URL = '#{Rails.configuration.x.streaming_api_base_url}';
window.INITIAL_STATE = #{json_escape(render(file: 'home/initial_state', formats: :json))}
= javascript_include_tag 'application'