mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-03-14 00:50:14 -04:00
Refactor notifications localStorage
cache into adapter pattern.
Sometimes you want stale data right away, then refresh it async. This adds `findStale` to the store for that case. If it returns an object with `hasResults` you can get the `results` and display them. It also returns a `refresh()` method to freshen up the stale data. To enable `localStorage` support for stale data, just include the mixin `StaleLocalStorage` into an adapter for that model. This commit includes a sample of doing that for `Notifications`.
This commit is contained in:
parent
b8c3187a94
commit
ddf0db0338
10 changed files with 113 additions and 47 deletions
|
@ -0,0 +1,4 @@
|
|||
import RestAdapter from 'discourse/adapters/rest';
|
||||
import StaleLocalStorage from 'discourse/mixins/stale-local-storage';
|
||||
|
||||
export default RestAdapter.extend(StaleLocalStorage);
|
|
@ -1,3 +1,4 @@
|
|||
import StaleResult from 'discourse/lib/stale-result';
|
||||
const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host'];
|
||||
|
||||
export function Result(payload, responseJson) {
|
||||
|
@ -53,6 +54,10 @@ export default Ember.Object.extend({
|
|||
return ajax(this.pathFor(store, type, findArgs)).catch(rethrow);
|
||||
},
|
||||
|
||||
findStale() {
|
||||
return new StaleResult();
|
||||
},
|
||||
|
||||
update(store, type, id, attrs) {
|
||||
const data = {};
|
||||
const typeField = Ember.String.underscore(type);
|
||||
|
|
|
@ -31,50 +31,27 @@ export default Ember.Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
loadCachedNotifications() {
|
||||
var notifications;
|
||||
try {
|
||||
notifications = JSON.parse(localStorage["notifications"]);
|
||||
notifications = notifications.map(n => Em.Object.create(n));
|
||||
} catch (e) {
|
||||
notifications = null;
|
||||
}
|
||||
return notifications;
|
||||
},
|
||||
|
||||
// TODO push this kind of functionality into Rest thingy
|
||||
cacheNotifications(notifications) {
|
||||
const keys = ["id", "notification_type", "read", "created_at", "post_number", "topic_id", "slug", "data"];
|
||||
const serialized = JSON.stringify(notifications.map(n => n.getProperties(keys)));
|
||||
const changed = serialized !== localStorage["notifications"];
|
||||
localStorage["notifications"] = serialized;
|
||||
return changed;
|
||||
},
|
||||
|
||||
refreshNotifications() {
|
||||
|
||||
if (this.get('loadingNotifications')) { return; }
|
||||
|
||||
var cached = this.loadCachedNotifications();
|
||||
|
||||
if (cached) {
|
||||
this.set("notifications", cached);
|
||||
} else {
|
||||
this.set("loadingNotifications", true);
|
||||
}
|
||||
|
||||
// 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');
|
||||
store.find('notification', {recent: true}).then((notifications) => {
|
||||
const stale = store.findStale('notification', {recent: true});
|
||||
|
||||
if (stale.hasResults) {
|
||||
this.set('notifications', stale.results);
|
||||
} else {
|
||||
this.set('loadingNotifications', true);
|
||||
}
|
||||
|
||||
stale.refresh().then((notifications) => {
|
||||
this.set('currentUser.unread_notifications', 0);
|
||||
if (this.cacheNotifications(notifications)) {
|
||||
this.setProperties({ notifications });
|
||||
}
|
||||
this.set('notifications', notifications);
|
||||
}).catch(() => {
|
||||
this.set('notifications', null);
|
||||
}).finally(() => {
|
||||
this.set("loadingNotifications", false);
|
||||
this.set('loadingNotifications', false);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/*eslint no-bitwise:0 */
|
||||
import { hashString } from 'discourse/lib/hash';
|
||||
|
||||
let _splitAvatars;
|
||||
|
||||
function defaultAvatar(username) {
|
||||
|
@ -7,11 +8,7 @@ function defaultAvatar(username) {
|
|||
_splitAvatars = _splitAvatars || defaultAvatars.split("\n");
|
||||
|
||||
if (_splitAvatars.length) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i<username.length; i++) {
|
||||
hash = ((hash<<5)-hash) + username.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
const hash = hashString(username);
|
||||
return _splitAvatars[Math.abs(hash) % _splitAvatars.length];
|
||||
}
|
||||
}
|
||||
|
|
11
app/assets/javascripts/discourse/lib/hash.js.es6
Normal file
11
app/assets/javascripts/discourse/lib/hash.js.es6
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*eslint no-bitwise:0 */
|
||||
|
||||
// Note: before changing this be aware the same algo is used server side for avatars.
|
||||
export function hashString(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i<str.length; i++) {
|
||||
hash = ((hash<<5)-hash) + str.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
12
app/assets/javascripts/discourse/lib/stale-result.js.es6
Normal file
12
app/assets/javascripts/discourse/lib/stale-result.js.es6
Normal file
|
@ -0,0 +1,12 @@
|
|||
const StaleResult = function() {
|
||||
this.hasResults = false;
|
||||
};
|
||||
|
||||
StaleResult.prototype.setResults = function(results) {
|
||||
if (results) {
|
||||
this.results = results;
|
||||
this.hasResults = true;
|
||||
}
|
||||
};
|
||||
|
||||
export default StaleResult;
|
|
@ -0,0 +1,32 @@
|
|||
import StaleResult from 'discourse/lib/stale-result';
|
||||
import { hashString } from 'discourse/lib/hash';
|
||||
|
||||
// Mix this in to an adapter to provide stale caching in localStorage
|
||||
export default {
|
||||
storageKey(type, findArgs) {
|
||||
const hashedArgs = Math.abs(hashString(JSON.stringify(findArgs)));
|
||||
return `${type}_${hashedArgs}`;
|
||||
},
|
||||
|
||||
findStale(store, type, findArgs) {
|
||||
const staleResult = new StaleResult();
|
||||
try {
|
||||
const stored = localStorage.getItem(this.storageKey(type, findArgs));
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
staleResult.setResults(parsed);
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// JSON parsing error
|
||||
}
|
||||
return staleResult;
|
||||
},
|
||||
|
||||
find(store, type, findArgs) {
|
||||
return this._super(store, type, findArgs).then((results) => {
|
||||
localStorage.setItem(this.storageKey(type, findArgs), JSON.stringify(results));
|
||||
return results;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -61,14 +61,28 @@ export default Ember.Object.extend({
|
|||
});
|
||||
},
|
||||
|
||||
_hydrateFindResults(result, type, findArgs) {
|
||||
if (typeof findArgs === "object") {
|
||||
return this._resultSet(type, result);
|
||||
} else {
|
||||
return this._hydrate(type, result[Ember.String.underscore(type)], result);
|
||||
}
|
||||
},
|
||||
|
||||
// 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);
|
||||
if (stale.hasResults) {
|
||||
stale.results = this._hydrateFindResults(stale.results, type, findArgs);
|
||||
}
|
||||
stale.refresh = () => this.find(type, findArgs);
|
||||
return stale;
|
||||
},
|
||||
|
||||
find(type, findArgs) {
|
||||
const self = this;
|
||||
return this.adapterFor(type).find(this, type, findArgs).then(function(result) {
|
||||
if (typeof findArgs === "object") {
|
||||
return self._resultSet(type, result);
|
||||
} else {
|
||||
return self._hydrate(type, result[Ember.String.underscore(type)], result);
|
||||
}
|
||||
return this.adapterFor(type).find(this, type, findArgs).then((result) => {
|
||||
return this._hydrateFindResults(result, type, findArgs);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
//= require ./ember-addons/decorator-alias
|
||||
//= require ./ember-addons/macro-alias
|
||||
//= require ./ember-addons/ember-computed-decorators
|
||||
//= require ./discourse/lib/hash
|
||||
//= require ./discourse/lib/stale-result
|
||||
//= require ./discourse/lib/load-script
|
||||
//= require ./discourse/lib/notification-levels
|
||||
//= require ./discourse/lib/app-events
|
||||
|
|
|
@ -63,6 +63,17 @@ test('find with query param', function() {
|
|||
});
|
||||
});
|
||||
|
||||
test('findStale with no stale results', (assert) => {
|
||||
const store = createStore();
|
||||
const stale = store.findStale('widget', {name: 'Trout Lure'});
|
||||
|
||||
assert.ok(!stale.hasResults, 'there are no stale results');
|
||||
assert.ok(!stale.results, 'results are present');
|
||||
return stale.refresh().then(function(w) {
|
||||
assert.equal(w.get('firstObject.id'), 123, 'a `refresh()` method provides results for stale');
|
||||
});
|
||||
});
|
||||
|
||||
test('update', function() {
|
||||
const store = createStore();
|
||||
return store.update('widget', 123, {name: 'hello'}).then(function(result) {
|
||||
|
@ -134,3 +145,4 @@ test('findAll embedded', function(assert) {
|
|||
assert.equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue