From b3f662945a0a9519735565a862ba4f75eb820b42 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jun 21 2017 13:41:17 +0000 Subject: Setup SSE as a global element used to reload widgets --- diff --git a/hubs/default_config.py b/hubs/default_config.py index 72cda31..917840b 100644 --- a/hubs/default_config.py +++ b/hubs/default_config.py @@ -11,7 +11,10 @@ PROMOTED_GROUPS = [ HUB_OF_THE_MONTH = 'commops' -SSE_URL = 'http://localhost:8080/user/' +SSE_URL = { + "port": "8080", + "path": "/sse", +} PLUS_PLUS_URL = 'http://localhost:5001/user/' PLUS_PLUS_TOKEN = 'thisismytoken' diff --git a/hubs/static/client/app/components/feed/ItemsGetter.jsx b/hubs/static/client/app/components/feed/ItemsGetter.jsx index 16d87de..bfc51a8 100644 --- a/hubs/static/client/app/components/feed/ItemsGetter.jsx +++ b/hubs/static/client/app/components/feed/ItemsGetter.jsx @@ -9,68 +9,71 @@ export default class ItemsGetter extends React.Component { sseError: false, loading: false, }; - this.source = null; - this.setupSource = this.setupSource.bind(this); - this.handleEventError = this.handleEventError.bind(this); - this.loadExistingFromServer = this.loadExistingFromServer.bind(this); + this.sseSource = null; + this.setupSSESource = this.setupSSESource.bind(this); + this.handleSSEEvent = this.handleSSEEvent.bind(this); + this.handleSSEEventError = this.handleSSEEventError.bind(this); + this.loadFromServer = this.loadFromServer.bind(this); } componentDidMount() { - this.loadExistingFromServer(); + this.setupSSESource(); + this.loadFromServer(); } componentWillUnmount() { this.serverRequest.abort(); - if (this.source !== null) { - this.source.removeEventListener('error', this.handleEventError); - this.source.close(); + if (this.sseSource) { + this.sseSource.removeEventListener( + this.props.sseEventName, this.this.props.handleSSEEvent); + this.sseSource.removeEventListener('error', this.handleSSEEventError); } } - componentDidUpdate(prevProps, prevState) { - if (this.props.sseActive && !this.source) { - // Only setup the source after the initial loading, or new elements may - // be overwritten by the call to this.props.handleInitialData(). - this.setupSource(); - } - } - - setupSource() { - if (!this.props.urls.sse) { + setupSSESource() { + //if (!this.props.sseEventName || !this.props.handleSSEEvent) { + if (!this.props.sse.eventName || !this.props.sse.shouldReload) { // Auto-update is disabled. return; } - if (!window.EventSource) { - this.setState({ - sseError: ("Cannot auto-update the feed, you will have to refresh " - +"the page manually to see new elements.") - }); + this.sseSource = document.sseSource; + if (!this.sseSource || this.sseSource.readyState === EventSource.CLOSED) { + this.handleSSEEventError(); return; } - this.source = new EventSource(this.props.urls.sse); - this.source.addEventListener('error', this.handleEventError); - this.source.onmessage = (resp) => { - this.props.handleMessage(JSON.parse(resp.data)); - }; + this.sseSource.addEventListener('error', this.handleSSEEventError); + this.sseSource.addEventListener( + this.props.sse.eventName, this.handleSSEEvent); } - handleEventError() { + handleSSEEvent(e) { + if (this.props.sse.shouldReload(e.data)) { + this.loadFromServer(); + } + } + + handleSSEEventError() { this.setState({ - sseError: "There was an error retrieving the new elements." + sseError: ("Cannot auto-update the feed, you will have to refresh " + +"the page manually to see new elements.") }); } - loadExistingFromServer() { + loadFromServer() { this.setState({loading: true}); + if (this.serverRequest && + this.serverRequest.readyState !== XMLHttpRequest.DONE) { + this.serverRequest.abort(); + } this.serverRequest = $.ajax({ - url: this.props.urls.content, + url: this.props.url, method: 'GET', dataType: 'json', cache: false, success: (data, textStatus, jqXHR) => { - this.props.handleInitialData(data.data); + this.props.handleData(data.data); }, error: (xhr, status, err) => { console.error(status, err.toString()); diff --git a/hubs/static/client/app/components/feed/__tests__/Feed.test.js b/hubs/static/client/app/components/feed/__tests__/Feed.test.js index d1cdf24..9f2e9be 100644 --- a/hubs/static/client/app/components/feed/__tests__/Feed.test.js +++ b/hubs/static/client/app/components/feed/__tests__/Feed.test.js @@ -23,7 +23,7 @@ describe('Feed', () => { it('should create the children', () => { const component = TestUtils.renderIntoDocument( - + ); const node = ReactDOM.findDOMNode(component); expect(Panel.mock.calls.length).toEqual(3); diff --git a/hubs/static/client/app/core/Streams.jsx b/hubs/static/client/app/core/Streams.jsx index 144fab7..a681951 100644 --- a/hubs/static/client/app/core/Streams.jsx +++ b/hubs/static/client/app/core/Streams.jsx @@ -16,35 +16,21 @@ export default class Streams extends React.Component { this.state = { notifItems: [], savedItems: [], - sseActive: false, }; - this.handleInitialData = this.handleInitialData.bind(this); - this.handleInitialSavedData = this.handleInitialSavedData.bind(this); - this.handleMessage = this.handleMessage.bind(this); + this.handleStreamData = this.handleStreamData.bind(this); + this.handleSavedData = this.handleSavedData.bind(this); this.handleSave = this.handleSave.bind(this); this.handleUnsave = this.handleUnsave.bind(this); } - handleInitialData(data) { - // Only activate SSE after the initial loading, or new elements may - // be overwritten. - this.setState({ - notifItems: data, - sseActive: true, - }); + handleStreamData(data) { + this.setState({notifItems: data}); } - handleInitialSavedData(data) { + handleSavedData(data) { this.setState({savedItems: data}); } - handleMessage(msg) { - var items = this.state.notifItems; - items.unshift(msg); - items = items.slice(0, this.props.messageLimit); - this.setState({notifItems: items}); - } - handleSave(item) { if (!this.props.urls.saved) { return; } const payload = { @@ -103,14 +89,16 @@ export default class Streams extends React.Component { (username === this.props.username), + }} + handleData={this.handleStreamData} > diff --git a/hubs/static/client/app/widgets/feed/Widget.jsx b/hubs/static/client/app/widgets/feed/Widget.jsx index 38a006d..ded612e 100644 --- a/hubs/static/client/app/widgets/feed/Widget.jsx +++ b/hubs/static/client/app/widgets/feed/Widget.jsx @@ -9,95 +9,26 @@ export default class Widget extends React.Component { super(props); this.state = { items: [], - sseActive: false, }; - this.handleInitialData = this.handleInitialData.bind(this); - this.handleMessage = this.handleMessage.bind(this); - this.handleSave = this.handleSave.bind(this); - this.handleUnsave = this.handleUnsave.bind(this); + this.handleServerData = this.handleServerData.bind(this); } - handleInitialData(data) { - // Only activate SSE after the initial loading, or new elements may - // be overwritten. - this.setState({ - items: data, - sseActive: true, - }); - } - - handleMessage(msg) { - if (this.state.items.length >= this.props.messageLimit) { - this.state.items.pop(); - } - this.state.items.unshift(msg); - this.setState({items: this.state.items}); - } - - handleSave(item) { - if (!this.props.urls.save) { return; } - const payload = { - link: item.link, - markup: item.markup, - secondary_icon: item.secondary_icon, - dom_id: item.dom_id, - }; - $.ajax({ - type: 'POST', - url: this.props.urls.save, - data: JSON.stringify(payload), - contentType: 'application/json', - success: () => { - this.setState((prevState, props) => { - var items = prevState.items.map((currentItem) => { - if (currentItem.dom_id === item.dom_id) { - currentItem.saved = true; - } - return currentItem; - }) - return {items}; - }); - }, - }); - } - - handleUnsave(item) { - if (!this.props.urls.save) { return; } - var updateItemState = (item) => { - this.setState((prevState, props) => { - var items = prevState.items.map((currentItem) => { - if (currentItem.dom_id === item.dom_id) { - currentItem.saved = false; - } - return currentItem; - }); - return {items}; - }); - }; - if (item.idx) { - // Already saved - $.ajax({ - type: 'DELETE', - url: `${this.props.urls.save}${item.idx}/`, - success: () => { updateItemState(item) }, - }); - } else { - updateItemState(item); - } + handleServerData(data) { + this.setState({items: data}); } render() { return ( (idx === this.props.widgetIdx), + }} + handleData={this.handleServerData} > ); diff --git a/hubs/static/js/utils.js b/hubs/static/js/utils.js index 3474786..1f33870 100644 --- a/hubs/static/js/utils.js +++ b/hubs/static/js/utils.js @@ -32,3 +32,30 @@ function setup_widgets() { }); }); } + +function setup_sse(url) { + if (!url || !window.EventSource) { + // Auto-update is disabled. + return; + } + var sseSource = new EventSource(url); + sseSource.addEventListener("hubs:widget-updated", function(e) { + var widget = $("#widget-" + e.data); + if (widget.find("div.card[data-disable-autoreload]").length !== 0) { return; } + $.ajax({ + url: widget.attr("data-url"), + dataType: 'html', + success: function(html) { + widget.css("opacity", "0.5"); + widget.html(html); + widget.animate({opacity: 1}, 200); + }, + error: function() { + console.log('error updating the widget'); + console.trace(); + }, + }); + }); + // Make it accessible by other components on the page. + document.sseSource = sseSource; + } diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html index c0f1a84..d39d1ca 100644 --- a/hubs/templates/hubs.html +++ b/hubs/templates/hubs.html @@ -323,6 +323,7 @@ $(function() { visit_counter() setup_widgets(); + setup_sse({{ sse_url | tojson }}); setup_edit_btns(); {% if hub.allows(g.auth.user, "config") %} setup_settings(); diff --git a/hubs/templates/stream.html b/hubs/templates/stream.html index cae069f..4292f6d 100644 --- a/hubs/templates/stream.html +++ b/hubs/templates/stream.html @@ -29,14 +29,13 @@ {{ super() }}