Skip to content

Commit

Permalink
FEATURE: live refresh notifications as they happen
Browse files Browse the repository at this point in the history
  • Loading branch information
SamSaffron committed Sep 4, 2015
1 parent 8bc7423 commit a54e8f3
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 23 deletions.
14 changes: 12 additions & 2 deletions app/assets/javascripts/discourse/components/user-menu.js.es6
Expand Up @@ -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);
}
Expand Down
Expand Up @@ -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) {

Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions app/assets/javascripts/discourse/lib/key-value-store.js.es6
Expand Up @@ -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) {}
}
};

Expand Down
23 changes: 10 additions & 13 deletions app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6
@@ -1,36 +1,33 @@
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) {
const hashedArgs = Math.abs(hashString(JSON.stringify(findArgs)));
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
}
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;
});
}
Expand Down
12 changes: 6 additions & 6 deletions app/assets/javascripts/discourse/models/store.js.es6
Expand Up @@ -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);
});
},

Expand Down
9 changes: 8 additions & 1 deletion app/models/user.rb
Expand Up @@ -306,10 +306,17 @@ def saw_notification_id(notification_id)
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
Expand Down

0 comments on commit a54e8f3

Please sign in to comment.