FEATURE: Desktop notifications

This commit is contained in:
riking 2015-03-27 17:09:22 -07:00
parent 706183f886
commit f5e27fe2c8
5 changed files with 193 additions and 13 deletions

View File

@ -0,0 +1,153 @@
// TODO deduplicate controllers/notification.js
function notificationUrl(n) {
const it = Em.Object.create(n);
var badgeId = it.get("data.badge_id");
if (badgeId) {
var badgeName = it.get("data.badge_name");
return '/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 '/my/invited';
}
}
export default Discourse.Controller.extend({
initSeenNotifications: function() {
const self = this;
// TODO make protocol to elect a tab responsible for desktop notifications
// and choose a new one when a tab is closed
// apparently needs to use localStorage !?
// https://github.com/diy/intercom.js
// Just causes a bit of a visual glitch as multiple are created and
// instantly replaced as is
self.set('primaryTab', true);
self.set('liveEnabled', false);
this.requestPermission().then(function() {
self.set('liveEnabled', true);
}).catch(function() {
self.set('liveEnabled', false);
});
self.set('seenNotificationDates', {});
Discourse.ajax("/notifications.json?silent=true").then(function(result) {
self.updateSeenNotificationDatesFrom(result);
});
}.on('init'),
// Call-in point from message bus
notificationsChanged(currentUser) {
if (!this.get('liveEnabled')) { return; }
if (!this.get('primaryTab')) { return; }
const blueNotifications = currentUser.get('unread_notifications');
const greenNotifications = currentUser.get('unread_private_messages');
const self = this;
if (blueNotifications > 0 || greenNotifications > 0) {
Discourse.ajax("/notifications.json?silent=true").then(function(result) {
const unread = result.filter(n => !n.read);
const unseen = self.updateSeenNotificationDatesFrom(result);
const unreadCount = unread.length;
const unseenCount = unseen.length;
if (unseenCount === 0) {
return;
}
if (typeof document.hidden !== "undefined" && !document.hidden) {
return;
}
let bodyParts = [];
unread.forEach(function(n) {
const i18nOpts = {
username: n.data['display_username'],
topic: n.data['topic_title'],
badge: n.data['badge_name']
};
bodyParts.push(I18n.t(self.i18nKey(n), i18nOpts));
});
const notificationTitle = I18n.t('notifications.popup_title', { count: unseenCount, site_title: Discourse.SiteSettings.title });
const notificationBody = bodyParts.join("\n");
const notificationIcon = Discourse.SiteSettings.logo_small_url || Discourse.SiteSettings.logo_url;
const notificationTag = self.get('notificationTagName');
// This shows the notification!
const notification = new Notification(notificationTitle, {
body: notificationBody,
icon: notificationIcon,
tag: notificationTag
});
const firstUnseen = unseen[0];
notification.addEventListener('click', function() {
window.location.href = notificationUrl(firstUnseen);
window.focus();
});
});
}
},
// Utility function
// Wraps Notification.requestPermission in a Promise
requestPermission() {
return new Ember.RSVP.Promise(function(resolve, reject) {
console.log('requesting');
Notification.requestPermission(function(status) {
console.log('requested, status:', status);
if (status === "granted") {
resolve();
} else {
reject();
}
});
});
},
i18nKey(notification) {
let key = "notifications.popup." + this.site.get("notificationLookup")[notification.notification_type];
if (notification.data.display_username && notification.data.original_username &&
notification.data.display_username !== notification.data.original_username) {
key += "_mul";
}
return key;
},
notificationTagName: function() {
return "discourse-notification-popup-" + Discourse.SiteSettings.title;
}.property(),
// Utility function
updateSeenNotificationDatesFrom(notifications) {
const oldSeenNotificationDates = this.get('seenNotificationDates');
let newSeenNotificationDates = {};
let previouslyUnseenNotifications = [];
notifications.forEach(function(notification) {
const dateString = new Date(notification.created_at).toUTCString();
if (!oldSeenNotificationDates[dateString]) {
previouslyUnseenNotifications.push(notification);
}
newSeenNotificationDates[dateString] = true;
});
this.set('seenNotificationDates', newSeenNotificationDates);
return previouslyUnseenNotifications;
}
})

View File

