From 8cb715bb3ae057f40ba920d939d680279b5af117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20B=C3=BCchele?= Date: Mon, 12 Nov 2018 01:47:52 -0800 Subject: [PATCH] adding actions to notifications Summary: This diff adds action buttons to the notifications. Notifications with actions can only be sent from the main process. This is why we need to send a message to the main process which then shows the notification. The action callbacks are sent back to the renderer process to handle the action and log the event. Reviewed By: passy Differential Revision: D12999886 fbshipit-source-id: b415fded3172582fad11d88cabf0cfc5b3b8d4f9 --- package.json | 5 +- src/dispatcher/notifications.js | 96 +++++++++++++++++++++++++++------ static/index.js | 34 +++++++++++- 3 files changed, 117 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index d23d82057..30de72323 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "productName": "Flipper", "artifactName": "Flipper-${os}.${ext}", "mac": { - "category": "public.app-category.developer-tools" + "category": "public.app-category.developer-tools", + "extendInfo": { + "NSUserNotificationAlertStyle": "alert" + } }, "win": { "publisherName": "Facebook, Inc." diff --git a/src/dispatcher/notifications.js b/src/dispatcher/notifications.js index 479f500ef..93a865dee 100644 --- a/src/dispatcher/notifications.js +++ b/src/dispatcher/notifications.js @@ -10,12 +10,18 @@ import type Logger from '../fb-stubs/Logger.js'; import type {PluginNotification} from '../reducers/notifications'; import type {FlipperPlugin} from '../plugin.js'; +import {ipcRenderer} from 'electron'; import {selectPlugin} from '../reducers/connections'; -import {setActiveNotifications} from '../reducers/notifications'; +import { + setActiveNotifications, + updatePluginBlacklist, +} from '../reducers/notifications'; import {textContent} from '../utils/index'; import {clientPlugins} from '../plugins/index.js'; import GK from '../fb-stubs/GK'; +type NotificationEvents = 'show' | 'click' | 'close' | 'reply' | 'action'; + export default (store: Store, logger: Logger) => { if (GK.get('flipper_disable_notifications')) { return; @@ -24,22 +30,67 @@ export default (store: Store, logger: Logger) => { const knownNotifications: Set = new Set(); const knownPluginStates: Map = new Map(); + ipcRenderer.on( + 'notificationEvent', + ( + e, + eventName: NotificationEvents, + pluginNotification: PluginNotification, + arg: null | string | number, + ) => { + if (eventName === 'click' || (eventName === 'action' && arg === 0)) { + store.dispatch( + selectPlugin({ + selectedPlugin: 'notifications', + selectedApp: null, + deepLinkPayload: pluginNotification.notification.id, + }), + ); + } else if (eventName === 'action') { + if (arg === 1 && pluginNotification.notification.category) { + // Hide similar (category) + logger.track( + 'usage', + 'notification-hide-category', + pluginNotification, + ); + } else if (arg === 2) { + // Hide plugin + logger.track('usage', 'notification-hide-plugin', pluginNotification); + + const {blacklistedPlugins} = store.getState().notifications; + if (blacklistedPlugins.indexOf(pluginNotification.pluginId) === -1) { + store.dispatch( + updatePluginBlacklist([ + ...blacklistedPlugins, + pluginNotification.pluginId, + ]), + ); + } + } + } + }, + ); + store.subscribe(() => { const {notifications, pluginStates} = store.getState(); + const pluginMap: Map>> = clientPlugins.reduce( + (acc, cv) => acc.set(cv.id, cv), + new Map(), + ); + Object.keys(pluginStates).forEach(key => { if (knownPluginStates.get(key) !== pluginStates[key]) { knownPluginStates.set(key, pluginStates[key]); const [client, pluginId] = key.split('#'); - const persistingPlugin: ?Class> = clientPlugins.find( - (p: Class>) => - p.id === pluginId && p.getActiveNotifications, + const persistingPlugin: ?Class> = pluginMap.get( + pluginId, ); - if (persistingPlugin) { + if (persistingPlugin && persistingPlugin.getActiveNotifications) { store.dispatch( setActiveNotifications({ - // $FlowFixMe: Ensured getActiveNotifications is implemented in filter notifications: persistingPlugin.getActiveNotifications( pluginStates[key], ), @@ -55,21 +106,34 @@ export default (store: Store, logger: Logger) => { activeNotifications.forEach((n: PluginNotification) => { if ( + store.getState().connections.selectedPlugin !== 'notifications' && !knownNotifications.has(n.notification.id) && blacklistedPlugins.indexOf(n.pluginId) === -1 ) { - const notification = new window.Notification(n.notification.title, { - body: textContent(n.notification.message), + ipcRenderer.send('sendNotification', { + payload: { + title: n.notification.title, + body: textContent(n.notification.message), + actions: [ + { + type: 'button', + text: 'Show', + }, + { + type: 'button', + text: 'Hide similar', + }, + { + type: 'button', + text: `Hide all ${pluginMap.get(n.pluginId)?.title || ''}`, + }, + ], + closeButtonText: 'Hide', + }, + closeAfter: 10000, + pluginNotification: n, }); logger.track('usage', 'native-notification', n.notification); - notification.onclick = () => - store.dispatch( - selectPlugin({ - selectedPlugin: 'notifications', - selectedApp: null, - deepLinkPayload: n.notification.id, - }), - ); knownNotifications.add(n.notification.id); } }); diff --git a/static/index.js b/static/index.js index 8153aa462..24bac5778 100644 --- a/static/index.js +++ b/static/index.js @@ -8,13 +8,14 @@ const [s, ns] = process.hrtime(); let launchStartTime = s * 1e3 + ns / 1e6; -const {app, BrowserWindow, ipcMain} = require('electron'); +const {app, BrowserWindow, ipcMain, Notification} = require('electron'); const path = require('path'); const url = require('url'); const fs = require('fs'); const {exec} = require('child_process'); const compilePlugins = require('./compilePlugins.js'); const os = require('os'); + // disable electron security warnings: https://github.com/electron/electron/blob/master/docs/tutorial/security.md#security-native-capabilities-and-your-responsibility process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true; @@ -168,6 +169,37 @@ ipcMain.on('getLaunchTime', event => { launchStartTime = null; } }); + +ipcMain.on( + 'sendNotification', + (e, {payload, pluginNotification, closeAfter}) => { + // notifications can only be sent when app is ready + if (appReady) { + const n = new Notification(payload); + + // Forwarding notification events to renderer process + // https://electronjs.org/docs/api/notification#instance-events + ['show', 'click', 'close', 'reply', 'action'].forEach(eventName => { + n.on(eventName, (event, ...args) => { + e.sender.send( + 'notificationEvent', + eventName, + pluginNotification, + ...args, + ); + }); + }); + n.show(); + + if (closeAfter) { + setTimeout(() => { + n.close(); + }, closeAfter); + } + } + }, +); + // Define custom protocol handler. Deep linking works on packaged versions of the application! app.setAsDefaultProtocolClient('flipper');