Unverified Commit db4a1f8d authored by Tasso Evangelista's avatar Tasso Evangelista Committed by GitHub

[FIX] Use Electron notifications (#1101)

* Relax autoplay policy to allow sound notifications

* Resolve notification icon URL

* Use Electron Notification API

* Add tests for notifications module
parent 08eb7b4c
......@@ -4,19 +4,17 @@ import url from 'url';
import './background/aboutDialog';
import appData from './background/appData';
import certificate from './background/certificate';
import dock from './background/dock';
export { default as dock } from './background/dock';
import { addServer, getMainWindow } from './background/mainWindow';
import menus from './background/menus';
import './background/notifications';
export { default as menus } from './background/menus';
export { default as notifications } from './background/notifications';
import './background/screenshareDialog';
import tray from './background/tray';
export { default as remoteServers } from './background/servers';
export { default as tray } from './background/tray';
import './background/updateDialog';
import './background/updates';
import i18n from './i18n';
export { default as remoteServers } from './background/servers';
export { certificate, dock, menus, tray };
export { certificate };
process.env.GOOGLE_API_KEY = 'AIzaSyADqUh_c1Qhji3Cp1NE43YrcpuPkmhXD-c';
......@@ -49,6 +47,7 @@ app.setAppUserModelId('chat.rocket');
if (process.platform === 'linux') {
app.disableHardwareAcceleration();
}
app.commandLine.appendSwitch('--autoplay-policy', 'no-user-gesture-required');
process.on('unhandledRejection', console.error.bind(console));
......
......@@ -121,4 +121,5 @@ ipcMain.on('focus', async() => {
}
mainWindow.show();
mainWindow.focus();
});
import { app, ipcMain, Notification } from 'electron';
import freedesktopNotifications from 'freedesktop-notifications';
import os from 'os';
import path from 'path';
import { nativeImage, Notification } from 'electron';
class BaseNotification {
constructor(options = {}) {
this.handleShow = this.handleShow.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleClose = this.handleClose.bind(this);
this.initialize(options);
}
handleShow() {
const { id, eventTarget } = this;
eventTarget && !eventTarget.isDestroyed() && eventTarget.send('notification-shown', id);
}
handleClick() {
const { id, eventTarget } = this;
eventTarget && !eventTarget.isDestroyed() && eventTarget.send('notification-clicked', id);
}
handleClose() {
const { id, eventTarget } = this;
eventTarget && !eventTarget.isDestroyed() && eventTarget.send('notification-closed', id);
}
initialize(/* options = {} */) {}
reset(/* options = {} */) {}
show() {}
close() {}
}
class ElectronNotification extends BaseNotification {
initialize({ title, body, icon, silent } = {}) {
this.notification = new Notification({
title,
body,
icon: icon && path.resolve(icon),
silent,
});
this.notification.on('show', this.handleShow);
this.notification.on('click', this.handleClick);
this.notification.on('close', this.handleClose);
}
reset(options = {}) {
this.notification.removeAllListeners();
this.notification.close();
this.createNotification(options);
}
show() {
this.notification.show();
}
close() {
this.notification.close();
}
}
class FreeDesktopNotification extends BaseNotification {
escapeBody(body) {
const escapeMap = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#x27;',
'`': '&#x60;',
};
const escapeRegex = new RegExp(`(?:${ Object.keys(escapeMap).join('|') })`, 'g');
return body.replace(escapeRegex, (match) => escapeMap[match]);
}
initialize({ title, body, icon, silent } = {}) {
this.notification = freedesktopNotifications.createNotification({
summary: title,
body: body && this.escapeBody(body),
icon: icon ? path.resolve(icon) : 'info',
appName: app.getName(),
timeout: 24 * 60 * 60 * 1000,
sound: silent ? undefined : 'message-new-instant',
actions: process.env.XDG_CURRENT_DESKTOP !== 'Unity' ? {
default: '',
} : null,
});
this.notification.on('action', (action) => action === 'default' && this.handleClick());
this.notification.on('close', this.handleClose);
}
reset({ title, body, icon } = {}) {
this.notification.set({
summary: title,
body,
icon: icon ? path.resolve(icon) : 'info',
});
}
show() {
this.notification.push(this.handleShow);
}
function create({ icon, ...options }) {
const notification = new Notification({
icon: icon && nativeImage.createFromDataURL(icon),
...options,
});
close() {
this.notification.close();
}
return notification;
}
const ImplementatedNotification = (() => {
if (os.platform() === 'linux') {
return FreeDesktopNotification;
}
return ElectronNotification;
})();
const instances = new Map();
let creationCount = 1;
const createOrGetNotification = (options = {}) => {
const tag = options.tag ? JSON.stringify(options.tag) : null;
if (!tag || !instances.get(tag)) {
const notification = new ImplementatedNotification(options);
notification.id = tag || creationCount++;
instances.set(notification.id, notification);
return notification;
}
const notification = instances.get(tag);
notification.reset(options);
return notification;
export default {
create,
};
ipcMain.on('request-notification', (event, options) => {
try {
const notification = createOrGetNotification(options);
notification.eventTarget = event.sender;
event.returnValue = notification.id;
setImmediate(() => notification.show());
} catch (e) {
console.error(e);
event.returnValue = -1;
}
});
ipcMain.on('close-notification', (event, id) => {
try {
const notification = instances.get(id);
if (notification) {
notification.close();
instances.delete(id);
}
} catch (e) {
console.error(e);
}
});
app.on('before-quit', () => {
instances.forEach((notification) => {
notification.close();
});
});
import { expect } from 'chai';
import { Notification } from 'electron';
import notifications from './notifications';
const { describe, it } = global;
describe('notifications', () => {
it('create', () => {
expect(notifications.create({})).to.be.instanceOf(Notification);
});
it('create with icon', () => {
const icon = 'data:image/png;base64,' +
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
expect(notifications.create({ icon })).to.be.instanceOf(Notification);
});
});
import { ipcRenderer, nativeImage } from 'electron';
import { ipcRenderer, remote } from 'electron';
import { EventEmitter } from 'events';
import jetpack from 'fs-jetpack';
import tmp from 'tmp';
import mem from 'mem';
const { notifications } = remote.require('./background');
const instances = new Map();
class Notification extends EventEmitter {
static requestPermission() {
return;
......@@ -17,69 +15,90 @@ class Notification extends EventEmitter {
constructor(title, options) {
super();
this.createIcon = mem(this.createIcon.bind(this));
this.create({ title, ...options });
}
async createIcon(icon) {
const img = new Image();
img.src = icon;
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
return canvas.toDataURL();
}
async create({ icon, ...options }) {
if (icon) {
Notification.cachedIcons = Notification.cachedIcons || {};
if (!Notification.cachedIcons[icon]) {
Notification.cachedIcons[icon] = await new Promise((resolve, reject) =>
tmp.file((err, path) => (err ? reject(err) : resolve(path))));
const buffer = nativeImage.createFromDataURL(icon).toPNG();
await jetpack.writeAsync(Notification.cachedIcons[icon], buffer);
}
icon = Notification.cachedIcons[icon];
icon = await this.createIcon(icon);
}
this.id = ipcRenderer.sendSync('request-notification', { icon, ...options });
instances.set(this.id, this);
}
const notification = notifications.create({ icon, ...options });
close() {
ipcRenderer.send('close-notification', this.id);
}
}
notification.on('show', this.handleShow.bind(this));
notification.on('close', this.handleClose.bind(this));
notification.on('click', this.handleClick.bind(this));
notification.on('reply', this.handleReply.bind(this));
notification.on('action', this.handleAction.bind(this));
const handleNotificationShown = (event, id) => {
const notification = instances.get(id);
if (!notification) {
return;
notification.show();
this.notification = notification;
}
typeof notification.onshow === 'function' && notification.onshow.call(notification);
notification.emit('show');
};
handleShow(event) {
event.currentTarget = this;
this.onshow && this.onshow.call(this, event);
this.emit('show', event);
}
const handleNotificationClicked = (event, id) => {
const notification = instances.get(id);
if (!notification) {
return;
handleClose(event) {
event.currentTarget = this;
this.onclose && this.onclose.call(this, event);
this.emit('close', event);
}
ipcRenderer.send('focus');
ipcRenderer.sendToHost('focus');
handleClick(event) {
ipcRenderer.send('focus');
ipcRenderer.sendToHost('focus');
event.currentTarget = this;
this.onclick && this.onclick.call(this, event);
this.emit('close', event);
}
typeof notification.onclick === 'function' && notification.onclick.call(notification);
notification.emit('click');
};
handleReply(event, reply) {
event.currentTarget = this;
event.response = reply;
this.onreply && this.onreply.call(this, event);
this.emit('reply', event);
}
const handleNotificationClosed = (event, id) => {
const notification = instances.get(id);
if (!notification) {
return;
handleAction(event, index) {
event.currentTarget = this;
event.index = index;
this.onaction && this.onaction.call(this, event);
this.emit('action', event);
}
typeof notification.onclose === 'function' && notification.onclose.call(notification);
notification.emit('close');
};
close() {
if (!this.notification) {
return;
}
this.notification.close();
this.notification = null;
}
}
export default () => {
window.Notification = Notification;
ipcRenderer.on('notification-shown', handleNotificationShown);
ipcRenderer.on('notification-clicked', handleNotificationClicked);
ipcRenderer.on('notification-closed', handleNotificationClosed);
};
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment