From fbaea432660c1351b4cd3a468eccafc94522c6a7 Mon Sep 17 00:00:00 2001 From: Addy Osmani Date: Mon, 9 May 2016 12:29:51 +0100 Subject: [PATCH 01/11] Offline mode: Add REST-based HNService fallback --- src/services/HNServiceRest.js | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/services/HNServiceRest.js diff --git a/src/services/HNServiceRest.js b/src/services/HNServiceRest.js new file mode 100644 index 0000000..701ce2a --- /dev/null +++ b/src/services/HNServiceRest.js @@ -0,0 +1,62 @@ +/*global fetch*/ +require('isomorphic-fetch') +/* +A version of HNService which concumes the Firebase REST +endpoint (https://www.firebase.com/docs/rest/api/). This +is used when a user has enabled 'Offline Mode' in the +Settings panel and ensures responses can be easily fetched +and cached when paired with Service Worker. This cannot be +trivially done using just Web Sockets with the default +Firebase API and provides a sufficient fallback that works. + */ +var endPoint = 'https://hacker-news.firebaseio.com/v0' +var options = { + method: 'GET', + headers: { + 'Accept': 'application/json' + } +} + +function storiesRef(path) { + return fetch(endPoint + '/' + path + '.json', options) +} + +function itemRef(id) { + return fetch(endPoint + '/item/' + id + '.json', options) +} + +function userRef(id) { + return fetch(endPoint + '/user/' + id + '.json', options) +} + +function updatesRef() { + return fetch(endPoint + '/updates/items/' + '.json', options) +} + +function fetchItem(id, cb) { + itemRef(id).once('value', function(snapshot) { + cb(snapshot.val()) + }) +} + +function fetchItems(ids, cb) { + var items = [] + ids.forEach(function(id) { + fetchItem(id, addItem) + }) + function addItem(item) { + items.push(item) + if (items.length >= ids.length) { + cb(items) + } + } +} + +module.exports = { + fetchItem, + fetchItems, + storiesRef, + itemRef, + userRef, + updatesRef +} From e0a2edad7469a784b879b9ca2d613cf1e80b500b Mon Sep 17 00:00:00 2001 From: Addy Osmani Date: Mon, 9 May 2016 12:30:24 +0100 Subject: [PATCH 02/11] Offline mode: enable for Comments --- src/Comment.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Comment.js b/src/Comment.js index 850ec78..db91788 100644 --- a/src/Comment.js +++ b/src/Comment.js @@ -3,6 +3,7 @@ var ReactFireMixin = require('reactfire') var CommentThreadStore = require('./stores/CommentThreadStore') var HNService = require('./services/HNService') +var HNServiceRest = require('./services/HNServiceRest') var SettingsStore = require('./stores/SettingsStore') var CommentMixin = require('./mixins/CommentMixin') @@ -86,7 +87,17 @@ var Comment = React.createClass({ }, bindFirebaseRef() { - this.bindAsObject(HNService.itemRef(this.props.id), 'comment', this.handleFirebaseRefCancelled) + if (SettingsStore.offlineMode) { + HNServiceRest.itemRef(this.props.id).then(function(res) { + return res.json() + }).then(function(snapshot) { + this.replaceState({ comment: snapshot }) + }.bind(this)) + } + else { + this.bindAsObject(HNService.itemRef(this.props.id), 'comment', this.handleFirebaseRefCancelled) + } + if (this.timeout) { this.timeout = null } From 710c13183690cf121cb47c84c32553a185d39fd2 Mon Sep 17 00:00:00 2001 From: Addy Osmani Date: Mon, 9 May 2016 12:30:39 +0100 Subject: [PATCH 03/11] Offline mode: enable for items --- src/Item.js | 29 ++++++++++++++++++++++++++--- src/stores/ItemStore.js | 10 ++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Item.js b/src/Item.js index f14a4fc..8369620 100644 --- a/src/Item.js +++ b/src/Item.js @@ -3,6 +3,7 @@ var ReactFireMixin = require('reactfire') var TimeAgo = require('react-timeago').default var HNService = require('./services/HNService') +var HNServiceRest = require('./services/HNServiceRest') var StoryCommentThreadStore = require('./stores/StoryCommentThreadStore') var ItemStore = require('./stores/ItemStore') @@ -14,6 +15,8 @@ var ItemMixin = require('./mixins/ItemMixin') var cx = require('./utils/buildClassName') var setTitle = require('./utils/setTitle') +var SettingsStore = require('./stores/SettingsStore') + function timeUnitsAgo(value, unit, suffix) { if (value === 1) { return unit @@ -31,7 +34,17 @@ var Item = React.createClass({ }, componentWillMount() { - this.bindAsObject(HNService.itemRef(this.props.params.id), 'item') + if (SettingsStore.offlineMode) { + HNServiceRest.itemRef(this.props.params.id).then(function(res) { + return res.json() + }).then(function(snapshot) { + this.replaceState({ item: snapshot }) + }.bind(this)) + } + else { + this.bindAsObject(HNService.itemRef(this.props.params.id), 'item') + } + if (this.state.item.id) { this.threadStore = new StoryCommentThreadStore(this.state.item, this.handleCommentsChanged, {cached: true}) setTitle(this.state.item.title) @@ -58,8 +71,18 @@ var Item = React.createClass({ this.threadStore = new StoryCommentThreadStore(item, this.handleCommentsChanged, {cached: true}) setTitle(item.title) } - this.bindAsObject(HNService.itemRef(nextProps.params.id), 'item') - this.setState({item: item || {}}) + + if (SettingsStore.offlineMode) { + HNServiceRest.itemRef(nextProps.params.id).then(function(res) { + return res.json() + }).then(function(snapshot) { + this.replaceState({ item: snapshot }) + }.bind(this)) + } + else { + this.bindAsObject(HNService.itemRef(nextProps.params.id), 'item') + this.setState({item: item || {}}) + } } }, diff --git a/src/stores/ItemStore.js b/src/stores/ItemStore.js index ec6de5c..9b637ac 100644 --- a/src/stores/ItemStore.js +++ b/src/stores/ItemStore.js @@ -1,8 +1,9 @@ var HNService = require('../services/HNService') +var HNServiceRest = require('../services/HNServiceRest') var StoryStore = require('./StoryStore') var UpdatesStore = require('./UpdatesStore') - +var SettingsStore = require('./SettingsStore') var commentParentLookup = {} var titleCache = {} @@ -72,7 +73,12 @@ var ItemStore = { setImmediate(cb, cachedItem) } else { - HNService.fetchItem(id, cb) + if (SettingsStore.offlineMode) { + HNServiceRest.fetchItem(id, cb) + } + else { + HNService.fetchItem(id, cb) + } } }, From 995b30f80990bd060c6849cbd834b3f0266d44ca Mon Sep 17 00:00:00 2001 From: Addy Osmani Date: Mon, 9 May 2016 12:31:03 +0100 Subject: [PATCH 04/11] Offline mode: enable for stories --- src/StoryListItem.js | 15 +++++++++++-- src/stores/StoryStore.js | 46 +++++++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/StoryListItem.js b/src/StoryListItem.js index 13580bc..2a50b46 100644 --- a/src/StoryListItem.js +++ b/src/StoryListItem.js @@ -3,6 +3,7 @@ var ReactFireMixin = require('reactfire') var StoryCommentThreadStore = require('./stores/StoryCommentThreadStore') var HNService = require('./services/HNService') +var HNServiceRest = require('./services/HNServiceRest') var SettingsStore = require('./stores/SettingsStore') var StoryStore = require('./stores/StoryStore') @@ -94,8 +95,18 @@ var StoryListItem = React.createClass({ * initialise its comment thread state. */ initLiveItem(props) { - // If we were given a cached item to display initially, it will be replaced - this.bindAsObject(HNService.itemRef(props.id), 'item') + if (SettingsStore.offlineMode) { + HNServiceRest.itemRef(props.id).then(function(res) { + return res.json() + }).then(function(snapshot) { + this.replaceState({ item: snapshot }) + }.bind(this)) + } + else { + // If we were given a cached item to display initially, it will be replaced + this.bindAsObject(HNService.itemRef(props.id), 'item') + } + this.threadState = StoryCommentThreadStore.loadState(props.id) this.props.store.addListener(props.id, this.updateThreadState) }, diff --git a/src/stores/StoryStore.js b/src/stores/StoryStore.js index 34057df..1765171 100644 --- a/src/stores/StoryStore.js +++ b/src/stores/StoryStore.js @@ -1,6 +1,8 @@ var {EventEmitter} = require('events') var HNService = require('../services/HNService') +var HNServiceRest = require('../services/HNServiceRest') +var SettingsStore = require('./SettingsStore') var extend = require('../utils/extend') @@ -91,20 +93,36 @@ class StoryStore extends EventEmitter { * Handle story id snapshots from Firebase. */ onStoriesUpdated(snapshot) { - idCache[this.type] = snapshot.val() + if (SettingsStore.offlineMode) { + idCache[this.type] = snapshot + } + else { + idCache[this.type] = snapshot.val() + } populateStoryList(this.type) this.emit('update', this.getState()) } start() { - firebaseRef = HNService.storiesRef(this.type) - firebaseRef.on('value', this.onStoriesUpdated) + if (SettingsStore.offlineMode) { + HNServiceRest.storiesRef(this.type).then(function(res) { + return res.json() + }).then(function(snapshot) { + this.onStoriesUpdated(snapshot) + }.bind(this)) + } + else { + firebaseRef = HNService.storiesRef(this.type) + firebaseRef.on('value', this.onStoriesUpdated) + } window.addEventListener('storage', this.onStorage) } stop() { if (firebaseRef !== null) { - firebaseRef.off() + if (!SettingsStore.offlineMode) { + firebaseRef.off() + } firebaseRef = null } window.removeEventListener('storage', this.onStorage) @@ -124,16 +142,28 @@ extend(StoryStore, { * Deserialise caches from sessionStorage. */ loadSession() { - idCache = parseJSON(window.sessionStorage.idCache, {}) - itemCache = parseJSON(window.sessionStorage.itemCache, {}) + if (SettingsStore.offlineMode) { + idCache = parseJSON(window.localStorage.idCache, {}) + itemCache = parseJSON(window.localStorage.itemCache, {}) + } + else { + idCache = parseJSON(window.sessionStorage.idCache, {}) + itemCache = parseJSON(window.sessionStorage.itemCache, {}) + } }, /** * Serialise caches to sessionStorage as JSON. */ saveSession() { - window.sessionStorage.idCache = JSON.stringify(idCache) - window.sessionStorage.itemCache = JSON.stringify(itemCache) + if (SettingsStore.offlineMode) { + window.localStorage.setItem('idCache', JSON.stringify(idCache)) + window.localStorage.setItem('itemCache', JSON.stringify(itemCache)) + } + else { + window.sessionStorage.idCache = JSON.stringify(idCache) + window.sessionStorage.itemCache = JSON.stringify(itemCache) + } } }) From 83ebc1b4fa9e03c38476bc6f07b036d3404efa67 Mon Sep 17 00:00:00 2001 From: Addy Osmani Date: Mon, 9 May 2016 12:31:15 +0100 Subject: [PATCH 05/11] Offline mode: enable for permalink comments --- src/PermalinkedComment.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/PermalinkedComment.js b/src/PermalinkedComment.js index 4d14230..47a40bc 100644 --- a/src/PermalinkedComment.js +++ b/src/PermalinkedComment.js @@ -4,6 +4,7 @@ var withRouter = require('react-router/lib/withRouter') var CommentThreadStore = require('./stores/CommentThreadStore') var HNService = require('./services/HNService') +var HNServiceRest = require('./services/HNServiceRest') var SettingsStore = require('./stores/SettingsStore') var UpdatesStore = require('./stores/UpdatesStore') @@ -32,7 +33,16 @@ var PermalinkedComment = React.createClass({ }, componentWillMount() { - this.bindAsObject(HNService.itemRef(this.props.params.id), 'comment') + if (SettingsStore.offlineMode) { + HNServiceRest.itemRef(this.props.params.id).then(function(res) { + return res.json() + }).then(function(snapshot) { + this.replaceState({ comment: snapshot }) + }.bind(this)) + } + else { + this.bindAsObject(HNService.itemRef(this.props.params.id), 'comment') + } if (this.state.comment.id) { this.commentLoaded(this.state.comment) } From fffe05e0c221ee4a032352a41916c41f3777ea91 Mon Sep 17 00:00:00 2001 From: Addy Osmani Date: Mon, 9 May 2016 12:31:35 +0100 Subject: [PATCH 06/11] Correct linting for runtime-caching --- public/runtime-caching.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/runtime-caching.js b/public/runtime-caching.js index 2824bd5..8b5065f 100644 --- a/public/runtime-caching.js +++ b/public/runtime-caching.js @@ -12,10 +12,10 @@ origin: /\.(?:googleapis|gstatic|firebaseio)\.com$/ }) global.toolbox.router.get('/(.+)', global.toolbox.fastest, { - origin: 'https://hacker-news.firebaseio.com' + origin: 'https://hacker-news.firebaseio.com' }) global.toolbox.router.get('/(.+)', global.toolbox.fastest, { - origin: 'https://s-usc1c-nss-136.firebaseio.com' + origin: 'https://s-usc1c-nss-136.firebaseio.com' }) })(self) From 240f39c9375627a1b066c6b4478fe4c76d5361ee Mon Sep 17 00:00:00 2001 From: Addy Osmani Date: Mon, 9 May 2016 12:31:56 +0100 Subject: [PATCH 07/11] Offline mode: enable for updates --- src/stores/UpdatesStore.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/stores/UpdatesStore.js b/src/stores/UpdatesStore.js index ff711e6..58051bc 100644 --- a/src/stores/UpdatesStore.js +++ b/src/stores/UpdatesStore.js @@ -1,6 +1,8 @@ var EventEmitter = require('events').EventEmitter var HNService = require('../services/HNService') +var HNServiceRest = require('../services/HNServiceRest') +var SettingsStore = require('./SettingsStore') var {UPDATES_CACHE_SIZE} = require('../utils/constants') var extend = require('../utils/extend') @@ -93,7 +95,6 @@ function handleUpdateItems(items) { updatesCache.stories[item.id] = item } } - populateUpdates() UpdatesStore.emit('updates', updates) } @@ -111,16 +112,27 @@ var UpdatesStore = extend(new EventEmitter(), { start() { if (updatesRef === null) { - updatesRef = HNService.updatesRef() - updatesRef.on('value', function(snapshot) { - HNService.fetchItems(snapshot.val(), handleUpdateItems) - }) + if (SettingsStore.offlineMode) { + HNServiceRest.updatesRef().then(function(res) { + return res.json() + }).then(function(snapshot) { + HNServiceRest.fetchItems(snapshot, handleUpdateItems) + }) + } + else { + updatesRef = HNService.updatesRef() + updatesRef.on('value', function(snapshot) { + HNService.fetchItems(snapshot.val(), handleUpdateItems) + }) + } } }, stop() { - updatesRef.off() - updatesRef = null + if (!SettingsStore.offlineMode) { + updatesRef.off() + updatesRef = null + } }, getUpdates() { From 82ddd1dd24e3c3e4c7601060ebba7c28b6044ecb Mon Sep 17 00:00:00 2001 From: Addy Osmani Date: Mon, 9 May 2016 12:32:09 +0100 Subject: [PATCH 08/11] Offline mode: expose UI/option in Settings panel --- src/Settings.js | 6 ++++++ src/stores/SettingsStore.js | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Settings.js b/src/Settings.js index 16f423e..5e8115a 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -38,6 +38,12 @@ var Settings = React.createClass({

Show "reply" links to Hacker News

+
+ +

Show items flagged as dead.

+