FEATURE: Desktop notifications
This commit is contained in:
parent
706183f886
commit
f5e27fe2c8
|
@ -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;
|
||||||
|
}
|
||||||
|
})
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue