add notification grouping (#48)

This commit is contained in:
Lynn 2017-03-29 17:42:15 -07:00 committed by GitHub
parent 75d1342b28
commit 27d58a94d7
5 changed files with 164 additions and 86 deletions

View File

@ -34,6 +34,9 @@
</body> </body>
<script> <script>
var notfEl = document.getElementById('notf'); var notfEl = document.getElementById('notf');
var num = 0;
// note: notification will close when clicked
notfEl.addEventListener('click', function() { notfEl.addEventListener('click', function() {
var title = document.getElementById('title').value; var title = document.getElementById('title').value;
var body = document.getElementById('body').value; var body = document.getElementById('body').value;
@ -41,26 +44,46 @@
var shouldFlash = document.getElementById('flash').checked; var shouldFlash = document.getElementById('flash').checked;
var color = document.getElementById('color').value; var color = document.getElementById('color').value;
num++;
// notfs with same groupId will replace existing notf.
var groupId = (num % 2).toString();
var notf = new SYM_API.Notification(title, { var notf = new SYM_API.Notification(title, {
body: body, body: (body + ' num=' + num + ' groupId=' + groupId),
image: imageUrl, image: imageUrl,
flash: shouldFlash, flash: shouldFlash,
color: color || 'white', color: color || 'white',
data: { data: {
hello: 'hello word' hello: 'hello word'
} },
groupId: groupId
}); });
notf.addEventListener('click', function(event) { notf.addEventListener('click', onclick);
function onclick(event) {
event.target.data.then(function(value) { event.target.data.then(function(value) {
alert('notification clicked: ' + value.hello); alert('notification clicked: ' + value.hello);
}) })
}); }
notf.addEventListener('close', function() { notf.addEventListener('close', onclose);
function onclose() {
alert('notification closed'); alert('notification closed');
}); removeEvents();
};
notf.addEventListener('error', onerror);
function onerror(event) {
alert('error=' + event.result);
};
// be sure to remove all events when closed, otherwise leaks
// will occur.
function removeEvents() {
notf.removeEventListener('click', onclick)
notf.removeEventListener('close', onclose)
notf.removeEventListener('error', onerror)
}
}); });
var badgeCount = 0; var badgeCount = 0;

View File

@ -54,11 +54,11 @@ function setContents(notificationObj) {
audio.play() audio.play()
} }
} catch (e) { } catch (e) {
log('electron-notify: ERROR could not find sound file: ' + notificationObj.sound.replace('file://', ''), e, e.stack) log('electron-notify: ERROR could not find sound file: ' + notificationObj.sound.replace('file://', ''), e, e.stack);
} }
} }
let notiDoc = window.document let notiDoc = window.document;
let container = notiDoc.getElementById('container'); let container = notiDoc.getElementById('container');
@ -78,34 +78,35 @@ function setContents(notificationObj) {
} }
// Title // Title
let titleDoc = notiDoc.getElementById('title') let titleDoc = notiDoc.getElementById('title');
titleDoc.innerHTML = notificationObj.title || '' titleDoc.innerHTML = notificationObj.title || '';
// message // message
let messageDoc = notiDoc.getElementById('message') let messageDoc = notiDoc.getElementById('message');
messageDoc.innerHTML = notificationObj.text || '' messageDoc.innerHTML = notificationObj.text || '';
// Image // Image
let imageDoc = notiDoc.getElementById('image') let imageDoc = notiDoc.getElementById('image');
if (notificationObj.image) { if (notificationObj.image) {
imageDoc.src = notificationObj.image imageDoc.src = notificationObj.image;
} else { } else {
setStyleOnDomElement({ display: 'none'}, imageDoc) setStyleOnDomElement({ display: 'none'}, imageDoc);
} }
const winId = notificationObj.windowId; const winId = notificationObj.windowId;
// Close button let closeButton = notiDoc.getElementById('close');
let closeButton = notiDoc.getElementById('close')
closeButton.addEventListener('click', function(clickEvent) { // note: use onclick because we only want one handler, for case
// when content gets overwritten by notf with same groupId
closeButton.onclick = function(clickEvent) {
clickEvent.stopPropagation() clickEvent.stopPropagation()
ipc.send('electron-notify-close', winId, notificationObj) ipc.send('electron-notify-close', winId, notificationObj)
}) }
// URL container.onclick = function() {
container.addEventListener('click', function() { ipc.send('electron-notify-click', winId, notificationObj);
ipc.send('electron-notify-click', winId, notificationObj) }
})
} }
function setStyleOnDomElement(styleObj, domElement) { function setStyleOnDomElement(styleObj, domElement) {

View File

@ -229,20 +229,6 @@ function setupConfig() {
function notify(notification) { function notify(notification) {
if (notificationQueue.length >= MAX_QUEUE_SIZE) {
var id = latestID;
incrementId();
if (typeof notification.onErrorFunc === 'function') {
setTimeout(function() {
notification.onErrorFunc({
id: id,
error: 'max notification queue size reached: ' + MAX_QUEUE_SIZE
});
}, 0);
}
return id;
}
// Is it an object and only one argument? // Is it an object and only one argument?
if (arguments.length === 1 && typeof notification === 'object') { if (arguments.length === 1 && typeof notification === 'object') {
let notf = Object.assign({}, notification); let notf = Object.assign({}, notification);
@ -265,6 +251,54 @@ function incrementId() {
function showNotification(notificationObj) { function showNotification(notificationObj) {
return new Promise(function(resolve) { return new Promise(function(resolve) {
if (notificationQueue.length >= MAX_QUEUE_SIZE) {
if (typeof notificationObj.onErrorFunc === 'function') {
setTimeout(function() {
notificationObj.onErrorFunc({
id: notificationObj.id,
error: 'max notification queue size reached: ' + MAX_QUEUE_SIZE
});
}, 0);
}
resolve();
return;
}
// check if group id provided. should replace existing notification
// if has same grouping id.
let groupId = notificationObj.groupId;
if (groupId) {
// first check waiting items
for(let i = 0; i < notificationQueue.length; i++) {
if (groupId === notificationQueue[ i ].groupId) {
notificationQueue[ i ] = notificationObj;
resolve();
return;
}
}
// next check items being shown
for(let i = 0; i < activeNotifications.length; i++) {
if (groupId === activeNotifications[ i ].groupId) {
let notificationWindow = activeNotifications[ i ];
// be sure to call close event for existing, so it gets
// cleaned up.
if (notificationWindow.electronNotifyOnCloseFunc) {
notificationWindow.electronNotifyOnCloseFunc({
event: 'close',
id: notificationObj.id
});
delete notificationWindow.electronNotifyOnCloseFunc;
}
setNotificationContents(notificationWindow, notificationObj)
resolve();
return;
}
}
}
// Can we show it? // Can we show it?
if (activeNotifications.length < config.maxVisibleNotifications) { if (activeNotifications.length < config.maxVisibleNotifications) {
// Get inactiveWindow or create new: // Get inactiveWindow or create new:
@ -273,60 +307,74 @@ function showNotification(notificationObj) {
calcInsertPos() calcInsertPos()
setWindowPosition(notificationWindow, nextInsertPos.x, nextInsertPos.y) setWindowPosition(notificationWindow, nextInsertPos.x, nextInsertPos.y)
// Add to activeNotifications let updatedNotfWindow = setNotificationContents(notificationWindow, notificationObj);
activeNotifications.push(notificationWindow)
activeNotifications.push(updatedNotfWindow);
resolve(updatedNotfWindow);
})
} else {
// Add to notificationQueue
notificationQueue.push(notificationObj);
resolve();
}
})
}
function setNotificationContents(notfWindow, notfObj) {
// Display time per notification basis. // Display time per notification basis.
let displayTime = notificationObj.displayTime ? notificationObj.displayTime : config.displayTime let displayTime = notfObj.displayTime ? notfObj.displayTime : config.displayTime;
if (notfWindow.displayTimer) {
clearTimeout(notfWindow.displayTimer);
}
// Set timeout to hide notification // Set timeout to hide notification
let timeoutId let timeoutId;
let closeFunc = buildCloseNotification(notificationWindow, notificationObj, function() { let closeFunc = buildCloseNotification(notfWindow, notfObj, function() {
return timeoutId return timeoutId
}) });
let closeNotificationSafely = buildCloseNotificationSafely(closeFunc) let closeNotificationSafely = buildCloseNotificationSafely(closeFunc);
timeoutId = setTimeout(function() { timeoutId = setTimeout(function() {
closeNotificationSafely('timeout') closeNotificationSafely('timeout');
}, displayTime) }, displayTime);
var updatedNotificationWindow = notfWindow;
updatedNotificationWindow.displayTimer = timeoutId;
updatedNotificationWindow.groupId = notfObj.groupId;
// Trigger onShowFunc if existent // Trigger onShowFunc if existent
if (notificationObj.onShowFunc) { if (notfObj.onShowFunc) {
notificationObj.onShowFunc({ notfObj.onShowFunc({
event: 'show', event: 'show',
id: notificationObj.id, id: notfObj.id,
closeNotification: closeNotificationSafely closeNotification: closeNotificationSafely
}) })
} }
var updatedNotificationWindow = notificationWindow;
// Save onClickFunc in notification window // Save onClickFunc in notification window
if (notificationObj.onClickFunc) { if (notfObj.onClickFunc) {
updatedNotificationWindow.electronNotifyOnClickFunc = notificationObj.onClickFunc updatedNotificationWindow.electronNotifyOnClickFunc = notfObj.onClickFunc
} else { } else {
delete updatedNotificationWindow.electronNotifyOnClickFunc delete updatedNotificationWindow.electronNotifyOnClickFunc;
} }
if (notificationObj.onCloseFunc) { if (notfObj.onCloseFunc) {
updatedNotificationWindow.electronNotifyOnCloseFunc = notificationObj.onCloseFunc updatedNotificationWindow.electronNotifyOnCloseFunc = notfObj.onCloseFunc
} else { } else {
delete updatedNotificationWindow.electronNotifyOnCloseFunc delete updatedNotificationWindow.electronNotifyOnCloseFunc;
} }
const windowId = notificationWindow.id; const windowId = notfWindow.id;
// Set contents, ... // Set contents, ...
updatedNotificationWindow.webContents.send('electron-notify-set-contents', updatedNotificationWindow.webContents.send('electron-notify-set-contents',
Object.assign({ windowId: windowId}, notificationObj)); Object.assign({ windowId: windowId}, notfObj));
// Show window // Show window
updatedNotificationWindow.showInactive(); updatedNotificationWindow.showInactive();
resolve(updatedNotificationWindow)
}) return updatedNotificationWindow;
} else {
// Add to notificationQueue
notificationQueue.push(notificationObj)
resolve()
}
})
} }
// Close notification function // Close notification function

View File

@ -17,6 +17,7 @@ class Notify {
image: options.image || options.icon, image: options.image || options.icon,
flash: options.flash, flash: options.flash,
color: options.color, color: options.color,
groupId: options.groupId,
onShowFunc: onShow.bind(this), onShowFunc: onShow.bind(this),
onClickFunc: onClick.bind(this), onClickFunc: onClick.bind(this),
onCloseFunc: onClose.bind(this), onCloseFunc: onClose.bind(this),
@ -35,6 +36,7 @@ class Notify {
function onClick(arg) { function onClick(arg) {
if (arg.id === this._id) { if (arg.id === this._id) {
this.emitter.emit('click'); this.emitter.emit('click');
arg.closeNotification();
} }
} }
@ -80,7 +82,7 @@ class Notify {
removeEventListener(event, cb) { removeEventListener(event, cb) {
if (event && typeof cb === 'function') { if (event && typeof cb === 'function') {
this.emitter.removeEventListener(event, cb); this.emitter.removeListener(event, cb);
} }
} }

View File

@ -24,6 +24,9 @@ class Notify {
* icon {string} url of image to show in notification * icon {string} url of image to show in notification
* flash {bool} true if notification should flash (default false) * flash {bool} true if notification should flash (default false)
* color {string} background color for notification * color {string} background color for notification
* groupId {string} non-empty string to unique identify notf, if another
* notification arrives with same groupId then it's content will
* replace existing notification.
* data {object} arbitrary object to be stored with notification * data {object} arbitrary object to be stored with notification
* } * }
*/ */
@ -41,7 +44,8 @@ class Notify {
static get permission() {} static get permission() {}
/** /**
* returns data object passed in via constructor options * returns data object passed in via constructor options, return a
* promise that will be fullfilled with the data.
*/ */
get data() {} get data() {}