mirror of
https://github.com/discourse/discourse.git
synced 2025-01-19 04:52:45 +08:00
Merge pull request #3304 from riking/desktop-notifications
Desktop notifications!
This commit is contained in:
commit
15ea0c4789
|
@ -1,42 +1,22 @@
|
|||
import ObjectController from 'discourse/controllers/object';
|
||||
|
||||
var INVITED_TYPE= 8;
|
||||
import { notificationUrl } from 'discourse/lib/desktop-notifications';
|
||||
|
||||
export default ObjectController.extend({
|
||||
|
||||
scope: function () {
|
||||
scope: function() {
|
||||
return "notifications." + this.site.get("notificationLookup")[this.get("notification_type")];
|
||||
}.property("notification_type"),
|
||||
|
||||
username: Em.computed.alias("data.display_username"),
|
||||
|
||||
safe: function (prop) {
|
||||
var val = this.get(prop);
|
||||
if (val) { val = Handlebars.Utils.escapeExpression(val); }
|
||||
return val;
|
||||
},
|
||||
|
||||
url: function () {
|
||||
var badgeId = this.safe("data.badge_id");
|
||||
if (badgeId) {
|
||||
var badgeName = this.safe("data.badge_name");
|
||||
return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase());
|
||||
}
|
||||
|
||||
var topicId = this.safe('topic_id');
|
||||
if (topicId) {
|
||||
return Discourse.Utilities.postUrl(this.safe("slug"), topicId, this.safe("post_number"));
|
||||
}
|
||||
|
||||
if (this.get('notification_type') === INVITED_TYPE) {
|
||||
return Discourse.getURL('/my/invited');
|
||||
}
|
||||
url: function() {
|
||||
return notificationUrl(this);
|
||||
}.property("data.{badge_id,badge_name}", "slug", "topic_id", "post_number"),
|
||||
|
||||
description: function () {
|
||||
var badgeName = this.safe("data.badge_name");
|
||||
if (badgeName) { return badgeName; }
|
||||
return this.blank("data.topic_title") ? "" : this.safe("data.topic_title");
|
||||
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}")
|
||||
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export default Ember.ArrayController.extend({
|
||||
needs: ['header'],
|
||||
loadingNotifications: Em.computed.alias('controllers.header.loadingNotifications'),
|
||||
|
||||
myNotificationsUrl: Discourse.computed.url('/my/notifications')
|
||||
});
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// Subscribes to user events on the message bus
|
||||
import { init as initDesktopNotifications, onNotification } from 'discourse/lib/desktop-notifications';
|
||||
|
||||
export default {
|
||||
name: 'subscribe-user-notifications',
|
||||
after: 'message-bus',
|
||||
|
@ -14,7 +16,7 @@ export default {
|
|||
|
||||
if (bus.baseUrl !== '/') {
|
||||
// zepto compatible, 1 param only
|
||||
bus.ajax = function(opts){
|
||||
bus.ajax = function(opts) {
|
||||
opts.headers = opts.headers || {};
|
||||
opts.headers['X-Shared-Session-Key'] = $('meta[name=shared_session_key]').attr('content');
|
||||
return $.ajax(opts);
|
||||
|
@ -35,23 +37,26 @@ export default {
|
|||
user.set('post_queue_new_count', data.post_queue_new_count);
|
||||
});
|
||||
}
|
||||
bus.subscribe("/notification/" + user.get('id'), (function(data) {
|
||||
bus.subscribe("/notification/" + user.get('id'), function(data) {
|
||||
const oldUnread = user.get('unread_notifications');
|
||||
const oldPM = user.get('unread_private_messages');
|
||||
|
||||
user.set('unread_notifications', data.unread_notifications);
|
||||
user.set('unread_private_messages', data.unread_private_messages);
|
||||
|
||||
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());
|
||||
onNotification(user);
|
||||
}
|
||||
}), user.notification_channel_position);
|
||||
}, user.notification_channel_position);
|
||||
|
||||
bus.subscribe("/categories", function(data){
|
||||
_.each(data.categories,function(c){
|
||||
bus.subscribe("/categories", function(data) {
|
||||
_.each(data.categories,function(c) {
|
||||
site.updateCategory(c);
|
||||
});
|
||||
});
|
||||
|
||||
initDesktopNotifications(bus);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
|
||||
let primaryTab = false;
|
||||
let liveEnabled = false;
|
||||
let mbClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
|
||||
let lastAction = -1;
|
||||
|
||||
const focusTrackerKey = "focus-tracker";
|
||||
const seenDataKey = "seen-notifications";
|
||||
const recentUpdateThreshold = 1000 * 60 * 2; // 2 minutes
|
||||
const idleThresholdTime = 1000 * 10; // 10 seconds
|
||||
const INVITED_TYPE = 8;
|
||||
let notificationTagName; // "discourse-notification-popup-" + Discourse.SiteSettings.title;
|
||||
|
||||
// Called from an initializer
|
||||
function init(messageBus) {
|
||||
liveEnabled = false;
|
||||
mbClientId = messageBus.clientId;
|
||||
requestPermission().then(function() {
|
||||
try {
|
||||
localStorage.getItem(focusTrackerKey);
|
||||
} catch (e) {
|
||||
Em.Logger.info('Discourse desktop notifications are disabled - localStorage denied.');
|
||||
return;
|
||||
}
|
||||
liveEnabled = true;
|
||||
Em.Logger.info('Discourse desktop notifications are enabled.');
|
||||
try {
|
||||
// Permission is granted, continue with setup
|
||||
setupNotifications();
|
||||
} catch (e) {
|
||||
Em.Logger.error(e);
|
||||
}
|
||||
}).catch(function() {
|
||||
liveEnabled = false;
|
||||
//Em.Logger.debug('Discourse desktop notifications are disabled - permission denied.');
|
||||
});
|
||||
}
|
||||
|
||||
// This function is only called if permission was granted
|
||||
function setupNotifications() {
|
||||
// Load up the current state of the notifications
|
||||
const seenData = JSON.parse(localStorage.getItem(seenDataKey));
|
||||
let markAllSeen = true;
|
||||
if (seenData) {
|
||||
const lastUpdatedAt = new Date(seenData.updated_at);
|
||||
if (lastUpdatedAt.getTime() + recentUpdateThreshold > new Date().getTime()) {
|
||||
// The following conditions are met:
|
||||
// - This is a new Discourse tab
|
||||
// - The seen notification data was updated in the last 2 minutes
|
||||
// Therefore, there is no need to reset the data.
|
||||
markAllSeen = false;
|
||||
}
|
||||
}
|
||||
if (markAllSeen) {
|
||||
Discourse.ajax("/notifications.json?silent=true").then(function(result) {
|
||||
updateSeenNotificationDatesFrom(result);
|
||||
});
|
||||
}
|
||||
|
||||
notificationTagName = "discourse-notification-popup-" + Discourse.SiteSettings.title;
|
||||
|
||||
|
||||
window.addEventListener("storage", function(e) {
|
||||
// note: This event only fires when other tabs setItem()
|
||||
const key = e.key;
|
||||
if (key !== focusTrackerKey) {
|
||||
return true;
|
||||
}
|
||||
primaryTab = false;
|
||||
});
|
||||
|
||||
window.addEventListener("focus", function() {
|
||||
if (!primaryTab) {
|
||||
primaryTab = true;
|
||||
localStorage.setItem(focusTrackerKey, mbClientId);
|
||||
}
|
||||
});
|
||||
|
||||
if (document && (typeof document.hidden !== "undefined") && document["hidden"]) {
|
||||
primaryTab = false;
|
||||
} else {
|
||||
primaryTab = true;
|
||||
localStorage.setItem(focusTrackerKey, mbClientId);
|
||||
}
|
||||
|
||||
if (document) {
|
||||
document.addEventListener("scroll", resetIdle);
|
||||
}
|
||||
window.addEventListener("mouseover", resetIdle);
|
||||
Discourse.PageTracker.on("change", resetIdle);
|
||||
}
|
||||
|
||||
function resetIdle() {
|
||||
lastAction = Date.now();
|
||||
}
|
||||
function isIdle() {
|
||||
return lastAction + idleThresholdTime < Date.now();
|
||||
}
|
||||
|
||||
// Call-in point from message bus
|
||||
function onNotification(currentUser) {
|
||||
if (!liveEnabled) { return; }
|
||||
if (!primaryTab) { return; }
|
||||
|
||||
const blueNotifications = currentUser.get('unread_notifications');
|
||||
const greenNotifications = currentUser.get('unread_private_messages');
|
||||
|
||||
if (blueNotifications > 0 || greenNotifications > 0) {
|
||||
Discourse.ajax("/notifications.json?silent=true").then(function(result) {
|
||||
|
||||
const unread = result.filter(n => !n.read);
|
||||
const unseen = updateSeenNotificationDatesFrom(result);
|
||||
const unreadCount = unread.length;
|
||||
const unseenCount = unseen.length;
|
||||
|
||||
|
||||
// If all notifications are seen, don't display
|
||||
if (unreadCount === 0 || unseenCount === 0) {
|
||||
return;
|
||||
}
|
||||
// If active in last 10 seconds, don't display
|
||||
if (!isIdle()) {
|
||||
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(i18nKey(n), i18nOpts));
|
||||
});
|
||||
|
||||
const notificationTitle = I18n.t('notifications.popup_title', { count: unreadCount, site_title: Discourse.SiteSettings.title });
|
||||
const notificationBody = bodyParts.join("\n");
|
||||
const notificationIcon = Discourse.SiteSettings.logo_small_url || Discourse.SiteSettings.logo_url;
|
||||
|
||||
// This shows the notification!
|
||||
const notification = new Notification(notificationTitle, {
|
||||
body: notificationBody,
|
||||
icon: notificationIcon,
|
||||
tag: notificationTagName
|
||||
});
|
||||
|
||||
const firstUnseen = unseen[0];
|
||||
|
||||
function clickEventHandler() {
|
||||
Discourse.URL.routeTo(_notificationUrl(firstUnseen));
|
||||
// Cannot delay this until the page renders :(
|
||||
// due to trigger-based permissions
|
||||
window.focus();
|
||||
}
|
||||
|
||||
notification.addEventListener('click', clickEventHandler);
|
||||
setTimeout(function() {
|
||||
notification.close();
|
||||
notification.removeEventListener('click', clickEventHandler);
|
||||
}, 10 * 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const DATA_VERSION = 2;
|
||||
function updateSeenNotificationDatesFrom(notifications) {
|
||||
const oldSeenData = JSON.parse(localStorage.getItem(seenDataKey));
|
||||
const oldSeenNotificationDates = (oldSeenData && oldSeenData.v === DATA_VERSION) ? oldSeenData.data : [];
|
||||
let newSeenNotificationDates = [];
|
||||
let previouslyUnseenNotifications = [];
|
||||
|
||||
notifications.forEach(function(notification) {
|
||||
const dateString = new Date(notification.created_at).toUTCString();
|
||||
|
||||
if (oldSeenNotificationDates.indexOf(dateString) === -1) {
|
||||
previouslyUnseenNotifications.push(notification);
|
||||
}
|
||||
newSeenNotificationDates.push(dateString);
|
||||
});
|
||||
|
||||
localStorage.setItem(seenDataKey, JSON.stringify({
|
||||
data: newSeenNotificationDates,
|
||||
updated_at: new Date(),
|
||||
v: DATA_VERSION
|
||||
}));
|
||||
return previouslyUnseenNotifications;
|
||||
}
|
||||
|
||||
// Utility function
|
||||
// Wraps Notification.requestPermission in a Promise
|
||||
function requestPermission() {
|
||||
return new Ember.RSVP.Promise(function(resolve, reject) {
|
||||
Notification.requestPermission(function(status) {
|
||||
if (status === "granted") {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function i18nKey(notification) {
|
||||
let key = "notifications.popup." + Discourse.Site.current().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;
|
||||
}
|
||||
|
||||
// Exported for controllers/notification.js.es6
|
||||
function notificationUrl(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');
|
||||
}
|
||||
}
|
||||
|
||||
function _notificationUrl(notificationJson) {
|
||||
const it = Em.Object.create(notificationJson);
|
||||
return notificationUrl(it);
|
||||
}
|
||||
|
||||
export { init, notificationUrl, onNotification };
|
|
@ -66,6 +66,7 @@
|
|||
//= require ./discourse/dialects/dialect
|
||||
//= require ./discourse/lib/emoji/emoji
|
||||
//= require ./discourse/lib/sharing
|
||||
//= require discourse/lib/desktop-notifications
|
||||
|
||||
//= require_tree ./discourse/dialects
|
||||
//= require_tree ./discourse/controllers
|
||||
|
|
|
@ -831,6 +831,25 @@ en:
|
|||
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>"
|
||||
|
||||
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:
|
||||
title: "Add an image"
|
||||
title_with_attachments: "Add an image or a file"
|
||||
|
|
Loading…
Reference in New Issue
Block a user