diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6
index d7cebb23f01..846b1441f7e 100644
--- a/app/assets/javascripts/discourse/controllers/header.js.es6
+++ b/app/assets/javascripts/discourse/controllers/header.js.es6
@@ -45,12 +45,16 @@ export default DiscourseController.extend({
if (self.get("loadingNotifications")) { return; }
self.set("loadingNotifications", true);
- Discourse.ajax("/notifications").then(function(result) {
+ Discourse.NotificationContainer.loadRecent().then(function(result) {
self.setProperties({
'currentUser.unread_notifications': 0,
notifications: result
});
- }).finally(function(){
+ }).catch(function() {
+ self.setProperties({
+ notifications: null
+ });
+ }).finally(function() {
self.set("loadingNotifications", false);
});
},
diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6
new file mode 100644
index 00000000000..6bb5aee5201
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6
@@ -0,0 +1,27 @@
+
+export default Ember.ArrayController.extend({
+ canLoadMore: true,
+ loading: false,
+
+ actions: {
+ 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);
+ self.pushObjects(result);
+ // Stop trying if it's the end
+ if (result.length === 0) {
+ self.set('canLoadMore', false);
+ }
+ }).catch(function(error) {
+ self.set('loading', false);
+ Em.Logger.error(error);
+ });
+ }
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6
index 2db276896eb..651347c2f8b 100644
--- a/app/assets/javascripts/discourse/controllers/user.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user.js.es6
@@ -20,6 +20,8 @@ export default ObjectController.extend({
return this.get('viewingSelf') || Discourse.User.currentProp('admin');
}.property('viewingSelf'),
+ canSeeNotificationHistory: Em.computed.alias('canSeePrivateMessages'),
+
showBadges: function() {
return Discourse.SiteSettings.enable_badges && (this.get('content.badge_count') > 0);
}.property('content.badge_count'),
diff --git a/app/assets/javascripts/discourse/models/notification.js b/app/assets/javascripts/discourse/models/notification.js
new file mode 100644
index 00000000000..90bc5fe4f1c
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/notification.js
@@ -0,0 +1,48 @@
+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() {
+ return Discourse.ajax('/notifications').then(function(result) {
+ return Discourse.NotificationContainer.createFromJson(result);
+ }).catch(function(error) {
+ // HeaderController can't handle it properly
+ 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);
+ });
+ }
+});
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
index e2539177499..2a0cd141c54 100644
--- a/app/assets/javascripts/discourse/routes/application_routes.js
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -69,6 +69,7 @@ Discourse.Route.buildRoutes(function() {
});
this.route('badges');
+ this.route('notifications');
this.route('flaggedPosts', { path: '/flagged-posts' });
this.route('deletedPosts', { path: '/deleted-posts' });
diff --git a/app/assets/javascripts/discourse/routes/user-notifications.js.es6 b/app/assets/javascripts/discourse/routes/user-notifications.js.es6
new file mode 100644
index 00000000000..91d273a04da
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user-notifications.js.es6
@@ -0,0 +1,19 @@
+export default Discourse.Route.extend({
+ model: function() {
+ var user = this.modelFor('user');
+ return Discourse.NotificationContainer.loadHistory(undefined, user.get('username'));
+ },
+
+ setupController: function(controller, model) {
+ this.controllerFor('user').set('indexStream', false);
+ if (this.controllerFor('user_activity').get('content')) {
+ this.controllerFor('user_activity').set('userActionType', -1);
+ }
+ controller.set('model', model);
+ controller.set('user', this.modelFor('user'));
+ },
+
+ renderTemplate: function() {
+ this.render('user-notification-history', {into: 'user', outlet: 'userOutlet'});
+ }
+});
diff --git a/app/assets/javascripts/discourse/templates/notifications.js.handlebars b/app/assets/javascripts/discourse/templates/notifications.js.handlebars
index e2220200519..b7117b7af60 100644
--- a/app/assets/javascripts/discourse/templates/notifications.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/notifications.js.handlebars
@@ -6,11 +6,15 @@
{{notification-item notification=this scope=scope}}
{{/each}}
- {{i18n notifications.more}} …
+ {{i18n notifications.more}}…
{{else}}
- {{i18n notifications.none}}
+ {{#if error}}
+ {{i18n notifications.none}}
+ {{else}}
+ {{i18n notifications.none}}
+ {{/if}}
{{/if}}
{{else}}
diff --git a/app/assets/javascripts/discourse/templates/user/notifications.js.handlebars b/app/assets/javascripts/discourse/templates/user/notifications.js.handlebars
new file mode 100644
index 00000000000..9c66d9f7a06
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/user/notifications.js.handlebars
@@ -0,0 +1,24 @@
+{{#if model.error}}
+
+ {{#if model.forbidden}}
+ {{i18n errors.reasons.forbidden}}
+ {{else}}
+ {{i18n errors.desc.unknown}}
+ {{/if}}
+
+{{/if}}
+
+{{#each itemController="notification"}}
+
+ {{notification-item notification=this scope=scope}}
+
+ {{date path="created_at" leaveAgo="true"}}
+
+
+{{/each}}
+{{#if loading}}
+ {{i18n loading}}
+{{/if}}
+{{#unless canLoadMore}}
+
+{{/unless}}
diff --git a/app/assets/javascripts/discourse/templates/user/user.js.handlebars b/app/assets/javascripts/discourse/templates/user/user.js.handlebars
index 509424f6cdf..9532d1f027f 100644
--- a/app/assets/javascripts/discourse/templates/user/user.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/user/user.js.handlebars
@@ -23,6 +23,16 @@
{{/link-to}}
{{/link-to}}
{{/if}}
+ {{#if canSeeNotificationHistory}}
+ {{#link-to 'user.notifications' tagName="li"}}
+ {{#link-to 'user.notifications'}}
+
+ {{i18n user.notification_history}}
+ ({{unread_notification_count}})
+
+ {{/link-to}}
+ {{/link-to}}
+ {{/if}}
{{#if canSeePrivateMessages}}
diff --git a/app/assets/javascripts/discourse/views/user-notification-history.js.es6 b/app/assets/javascripts/discourse/views/user-notification-history.js.es6
new file mode 100644
index 00000000000..a31bea4931d
--- /dev/null
+++ b/app/assets/javascripts/discourse/views/user-notification-history.js.es6
@@ -0,0 +1,5 @@
+export default Ember.View.extend(Discourse.LoadMore, {
+ eyelineSelector: '.user-stream .notification',
+ classNames: ['user-stream', 'notification-history'],
+ templateName: 'user/notifications'
+});
diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss
index 4d09ac6180f..e2b9a56e3e6 100644
--- a/app/assets/stylesheets/common/base/user.scss
+++ b/app/assets/stylesheets/common/base/user.scss
@@ -15,6 +15,11 @@
}
}
+.end-of-stream {
+ border: 3px solid $primary;
+ width: 100%;
+}
+
.user-navigation {
.map {
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index fe6f7144399..caefbf72f53 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -367,6 +367,30 @@
float: right;
margin-top: -4px;
}
+ .notification {
+ &.unread {
+ background-color: dark-light-diff($tertiary, $secondary, 90%, -60%);
+ }
+
+ li { display: inline-block; }
+ p {
+ display: inline-block;
+ margin-left: 10px;
+ span {
+ color: $primary;
+ }
+ }
+ .time {
+ display: inline-block;
+ margin-left: 10px;
+ float: none;
+ }
+ // common/base/header.scss
+ .fa, .icon {
+ color: scale-color($primary, $lightness: 50%);
+ font-size: 24px;
+ }
+ }
}
.staff-counters {
diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss
index 77de38d4f58..cabbc38fed4 100644
--- a/app/assets/stylesheets/mobile/user.scss
+++ b/app/assets/stylesheets/mobile/user.scss
@@ -284,6 +284,26 @@
float: right !important;
margin-top: -8px;
}
+ .notification {
+ padding: 0 8px;
+ li { display: inline-block; }
+ p {
+ display: inline-block;
+ margin: 7px;
+ span {
+ color: $primary;
+ }
+ }
+ .time {
+ display: inline-block;
+ margin: 0;
+ float: none;
+ }
+ // common/base/header.scss
+ .fa, .icon {
+ color: scale-color($primary, $lightness: 50%);
+ }
+ }
}
.staff-counters {
diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb
index 46c4374c117..40c0b6227e6 100644
--- a/app/controllers/notifications_controller.rb
+++ b/app/controllers/notifications_controller.rb
@@ -2,7 +2,7 @@ class NotificationsController < ApplicationController
before_filter :ensure_logged_in
- def index
+ def recent
notifications = Notification.recent_report(current_user, 10)
if notifications.present?
@@ -16,4 +16,25 @@ class NotificationsController < ApplicationController
render_serialized(notifications, NotificationSerializer)
end
+ def history
+ params.permit(:before, :user)
+ params[:before] ||= 1.day.from_now
+
+ user = current_user
+ if params[:user]
+ user = User.find_by_username(params[:user].to_s)
+ end
+
+ 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)
+ .includes(:topic)
+ .limit(60)
+ .where('created_at < ?', params[:before])
+ .order(created_at: :desc)
+
+ render_serialized(notifications, NotificationSerializer)
+ end
end
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 1edb4034d2f..327c7952614 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -43,6 +43,7 @@ class UserSerializer < BasicUserSerializer
:suspended_till,
:uploaded_avatar_id,
:badge_count,
+ :unread_notification_count,
:has_title_badges,
:edit_history_public,
:custom_fields
@@ -76,6 +77,7 @@ class UserSerializer < BasicUserSerializer
:tracked_category_ids,
:watched_category_ids,
:private_messages_stats,
+ :unread_notification_count,
:disable_jump_reply,
:gravatar_avatar_upload_id,
:custom_avatar_upload_id,
@@ -242,6 +244,10 @@ class UserSerializer < BasicUserSerializer
object.badges.where(allow_title: true).count > 0
end
+ def unread_notification_count
+ Notification.where(user_id: object.id, read: false).count
+ end
+
def include_edit_history_public?
can_edit && !SiteSetting.edit_history_visible_to_public
end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 49355a5c016..5b18c2fa076 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -306,6 +306,7 @@ en:
invited_by: "Invited By"
trust_level: "Trust Level"
notifications: "Notifications"
+ notification_history: "Notification History"
disable_jump_reply: "Don't jump to your new post after replying"
dynamic_favicon: "Show incoming message notifications on favicon (experimental)"
edit_history_public: "Let other users view my post revisions"
@@ -520,6 +521,7 @@ en:
network: "Please check your connection."
network_fixed: "Looks like it's back."
server: "Error code: {{status}}"
+ forbidden: "You're not allowed to view that."
unknown: "Something went wrong."
buttons:
back: "Go Back"
diff --git a/config/routes.rb b/config/routes.rb
index 786b510fc20..654839be257 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -232,6 +232,7 @@ Discourse::Application.routes.draw do
get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
+ get "users/:username/notifications" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
delete "users/:username" => "users#destroy", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/by-external/:external_id" => "users#show"
get "users/:username/flagged-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
@@ -270,7 +271,8 @@ Discourse::Application.routes.draw do
end
end
- resources :notifications
+ get "notifications" => "notifications#recent"
+ get "notifications/history" => "notifications#history"
match "/auth/:provider/callback", to: "users/omniauth_callbacks#complete", via: [:get, :post]
match "/auth/failure", to: "users/omniauth_callbacks#failure", via: [:get, :post]
diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb
index 2b624c0012b..af6596b9bb8 100644
--- a/lib/guardian/user_guardian.rb
+++ b/lib/guardian/user_guardian.rb
@@ -26,6 +26,10 @@ module UserGuardian
can_edit?(user)
end
+ def can_see_notifications?(user)
+ is_me?(user) || is_admin?
+ end
+
def can_block_user?(user)
user && is_staff? && not(user.staff?)
end
diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb
index 1259bda2270..71326786866 100644
--- a/spec/controllers/notifications_controller_spec.rb
+++ b/spec/controllers/notifications_controller_spec.rb
@@ -5,29 +5,34 @@ describe NotificationsController do
context 'when logged in' do
let!(:user) { log_in }
- it 'should succeed' do
- xhr :get, :index
+ it 'should succeed for recent' do
+ xhr :get, :recent
+ response.should be_success
+ end
+
+ it 'should succeed for history' do
+ xhr :get, :history
response.should be_success
end
it 'should mark notifications as viewed' do
notification = Fabricate(:notification, user: user)
user.reload.unread_notifications.should == 1
- xhr :get, :index
+ xhr :get, :recent
user.reload.unread_notifications.should == 0
end
it 'should not mark notifications as viewed if silent param is present' do
notification = Fabricate(:notification, user: user)
user.reload.unread_notifications.should == 1
- xhr :get, :index, silent: true
+ xhr :get, :recent, silent: true
user.reload.unread_notifications.should == 1
end
end
context 'when not logged in' do
it 'should raise an error' do
- lambda { xhr :get, :index }.should raise_error(Discourse::NotLoggedIn)
+ lambda { xhr :get, :recent }.should raise_error(Discourse::NotLoggedIn)
end
end
diff --git a/test/javascripts/controllers/header-test.js.es6 b/test/javascripts/controllers/header-test.js.es6
index aabbd9ad5ed..d5bdbe501f5 100644
--- a/test/javascripts/controllers/header-test.js.es6
+++ b/test/javascripts/controllers/header-test.js.es6
@@ -29,7 +29,8 @@ test("showNotifications action", function() {
resolveRequestWith(["notification"]);
});
- deepEqual(controller.get("notifications"), ["notification"], "notifications are set correctly after data has finished loading");
+ // 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(Discourse.User.current().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");
});