From ccb8ac857330e1ad3aee37b340e5c6e242ac1dd6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 4 Feb 2017 00:34:31 +0100 Subject: 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. --- app/assets/javascripts/application.js | 1 - app/assets/javascripts/cable.js | 12 ------ .../javascripts/components/containers/mastodon.jsx | 43 ++++++++++---------- .../components/features/hashtag_timeline/index.jsx | 46 ++++++++++++---------- .../components/features/public_timeline/index.jsx | 41 ++++++++++--------- app/assets/javascripts/components/stream.jsx | 21 ++++++++++ 6 files changed, 91 insertions(+), 73 deletions(-) delete mode 100644 app/assets/javascripts/cable.js create mode 100644 app/assets/javascripts/components/stream.jsx (limited to 'app/assets') diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c442ded61..e2fffd932 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,4 +13,3 @@ //= require jquery //= require jquery_ujs //= require components -//= require cable diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js deleted file mode 100644 index 03258761c..000000000 --- a/app/assets/javascripts/cable.js +++ /dev/null @@ -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); diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 5fd43fb2b..46a01b200 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -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; - - if (typeof App !== 'undefined') { - this.subscription = App.cable.subscriptions.create('TimelineChannel', { - - 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; - } + componentDidMount() { + const { locale } = this.props; + const accessToken = store.getState().getIn(['meta', 'access_token']); + + 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; } + } - }); - } + }); // 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; } }, diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx index 7548e6d56..4a0e7684d 100644 --- a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx +++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx @@ -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 - }, { - - received (data) { - switch(data.event) { - case 'update': - dispatch(updateTimeline('tag', JSON.parse(data.payload))); - break; - case 'delete': - dispatch(deleteFromTimelines(data.payload)); - break; - } + const { accessToken } = this.props; + + 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); diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx index 42970061c..36d68dbbb 100644 --- a/app/assets/javascripts/components/features/public_timeline/index.jsx +++ b/app/assets/javascripts/components/features/public_timeline/index.jsx @@ -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)); diff --git a/app/assets/javascripts/components/stream.jsx b/app/assets/javascripts/components/stream.jsx new file mode 100644 index 000000000..0787399f6 --- /dev/null +++ b/app/assets/javascripts/components/stream.jsx @@ -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; +}; -- cgit