Upgrade Notifications to fix deprecations and use store

This commit is contained in:
Robin Ward 2015-05-05 13:44:19 -04:00
parent aab9706b7a
commit 0b65c88003
22 changed files with 133 additions and 323 deletions

View File

@ -1,22 +1,55 @@
const INVITED_TYPE = 8;
export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['notification.read', 'notification.is_warning'],
scope: function() {
return "notifications." + this.site.get("notificationLookup")[this.get("notification.notification_type")];
}.property("notification.notification_type"),
url: function() {
const it = this.get('notification');
const badgeId = it.get("data.badge_id");
if (badgeId) {
const badgeName = it.get("data.badge_name");
return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase());
}
const topicId = it.get('topic_id');
if (topicId) {
return Discourse.Utilities.postUrl(it.get("slug"), topicId, it.get("post_number"));
}
if (it.get('notification_type') === INVITED_TYPE) {
return Discourse.getURL('/my/invited');
}
}.property("notification.data.{badge_id,badge_name}", "model.slug", "model.topic_id", "model.post_number"),
description: function() {
const badgeName = this.get("notification.data.badge_name");
if (badgeName) { return Handlebars.Utils.escapeExpression(badgeName); }
const title = this.get('notification.data.topic_title');
return Ember.isEmpty(title) ? "" : Handlebars.Utils.escapeExpression(title);
}.property("notification.data.{badge_name,topic_title}"),
_markRead: function(){
var self = this;
this.$('a').click(function(){
self.set('notification.read', true);
this.$('a').click(() => {
this.set('notification.read', true);
return true;
});
}.on('didInsertElement'),
render: function(buffer) {
var notification = this.get('notification'),
text = I18n.t(this.get('scope'), Em.getProperties(notification, 'description', 'username'));
render(buffer) {
const notification = this.get('notification');
const description = this.get('description');
const username = notification.get('data.display_username');
const text = I18n.t(this.get('scope'), {description, username});
var url = notification.get('url');
const url = this.get('url');
if (url) {
buffer.push('<a href="' + notification.get('url') + '">' + text + '</a>');
buffer.push('<a href="' + url + '">' + text + '</a>');
} else {
buffer.push(text);
}

View File

@ -41,10 +41,11 @@ const HeaderController = DiscourseController.extend({
if (self.get("loadingNotifications")) { return; }
self.set("loadingNotifications", true);
Discourse.NotificationContainer.loadRecent().then(function(result) {
this.store.find('notification', {recent: true}).then(function(notifications) {
self.setProperties({
'currentUser.unread_notifications': 0,
notifications: result
notifications
});
}).catch(function() {
self.setProperties({

View File

@ -1,40 +0,0 @@
import ObjectController from 'discourse/controllers/object';
const INVITED_TYPE = 8;
export default ObjectController.extend({
notificationUrl: function(it) {
var badgeId = it.get("data.badge_id");
if (badgeId) {
var badgeName = it.get("data.badge_name");
return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase());
}
var topicId = it.get('topic_id');
if (topicId) {
return Discourse.Utilities.postUrl(it.get("slug"), topicId, it.get("post_number"));
}
if (it.get('notification_type') === INVITED_TYPE) {
return Discourse.getURL('/my/invited');
}
},
scope: function() {
return "notifications." + this.site.get("notificationLookup")[this.get("notification_type")];
}.property("notification_type"),
username: Em.computed.alias("data.display_username"),
url: function() {
return this.notificationUrl(this);
}.property("data.{badge_id,badge_name}", "slug", "topic_id", "post_number"),
description: function() {
const badgeName = this.get("data.badge_name");
if (badgeName) { return Handlebars.Utils.escapeExpression(badgeName); }
return this.blank("data.topic_title") ? "" : Handlebars.Utils.escapeExpression(this.get("data.topic_title"));
}.property("data.{badge_name,topic_title}")
});

View File

@ -1,43 +1,21 @@
export default Ember.ArrayController.extend({
needs: ['user-notifications', 'application'],
loading: false,
needs: ['application'],
_showFooter: function() {
this.set("controllers.application.showFooter", !this.get("canLoadMore"));
}.observes("canLoadMore"),
this.set("controllers.application.showFooter", !this.get("model.canLoadMore"));
}.observes("model.canLoadMore"),
showDismissButton: function() {
return this.get('user').total_unread_notifications > 0;
}.property('user'),
showDismissButton: Ember.computed.gt('user.total_unread_notifications', 0),
actions: {
resetNew: function() {
var self = this;
Discourse.NotificationContainer.resetNew().then(function() {
self.get('controllers.user-notifications').setEach('read', true);
Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => {
this.setEach('read', true);
});
},
loadMore: function() {
if (this.get('canLoadMore') && !this.get('loading')) {
this.set('loading', true);
var self = this;
Discourse.NotificationContainer.loadHistory(
self.get('model.lastObject.created_at'),
self.get('user.username')).then(function(result) {
self.set('loading', false);
var notifications = result.get('content');
self.pushObjects(notifications);
// Stop trying if it's the end
if (notifications && (notifications.length === 0 || notifications.length < 60)) {
self.set('canLoadMore', false);
}
}).catch(function(error) {
self.set('loading', false);
Em.Logger.error(error);
});
}
this.get('model').loadMore();
}
}
});

View File

@ -38,13 +38,13 @@ export default {
app.register('session:main', Session.current(), { instantiate: false });
injectAll(app, 'session');
app.register('store:main', Store);
inject(app, 'store', 'route', 'controller');
app.register('current-user:main', Discourse.User.current(), { instantiate: false });
inject(app, 'currentUser', 'component', 'route', 'controller');
app.register('message-bus:main', window.MessageBus, { instantiate: false });
inject(app, 'messageBus', 'route', 'controller', 'view');
app.register('store:main', Store);
inject(app, 'store', 'route', 'controller');
}
};

View File

@ -1,22 +1,15 @@
/**
Provides the ability to load more items for a view which is scrolled to the bottom.
**/
// Provides the ability to load more items for a view which is scrolled to the bottom.
export default Em.Mixin.create(Ember.ViewTargetActionSupport, Discourse.Scrolling, {
scrolled: function() {
var eyeline = this.get('eyeline');
const eyeline = this.get('eyeline');
if (eyeline) { eyeline.update(); }
},
_bindEyeline: function() {
var eyeline = new Discourse.Eyeline(this.get('eyelineSelector') + ":last");
const eyeline = new Discourse.Eyeline(this.get('eyelineSelector') + ":last");
this.set('eyeline', eyeline);
var self = this;
eyeline.on('sawBottom', function() {
self.send('loadMore');
});
eyeline.on('sawBottom', () => this.send('loadMore'));
this.bindScrolling();
}.on('didInsertElement'),

View File

@ -1,54 +0,0 @@
Discourse.NotificationContainer = Ember.ArrayProxy.extend({
});
Discourse.NotificationContainer.reopenClass({
createFromJson: function(json_array) {
return Discourse.NotificationContainer.create({content: json_array});
},
createFromError: function(error) {
return Discourse.NotificationContainer.create({
content: [],
error: true,
forbidden: error.status === 403
});
},
loadRecent: function() {
// TODO - add .json (breaks tests atm)
return Discourse.ajax('/notifications').then(function(result) {
return Discourse.NotificationContainer.createFromJson(result);
}).catch(function(error) {
// TODO HeaderController can't handle a createFromError
// just throw for now
throw error;
});
},
loadHistory: function(beforeDate, username) {
var url = '/notifications/history.json',
params = [
beforeDate ? ('before=' + beforeDate) : null,
username ? ('user=' + username) : null
];
// Remove nulls
params = params.filter(function(param) { return !!param; });
// Build URL
params.forEach(function(param, idx) {
url = url + (idx === 0 ? '?' : '&') + param;
});
return Discourse.ajax(url).then(function(result) {
return Discourse.NotificationContainer.createFromJson(result);
}).catch(function(error) {
return Discourse.NotificationContainer.createFromError(error);
});
},
resetNew: function() {
return Discourse.ajax("/notifications/reset-new", {type: 'PUT'});
}
});

View File

@ -4,6 +4,10 @@ export default Ember.ArrayProxy.extend({
totalRows: 0,
refreshing: false,
canLoadMore: function() {
return this.get('length') < this.get('totalRows');
}.property('totalRows', 'length'),
loadMore() {
const loadMoreUrl = this.get('loadMoreUrl');
if (!loadMoreUrl) { return; }

View File

@ -434,16 +434,13 @@ User.reopenClass(Discourse.Singleton, {
return user.findDetails(options);
},
/**
The current singleton will retrieve its attributes from the `PreloadStore`
if it exists. Otherwise, no instance is created.
@method createCurrent
@returns {Discourse.User} the user, if logged in.
**/
// TODO: Use app.register and junk Discourse.Singleton
createCurrent: function() {
var userJson = PreloadStore.get('currentUser');
if (userJson) { return Discourse.User.create(userJson); }
if (userJson) {
const store = Discourse.__container__.lookup('store:main');
return store.createRecord('user', userJson);
}
return null;
},

View File

@ -1,31 +1,22 @@
import ShowFooter from "discourse/mixins/show-footer";
import ViewingActionType from "discourse/mixins/viewing-action-type";
export default Discourse.Route.extend(ShowFooter, {
export default Discourse.Route.extend(ShowFooter, ViewingActionType, {
actions: {
didTransition: function() {
this.controllerFor("user_notifications")._showFooter();
didTransition() {
this.controllerFor("user-notifications")._showFooter();
return true;
}
},
model: function() {
model() {
var user = this.modelFor('user');
return Discourse.NotificationContainer.loadHistory(undefined, user.get('username'));
return this.store.find('notification', {username: user.get('username')});
},
setupController: function(controller, model) {
setupController(controller, model) {
controller.set('model', model);
controller.set('user', this.modelFor('user'));
if (this.controllerFor('user_activity').get('content')) {
this.controllerFor('user_activity').set('userActionType', -1);
}
// properly initialize "canLoadMore"
controller.set("canLoadMore", model.get("length") === 60);
},
renderTemplate: function() {
this.render('user-notification-history', {into: 'user'});
this.viewingActionType(-1);
}
});

View File

@ -2,8 +2,8 @@
{{#conditional-loading-spinner condition=loadingNotifications}}
{{#if content}}
<ul>
{{#each n in model itemController="notification"}}
{{notification-item notification=n scope=n.scope}}
{{#each n in model}}
{{notification-item notification=n}}
{{/each}}
<li class="read last">
<a href="{{unbound myNotificationsUrl}}">{{i18n 'notifications.more'}}&hellip;</a>

View File

@ -14,9 +14,9 @@
</div>
{{/if}}
{{#each n in model itemController="notification"}}
{{#each n in model}}
<div {{bind-attr class=":item :notification n.read::unread"}}>
{{notification-item notification=n scope=n.scope}}
{{notification-item notification=n}}
<span class="time">
{{format-date n.created_at leaveAgo="true"}}
</span>
@ -24,7 +24,7 @@
{{/each}}
{{#conditional-loading-spinner condition=loading}}
{{#unless canLoadMore}}
{{#unless model.canLoadMore}}
{{#if showDismissButton}}
<div class='notification-buttons'>
<button title="{{i18n 'user.dismiss_notifications_tooltip'}}" id='dismiss-notifications' class='btn notifications-read' {{action "resetNew"}}>{{i18n 'user.dismiss_notifications'}}</button>

View File

@ -2,6 +2,5 @@ import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
eyelineSelector: '.user-stream .notification',
classNames: ['user-stream', 'notification-history'],
templateName: 'user/notifications'
classNames: ['user-stream', 'notification-history']
});

View File

@ -1,56 +1,51 @@
require_dependency 'notification_serializer'
class NotificationsController < ApplicationController
before_filter :ensure_logged_in
def recent
notifications = Notification.recent_report(current_user, 10)
def index
user = current_user
if params[:recent].present?
notifications = Notification.recent_report(current_user, 10)
if notifications.present?
# ordering can be off due to PMs
max_id = notifications.map(&:id).max
current_user.saw_notification_id(max_id) unless params.has_key?(:silent)
if notifications.present?
# ordering can be off due to PMs
max_id = notifications.map(&:id).max
current_user.saw_notification_id(max_id) unless params.has_key?(:silent)
end
current_user.reload
current_user.publish_notifications_state
render_serialized(notifications, NotificationSerializer, root: :notifications)
else
offset = params[:offset].to_i
user = User.find_by_username(params[:username].to_s) if params[:username]
guardian.ensure_can_see_notifications!(user)
notifications = Notification.where(user_id: user.id)
.visible
.includes(:topic)
.order(created_at: :desc)
total_rows = notifications.dup.count
notifications = notifications.offset(offset).limit(60)
render_json_dump(notifications: serialize_data(notifications, NotificationSerializer),
total_rows_notifications: total_rows,
load_more_notifications: notifications_path(username: user.username, offset: offset + 60))
end
current_user.reload
current_user.publish_notifications_state
render_serialized(notifications, NotificationSerializer)
end
def history
params.permit(:before, :user)
params[:before] ||= 1.day.from_now
user = current_user
user = User.find_by_username(params[:user].to_s) if params[:user]
unless guardian.can_see_notifications?(user)
return render json: {errors: [I18n.t('js.errors.reasons.forbidden')]}, status: 403
end
notifications = Notification.where(user_id: user.id)
.visible
.includes(:topic)
.limit(60)
.where('created_at < ?', params[:before])
.order(created_at: :desc)
render_serialized(notifications, NotificationSerializer)
end
def reset_new
params.permit(:user)
user = current_user
if params[:user]
user = User.find_by_username(params[:user].to_s)
end
Notification.where(user_id: user.id).includes(:topic).where(read: false).update_all(read: true)
def mark_read
Notification.where(user_id: current_user.id).includes(:topic).where(read: false).update_all(read: true)
current_user.saw_notification_id(Notification.recent_report(current_user, 1).max)
current_user.reload
current_user.publish_notifications_state
render nothing: true
render json: success_json
end
end

View File

@ -1,6 +1,7 @@
class NotificationSerializer < ApplicationSerializer
attributes :notification_type,
attributes :id,
:notification_type,
:read,
:created_at,
:post_number,

View File

@ -329,9 +329,8 @@ Discourse::Application.routes.draw do
end
end
get "notifications" => "notifications#recent"
get "notifications/history" => "notifications#history"
put "notifications/reset-new" => 'notifications#reset_new'
get 'notifications' => 'notifications#index'
put 'notifications/mark-read' => 'notifications#mark_read'
match "/auth/:provider/callback", to: "users/omniauth_callbacks#complete", via: [:get, :post]
match "/auth/failure", to: "users/omniauth_callbacks#failure", via: [:get, :post]

View File

@ -6,17 +6,17 @@ describe NotificationsController do
let!(:user) { log_in }
it 'should succeed for recent' do
xhr :get, :recent
xhr :get, :index, recent: true
expect(response).to be_success
end
it 'should succeed for history' do
xhr :get, :history
xhr :get, :index
expect(response).to be_success
end
it 'should succeed for history' do
xhr :get, :reset_new
it 'should succeed' do
xhr :put, :mark_read
expect(response).to be_success
end
@ -24,7 +24,7 @@ describe NotificationsController do
notification = Fabricate(:notification, user: user)
expect(user.reload.unread_notifications).to eq(1)
expect(user.reload.total_unread_notifications).to eq(1)
xhr :get, :recent
xhr :get, :index, recent: true
expect(user.reload.unread_notifications).to eq(0)
expect(user.reload.total_unread_notifications).to eq(1)
end
@ -33,7 +33,7 @@ describe NotificationsController do
notification = Fabricate(:notification, user: user)
expect(user.reload.unread_notifications).to eq(1)
expect(user.reload.total_unread_notifications).to eq(1)
xhr :get, :recent, silent: true
xhr :get, :index, recent: true, silent: true
expect(user.reload.unread_notifications).to eq(1)
expect(user.reload.total_unread_notifications).to eq(1)
end
@ -42,7 +42,7 @@ describe NotificationsController do
notification = Fabricate(:notification, user: user)
expect(user.reload.unread_notifications).to eq(1)
expect(user.reload.total_unread_notifications).to eq(1)
xhr :put, :reset_new
xhr :put, :mark_read
user.reload
expect(user.reload.unread_notifications).to eq(0)
expect(user.reload.total_unread_notifications).to eq(0)
@ -51,7 +51,7 @@ describe NotificationsController do
context 'when not logged in' do
it 'should raise an error' do
expect { xhr :get, :recent }.to raise_error(Discourse::NotLoggedIn)
expect { xhr :get, :index, recent: true }.to raise_error(Discourse::NotLoggedIn)
end
end

View File

@ -1,33 +0,0 @@
moduleFor("controller:header", "controller:header", {
needs: ['controller:application']
});
test("showNotifications action", function() {
let resolveRequestWith;
const request = new Ember.RSVP.Promise(function(resolve) {
resolveRequestWith = resolve;
});
const currentUser = Discourse.User.create({ unread_notifications: 1});
const controller = this.subject({ currentUser: currentUser });
const viewSpy = { showDropdownBySelector: sinon.spy() };
sandbox.stub(Discourse, "ajax").withArgs("/notifications").returns(request);
Ember.run(function() {
controller.send("showNotifications", viewSpy);
});
equal(controller.get("notifications"), null, "notifications are null before data has finished loading");
equal(currentUser.get("unread_notifications"), 1, "current user's unread notifications count is not zeroed before data has finished loading");
ok(viewSpy.showDropdownBySelector.calledWith("#user-notifications"), "dropdown with loading glyph is shown before data has finished loading");
Ember.run(function() {
resolveRequestWith(["notification"]);
});
// Can't use deepEquals because controller.get("notifications") is an ArrayProxy, not an Array
ok(controller.get("notifications").indexOf("notification") !== -1, "notification is in the controller");
equal(currentUser.get("unread_notifications"), 0, "current user's unread notifications count is zeroed after data has finished loading");
ok(viewSpy.showDropdownBySelector.calledWith("#user-notifications"), "dropdown with notifications is shown after data has finished loading");
});

View File

@ -1,56 +0,0 @@
import Site from 'discourse/models/site';
function buildFixture() {
return {
notification_type: 1, //mentioned
post_number: 1,
topic_id: 1234,
slug: "a-slug",
data: {
topic_title: "some title",
display_username: "velesin"
},
site: Site.current()
};
}
moduleFor("controller:notification");
test("scope property is correct", function() {
const controller = this.subject(buildFixture());
equal(controller.get("scope"), "notifications.mentioned");
});
test("username property is correct", function() {
const controller = this.subject(buildFixture());
equal(controller.get("username"), "velesin");
});
test("description property returns badge name when there is one", function() {
const fixtureWithBadgeName = _.extend({}, buildFixture(), { data: { badge_name: "badge" } });
const controller = this.subject(fixtureWithBadgeName);
equal(controller.get("description"), "badge");
});
test("description property returns empty string when there is no topic title", function() {
const fixtureWithEmptyTopicTitle = _.extend({}, buildFixture(), { data: { topic_title: "" } });
const controller = this.subject(fixtureWithEmptyTopicTitle);
equal(controller.get("description"), "");
});
test("description property returns topic title", function() {
const fixtureWithTopicTitle = _.extend({}, buildFixture(), { data: { topic_title: "topic" } });
const controller = this.subject(fixtureWithTopicTitle);
equal(controller.get("description"), "topic");
});
test("url property returns badge's url when there is a badge", function() {
const fixtureWithBadge = _.extend({}, buildFixture(), { data: { badge_id: 1, badge_name: "Badge Name"} });
const controller = this.subject(fixtureWithBadge);
equal(controller.get("url"), "/badges/1/badge-name");
});
test("url property returns topic's url when there is a topic", function() {
const controller = this.subject(buildFixture());
equal(controller.get("url"), "/t/a-slug/1234");
});

View File

@ -1,2 +1,2 @@
/*jshint maxlen:10000000 */
export default {"/notifications": [ { notification_type: 2, read: true, post_number: 2, topic_id: 1234, slug: "a-slug", data: { topic_title: "some title", display_username: "velesin" } } ] };
export default {"/notifications": {notifications: [ { id: 123, notification_type: 2, read: true, post_number: 2, topic_id: 1234, slug: "a-slug", data: { topic_title: "some title", display_username: "velesin" } } ] }};

View File

@ -36,7 +36,7 @@ test('updating simultaneously', function() {
expect(2);
const store = createStore();
store.find('widget', 123).then(function(widget) {
return store.find('widget', 123).then(function(widget) {
const firstPromise = widget.update({ name: 'new name' });
const secondPromise = widget.update({ name: 'new name' });
@ -90,7 +90,7 @@ test('creating simultaneously', function() {
test('destroyRecord', function() {
const store = createStore();
store.find('widget', 123).then(function(widget) {
return store.find('widget', 123).then(function(widget) {
widget.destroyRecord().then(function(result) {
ok(result);
});

View File

@ -20,6 +20,7 @@ test('pagination support', function() {
equal(rs.get('totalRows'), 4);
ok(rs.get('loadMoreUrl'), 'has a url to load more');
ok(!rs.get('loadingMore'), 'it is not loading more');
ok(rs.get('canLoadMore'));
const promise = rs.loadMore();
@ -28,6 +29,7 @@ test('pagination support', function() {
ok(!rs.get('loadingMore'), 'it finished loading more');
equal(rs.get('length'), 4);
ok(!rs.get('loadMoreUrl'));
ok(!rs.get('canLoadMore'));
});
});
});