FEATURE: Actually show more notifications
The "Show more notifications..." link in the notifications dropdown now links to /my/notifications, which is a historical view of all notifications you have recieved. Notification history is loaded in blocks of 60 at a time. Admins can see others' notification history. (This was requested for 'debugging purposes', though that's what impersonation is for, IMO.)
This commit is contained in:
parent
a5e98c9906
commit
69bc552054
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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' });
|
||||
|
||||
|
|
|
@ -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'});
|
||||
}
|
||||
});
|
|
@ -6,11 +6,15 @@
|
|||
{{notification-item notification=this scope=scope}}
|
||||
{{/each}}
|
||||
<li class="read last">
|
||||
<a {{bind-attr href="currentUser.path"}}>{{i18n notifications.more}} …</a>
|
||||
<a href="/my/notifications">{{i18n notifications.more}}…</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="none">{{i18n notifications.none}}</div>
|
||||
{{#if error}}
|
||||
<div class="none error">{{i18n notifications.none}}</div>
|
||||
{{else}}
|
||||
<div class="none">{{i18n notifications.none}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class='loading'><i class='fa fa-spinner fa-spin'></i></div>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{{#if model.error}}
|
||||
<div class="item error">
|
||||
{{#if model.forbidden}}
|
||||
{{i18n errors.reasons.forbidden}}
|
||||
{{else}}
|
||||
{{i18n errors.desc.unknown}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#each itemController="notification"}}
|
||||
<div {{bind-attr class=":item :notification read::unread"}}>
|
||||
{{notification-item notification=this scope=scope}}
|
||||
<span class="time">
|
||||
{{date path="created_at" leaveAgo="true"}}
|
||||
</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#if loading}}
|
||||
<div class='spinner'>{{i18n loading}}</div>
|
||||
{{/if}}
|
||||
{{#unless canLoadMore}}
|
||||
<div class='end-of-stream'></div>
|
||||
{{/unless}}
|
|
@ -23,6 +23,16 @@
|
|||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
{{#if canSeeNotificationHistory}}
|
||||
{{#link-to 'user.notifications' tagName="li"}}
|
||||
{{#link-to 'user.notifications'}}
|
||||
<i class='glyph fa fa-comment'></i>
|
||||
{{i18n user.notification_history}}
|
||||
<span class='count'>({{unread_notification_count}})</span>
|
||||
<span class='fa fa-chevron-right'></span>
|
||||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
|
||||
{{#if canSeePrivateMessages}}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export default Ember.View.extend(Discourse.LoadMore, {
|
||||
eyelineSelector: '.user-stream .notification',
|
||||
classNames: ['user-stream', 'notification-history'],
|
||||
templateName: 'user/notifications'
|
||||
});
|
|
@ -15,6 +15,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.end-of-stream {
|
||||
border: 3px solid $primary;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-navigation {
|
||||
|
||||
.map {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue