mirror of
https://github.com/discourse/discourse.git
synced 2025-06-28 12:41:40 +08: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:
@ -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'];
|
const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host'];
|
||||||
|
|
||||||
export function Result(payload, responseJson) {
|
export function Result(payload, responseJson) {
|
||||||
@ -53,6 +54,10 @@ export default Ember.Object.extend({
|
|||||||
return ajax(this.pathFor(store, type, findArgs)).catch(rethrow);
|
return ajax(this.pathFor(store, type, findArgs)).catch(rethrow);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
findStale() {
|
||||||
|
return new StaleResult();
|
||||||
|
},
|
||||||
|
|
||||||
update(store, type, id, attrs) {
|
update(store, type, id, attrs) {
|
||||||
const data = {};
|
const data = {};
|
||||||
const typeField = Ember.String.underscore(type);
|
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() {
|
refreshNotifications() {
|
||||||
|
|
||||||
if (this.get('loadingNotifications')) { return; }
|
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
|
// 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');
|
||||||
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);
|
this.set('currentUser.unread_notifications', 0);
|
||||||
if (this.cacheNotifications(notifications)) {
|
this.set('notifications', notifications);
|
||||||
this.setProperties({ notifications });
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.set('notifications', null);
|
this.set('notifications', null);
|
||||||
}).finally(() => {
|
}).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;
|
let _splitAvatars;
|
||||||
|
|
||||||
function defaultAvatar(username) {
|
function defaultAvatar(username) {
|
||||||
@ -7,11 +8,7 @@ function defaultAvatar(username) {
|
|||||||
_splitAvatars = _splitAvatars || defaultAvatars.split("\n");
|
_splitAvatars = _splitAvatars || defaultAvatars.split("\n");
|
||||||
|
|
||||||
if (_splitAvatars.length) {
|
if (_splitAvatars.length) {
|
||||||
let hash = 0;
|
const hash = hashString(username);
|
||||||
for (let i = 0; i<username.length; i++) {
|
|
||||||
hash = ((hash<<5)-hash) + username.charCodeAt(i);
|
|
||||||
hash |= 0;
|
|
||||||
}
|
|
||||||
return _splitAvatars[Math.abs(hash) % _splitAvatars.length];
|
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) {
|
find(type, findArgs) {
|
||||||
const self = this;
|
return this.adapterFor(type).find(this, type, findArgs).then((result) => {
|
||||||
return this.adapterFor(type).find(this, type, findArgs).then(function(result) {
|
return this._hydrateFindResults(result, type, findArgs);
|
||||||
if (typeof findArgs === "object") {
|
|
||||||
return self._resultSet(type, result);
|
|
||||||
} else {
|
|
||||||
return self._hydrate(type, result[Ember.String.underscore(type)], result);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
//= require ./ember-addons/decorator-alias
|
//= require ./ember-addons/decorator-alias
|
||||||
//= require ./ember-addons/macro-alias
|
//= require ./ember-addons/macro-alias
|
||||||
//= require ./ember-addons/ember-computed-decorators
|
//= require ./ember-addons/ember-computed-decorators
|
||||||
|
//= require ./discourse/lib/hash
|
||||||
|
//= require ./discourse/lib/stale-result
|
||||||
//= require ./discourse/lib/load-script
|
//= require ./discourse/lib/load-script
|
||||||
//= require ./discourse/lib/notification-levels
|
//= require ./discourse/lib/notification-levels
|
||||||
//= require ./discourse/lib/app-events
|
//= 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() {
|
test('update', function() {
|
||||||
const store = createStore();
|
const store = createStore();
|
||||||
return store.update('widget', 123, {name: 'hello'}).then(function(result) {
|
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');
|
assert.equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user