FEATURE: live refresh notifications as they happen

This commit is contained in:
Sam 2015-09-04 13:20:33 +10:00
parent 8bc7423045
commit a54e8f3c5e
6 changed files with 75 additions and 23 deletions

View File

@ -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 // TODO: It's a bit odd to use the store in a component, but this one really
// wants to reach out and grab notifications // wants to reach out and grab notifications
const store = this.container.lookup('store:main'); 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) { 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 { } else {
this.set('loadingNotifications', true); this.set('loadingNotifications', true);
} }

View File

@ -8,7 +8,12 @@ export default {
const user = container.lookup('current-user:main'), const user = container.lookup('current-user:main'),
site = container.lookup('site:main'), site = container.lookup('site:main'),
siteSettings = container.lookup('site-settings: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) { if (user) {
@ -38,6 +43,32 @@ export default {
if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) { if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) {
user.set('lastNotificationChange', new Date()); 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); }, user.notification_channel_position);
bus.subscribe("/categories", function(data) { bus.subscribe("/categories", function(data) {

View File

@ -43,6 +43,13 @@ KeyValueStore.prototype = {
get(key) { get(key) {
if (!safeLocalStorage) { return null; } if (!safeLocalStorage) { return null; }
return safeLocalStorage[this.context + key]; return safeLocalStorage[this.context + key];
},
getObject(key) {
if (!safeLocalStorage) { return null; }
try {
return JSON.parse(safeLocalStorage[this.context + key]);
} catch(e) {}
} }
}; };

View File

@ -1,8 +1,6 @@
import StaleResult from 'discourse/lib/stale-result'; import StaleResult from 'discourse/lib/stale-result';
import { hashString } from 'discourse/lib/hash'; import { hashString } from 'discourse/lib/hash';
var skipFirst = true;
// Mix this in to an adapter to provide stale caching in our key value store // Mix this in to an adapter to provide stale caching in our key value store
export default { export default {
storageKey(type, findArgs) { storageKey(type, findArgs) {
@ -10,27 +8,26 @@ export default {
return `${type}_${hashedArgs}`; return `${type}_${hashedArgs}`;
}, },
findStale(store, type, findArgs) { findStale(store, type, findArgs, opts) {
const staleResult = new StaleResult(); const staleResult = new StaleResult();
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs)
try { try {
if (!skipFirst) { const stored = this.keyValueStore.getItem(key);
const stored = this.keyValueStore.getItem(this.storageKey(type, findArgs));
if (stored) { if (stored) {
const parsed = JSON.parse(stored); const parsed = JSON.parse(stored);
staleResult.setResults(parsed); staleResult.setResults(parsed);
} }
} else {
skipFirst = false;
}
} catch(e) { } catch(e) {
// JSON parsing error // JSON parsing error
} }
return staleResult; 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) => { 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; return results;
}); });
} }

View File

@ -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 // See if the store can find stale data. We sometimes prefer to show stale data and
// refresh it in the background. // refresh it in the background.
findStale(type, findArgs) { findStale(type, findArgs, opts) {
const stale = this.adapterFor(type).findStale(this, type, findArgs); const stale = this.adapterFor(type).findStale(this, type, findArgs, opts);
if (stale.hasResults) { if (stale.hasResults) {
stale.results = this._hydrateFindResults(stale.results, type, findArgs); stale.results = this._hydrateFindResults(stale.results, type, findArgs);
} }
stale.refresh = () => this.find(type, findArgs); stale.refresh = () => this.find(type, findArgs, opts);
return stale; return stale;
}, },
find(type, findArgs) { find(type, findArgs, opts) {
return this.adapterFor(type).find(this, type, findArgs).then((result) => { return this.adapterFor(type).find(this, type, findArgs, opts).then((result) => {
return this._hydrateFindResults(result, type, findArgs); return this._hydrateFindResults(result, type, findArgs, opts);
}); });
}, },

View File

@ -306,10 +306,17 @@ class User < ActiveRecord::Base
end end
def publish_notifications_state 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}", MessageBus.publish("/notification/#{id}",
{unread_notifications: unread_notifications, {unread_notifications: unread_notifications,
unread_private_messages: unread_private_messages, 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 user_ids: [id] # only publish the notification to this user
) )
end end