From a54e8f3c5eebaae06c1041fb9f4c02d9a6704acc Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 4 Sep 2015 13:20:33 +1000 Subject: [PATCH] FEATURE: live refresh notifications as they happen --- .../discourse/components/user-menu.js.es6 | 14 ++++++-- .../subscribe-user-notifications.js.es6 | 33 ++++++++++++++++++- .../discourse/lib/key-value-store.js.es6 | 7 ++++ .../mixins/stale-local-storage.js.es6 | 23 ++++++------- .../javascripts/discourse/models/store.js.es6 | 12 +++---- app/models/user.rb | 9 ++++- 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/discourse/components/user-menu.js.es6 b/app/assets/javascripts/discourse/components/user-menu.js.es6 index b60a0eb8282..7815295ef20 100644 --- a/app/assets/javascripts/discourse/components/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/user-menu.js.es6 @@ -50,10 +50,20 @@ export default Ember.Component.extend({ // TODO: It's a bit odd to use the store in a component, but this one really // wants to reach out and grab notifications const store = this.container.lookup('store:main'); - const stale = store.findStale('notification', {recent: true, limit }); + const stale = store.findStale('notification', {recent: true, limit }, {storageKey: 'recent-notifications'}); if (stale.hasResults) { - this.set('notifications', stale.results); + const results = stale.results; + var content = results.get('content'); + + // we have to truncate to limit, otherwise we will render too much + if (content && (content.length > limit)) { + content = content.splice(0, limit); + results.set('content', content); + results.set('totalRows', limit); + } + + this.set('notifications', results); } else { this.set('loadingNotifications', true); } diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index 19c64a61b73..92ab828c680 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -8,7 +8,12 @@ export default { const user = container.lookup('current-user:main'), site = container.lookup('site:main'), siteSettings = container.lookup('site-settings:main'), - bus = container.lookup('message-bus:main'); + bus = container.lookup('message-bus:main'), + keyValueStore = container.lookup('key-value-store:main'); + + // clear old cached notifications + // they could be a week old for all we know + keyValueStore.remove('recent-notifications'); if (user) { @@ -38,6 +43,32 @@ export default { if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) { user.set('lastNotificationChange', new Date()); } + + var stale = keyValueStore.getObject('recent-notifications'); + const lastNotification = data.last_notification && data.last_notification.notification; + + if (stale && stale.notifications && lastNotification) { + + const oldNotifications = stale.notifications; + const staleIndex = _.findIndex(oldNotifications, {id: lastNotification.id}); + + if (staleIndex > -1) { + oldNotifications.splice(staleIndex, 1); + } + + // this gets a bit tricky, uread pms are bumped to front + var insertPosition = 0; + if (lastNotification.notification_type !== 6) { + insertPosition = _.findIndex(oldNotifications, function(n){ + return n.notification_type !== 6 || n.read; + }); + insertPosition = insertPosition === -1 ? oldNotifications.length - 1 : insertPosition; + } + + oldNotifications.splice(insertPosition, 0, lastNotification); + keyValueStore.setItem('recent-notifications', JSON.stringify(stale)); + + } }, user.notification_channel_position); bus.subscribe("/categories", function(data) { diff --git a/app/assets/javascripts/discourse/lib/key-value-store.js.es6 b/app/assets/javascripts/discourse/lib/key-value-store.js.es6 index b5f13d024ca..30f86b16ec7 100644 --- a/app/assets/javascripts/discourse/lib/key-value-store.js.es6 +++ b/app/assets/javascripts/discourse/lib/key-value-store.js.es6 @@ -43,6 +43,13 @@ KeyValueStore.prototype = { get(key) { if (!safeLocalStorage) { return null; } return safeLocalStorage[this.context + key]; + }, + + getObject(key) { + if (!safeLocalStorage) { return null; } + try { + return JSON.parse(safeLocalStorage[this.context + key]); + } catch(e) {} } }; diff --git a/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 b/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 index d8816c981e6..07dcd548a55 100644 --- a/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 +++ b/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 @@ -1,8 +1,6 @@ import StaleResult from 'discourse/lib/stale-result'; import { hashString } from 'discourse/lib/hash'; -var skipFirst = true; - // Mix this in to an adapter to provide stale caching in our key value store export default { storageKey(type, findArgs) { @@ -10,17 +8,14 @@ export default { return `${type}_${hashedArgs}`; }, - findStale(store, type, findArgs) { + findStale(store, type, findArgs, opts) { const staleResult = new StaleResult(); + const key = (opts && opts.storageKey) || this.storageKey(type, findArgs) try { - if (!skipFirst) { - const stored = this.keyValueStore.getItem(this.storageKey(type, findArgs)); - if (stored) { - const parsed = JSON.parse(stored); - staleResult.setResults(parsed); - } - } else { - skipFirst = false; + const stored = this.keyValueStore.getItem(key); + if (stored) { + const parsed = JSON.parse(stored); + staleResult.setResults(parsed); } } catch(e) { // JSON parsing error @@ -28,9 +23,11 @@ export default { return staleResult; }, - find(store, type, findArgs) { + find(store, type, findArgs, opts) { + const key = (opts && opts.storageKey) || this.storageKey(type, findArgs) + return this._super(store, type, findArgs).then((results) => { - this.keyValueStore.setItem(this.storageKey(type, findArgs), JSON.stringify(results)); + this.keyValueStore.setItem(key, JSON.stringify(results)); return results; }); } diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index 851b712a61c..dec3fcec764 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -71,18 +71,18 @@ export default Ember.Object.extend({ // See if the store can find stale data. We sometimes prefer to show stale data and // refresh it in the background. - findStale(type, findArgs) { - const stale = this.adapterFor(type).findStale(this, type, findArgs); + findStale(type, findArgs, opts) { + const stale = this.adapterFor(type).findStale(this, type, findArgs, opts); if (stale.hasResults) { stale.results = this._hydrateFindResults(stale.results, type, findArgs); } - stale.refresh = () => this.find(type, findArgs); + stale.refresh = () => this.find(type, findArgs, opts); return stale; }, - find(type, findArgs) { - return this.adapterFor(type).find(this, type, findArgs).then((result) => { - return this._hydrateFindResults(result, type, findArgs); + find(type, findArgs, opts) { + return this.adapterFor(type).find(this, type, findArgs, opts).then((result) => { + return this._hydrateFindResults(result, type, findArgs, opts); }); }, diff --git a/app/models/user.rb b/app/models/user.rb index a329f88dc05..31a37e2b7e6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -306,10 +306,17 @@ class User < ActiveRecord::Base end def publish_notifications_state + # publish last notification json with the message so we + # can apply an update + notification = notifications.visible.order('notifications.id desc').first + json = NotificationSerializer.new(notification).as_json if notification + MessageBus.publish("/notification/#{id}", {unread_notifications: unread_notifications, unread_private_messages: unread_private_messages, - total_unread_notifications: total_unread_notifications}, + total_unread_notifications: total_unread_notifications, + last_notification: json + }, user_ids: [id] # only publish the notification to this user ) end