@ -2,7 +2,7 @@ import ObjectController from 'discourse/controllers/object';
var INVITED_TYPE= 8; var INVITED_TYPE= 8;
export default ObjectController.extend({ const NotificationController = ObjectController.extend({
scope: function() { scope: function() {
return "notifications." + this.site.get("notificationLookup")[this.get("notification_type")]; return "notifications." + this.site.get("notificationLookup")[this.get("notification_type")];
@ -10,20 +10,23 @@ export default ObjectController.extend({
username: Em.computed.alias("data.display_username"), username: Em.computed.alias("data.display_username"),
safe: function (prop) { safe(prop) {
var val = this.get(prop); let val = this.get(prop);
if (val) { val = Handlebars.Utils.escapeExpression(val); } if (val) { val = Handlebars.Utils.escapeExpression(val); }
return val; return val;
}, },
// This is model logic
// It belongs in a model
// TODO deduplicate controllers/background-notifications.js
url: function() { url: function() {
var badgeId = this.safe("data.badge_id"); const badgeId = this.safe("data.badge_id");
if (badgeId) { if (badgeId) {
var badgeName = this.safe("data.badge_name"); const badgeName = this.safe("data.badge_name");
return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase()); return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase());
} }
var topicId = this.safe('topic_id'); const topicId = this.safe('topic_id');
if (topicId) { if (topicId) {
return Discourse.Utilities.postUrl(this.safe("slug"), topicId, this.safe("post_number")); return Discourse.Utilities.postUrl(this.safe("slug"), topicId, this.safe("post_number"));
} }
@ -34,9 +37,11 @@ export default ObjectController.extend({
}.property("data.{badge_id,badge_name}", "slug", "topic_id", "post_number"), }.property("data.{badge_id,badge_name}", "slug", "topic_id", "post_number"),
description: function() { description: function() {
var badgeName = this.safe("data.badge_name"); const badgeName = this.safe("data.badge_name");
if (badgeName) { return badgeName; } if (badgeName) { return badgeName; }
return this.blank("data.topic_title") ? "" : this.safe("data.topic_title"); return this.blank("data.topic_title") ? "" : this.safe("data.topic_title");
}.property("data.{badge_name,topic_title}") }.property("data.{badge_name,topic_title}")
}); });
export default NotificationController;

View File

@ -1,6 +1,7 @@
export default Ember.ArrayController.extend({ const NotificationsController = Ember.ArrayController.extend({
needs: ['header'], needs: ['header'],
loadingNotifications: Em.computed.alias('controllers.header.loadingNotifications'), loadingNotifications: Em.computed.alias('controllers.header.loadingNotifications'),
myNotificationsUrl: Discourse.computed.url('/my/notifications') myNotificationsUrl: Discourse.computed.url('/my/notifications')
}); });
export default NotificationsController;

View File

@ -6,7 +6,8 @@ export default {
const user = container.lookup('current-user:main'), const user = container.lookup('current-user:main'),
site = container.lookup('site:main'), site = container.lookup('site:main'),
siteSettings = container.lookup('site-settings:main'), siteSettings = container.lookup('site-settings:main'),
bus = container.lookup('message-bus:main'); bus = container.lookup('message-bus:main'),
bgController = container.lookup('controller:background-notifications');
bus.callbackInterval = siteSettings.anon_polling_interval; bus.callbackInterval = siteSettings.anon_polling_interval;
bus.backgroundCallbackInterval = siteSettings.background_polling_interval; bus.backgroundCallbackInterval = siteSettings.background_polling_interval;
@ -44,6 +45,7 @@ export default {
if(oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) { if(oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) {
user.set('lastNotificationChange', new Date()); user.set('lastNotificationChange', new Date());
bgController.notificationsChanged(user);
} }
}), user.notification_channel_position); }), user.notification_channel_position);

View File

@ -821,6 +821,25 @@ en:
linked: "<i title='linked post' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" linked: "<i title='linked post' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>"
granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p>Earned '{{description}}'</p>" granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p>Earned '{{description}}'</p>"
popup_title:
one: "New notification on {{site_title}}"
other: "{{count}} new notifications on {{site_title}}"
popup:
mentioned: '{{username}} mentioned you in "{{topic}}"'
quoted: '{{username}} quoted you in "{{topic}}"'
replied: '{{username}} replied to you in "{{topic}}"'
replied_mul: '{{username}} in "{{topic}}"'
posted: '{{username}} posted in "{{topic}}"'
posted_mul: '{{username}} posted in "{{topic}}"'
edited: '{{username}} edited your post in "{{topic}}"'
liked: '{{username}} liked your post in "{{topic}}"'
private_message: '{{username}} sent you a private message in "{{topic}}"'
invited_to_private_message: '{{username}} invited you to a private message: "{{topic}}"'
invitee_accepted: '{{username}} joined the forum!'
moved_post: '{{username}} moved your post in "{{topic}}"'
linked: '{{username}} linked to your post from "{{topic}}"'
granted_badge: 'You earned the "{{badge}}" badge!'
upload_selector: upload_selector:
title: "Add an image" title: "Add an image"
title_with_attachments: "Add an image or a file" title_with_attachments: "Add an image or a file"