From 6e71fa5902bf36c2e976f6ed095d95cd90129283 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 15 Nov 2018 19:01:02 +0100 Subject: [PATCH] Jitsi Push-to-Talk --- electron_app/package.json | 20 ++++++ electron_app/src/electron-main.js | 94 +++++++++++++++++++++++++ package-lock.json | 28 ++++++-- src/i18n/strings/en_EN.json | 3 +- src/vector/platform/ElectronPlatform.js | 42 ++++++++++- 5 files changed, 178 insertions(+), 9 deletions(-) diff --git a/electron_app/package.json b/electron_app/package.json index 8481db77..071dcd66 100644 --- a/electron_app/package.json +++ b/electron_app/package.json @@ -8,7 +8,27 @@ "dependencies": { "auto-launch": "^5.0.1", "electron-window-state": "^4.1.0", + "iohook": "^0.2.4", "minimist": "^1.2.0", "png-to-ico": "^1.0.2" + }, + "iohook": { + "targets": [ + "node-64", + "electron-64" + ], + "platforms": [ + "win32", + "darwin", + "linux" + ], + "arches": [ + "x64", + "ia32" + ] + }, + "cmake-js": { + "runtime": "electron", + "runtimeVersion": "3.0.5" } } diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index 12203e79..452c9631 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -27,6 +27,7 @@ const argv = require('minimist')(process.argv); const {app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater, protocol} = require('electron'); const AutoLaunch = require('auto-launch'); const path = require('path'); +const ioHook = require('iohook'); const tray = require('./tray'); const vectorMenu = require('./vectormenu'); @@ -340,6 +341,13 @@ app.on('ready', () => { return false; } }); + mainWindow.on('blur', () => { + // Stop recording keypresses if Riot loses focus + // Used for Push-To-Talk, keypress recording only triggered when setting + // a global shortcut in Settings + mainWindow.webContents.send('window-blurred'); + stopListeningKeys(); + }); if (process.platform === 'win32') { // Handle forward/backward mouse buttons in Windows @@ -382,6 +390,92 @@ app.on('second-instance', (ev, commandLine, workingDirectory) => { } }); +// Counter for keybindings we have registered +let ioHookTasks = 0; + +// Limit for amount of keybindings that can be +// registered at once. +const keybindingRegistrationLimit = 1; + +// Fires when a global keybinding is being registered +ipcMain.on('register-keybinding', function(ev, keybinding) { + // Prevent registering more than the defined limit + if (ioHookTasks >= keybindingRegistrationLimit) { + ioHookTasks = keybindingRegistrationLimit; + return; + } + + // Start listening for global keyboard shortcuts + if (ioHookTasks <= 0) { + ioHookTasks = 0; + ioHook.start(); + } + ioHookTasks++; + + ioHook.registerShortcut(keybinding.code, () => { + ev.sender.send('keybinding-pressed', keybinding.name); + }, () => { + ev.sender.send('keybinding-released', keybinding.name); + }); +}); + +// Fires when a global keybinding is being unregistered +ipcMain.on('unregister-keybinding', function(ev, keybindingCode) { + // Stop listening for global keyboard shortcuts if we're + // unregistering the last one + if (ioHookTasks <= 1) { + ioHook.stop(); + } + ioHookTasks--; + + ioHook.unregisterShortcutByKeys(keybindingCode); +}); + +// Tell renderer process what key was pressed +// iohook has its own encoding for keys, so we can't just use a +// listener in the renderer process to register iohook shortcuts +let renderProcessID = null; +const reportKeyEvent = function(keyEvent) { + // "this" is the renderer process because we call this method with .bind() + renderProcessID.sender.send('keypress', { + keydown: keyEvent.type == 'keydown', + keycode: keyEvent.keycode, + }); +}; + +// Fires when listening on all keys +// !!Security note: Ensure iohook is only allowed to listen to keybindings +// when the browser window is in focus, else an XSS could lead to keylogging +// Currently, this is achieved by leveraging browserWindow to act on focus loss +ipcMain.on('start-listening-keys', function(ev, keybindingCode) { + // Start recording keypresses + if (ioHookTasks <= 0) { + ioHookTasks = 0; + ioHook.start(); + } + ioHookTasks++; + + renderProcessID = ev; + ioHook.on('keydown', reportKeyEvent); + ioHook.on('keyup', reportKeyEvent); +}); + +const stopListeningKeys = () => { + // Stop recording keypresses + ioHook.off('keydown', reportKeyEvent); + ioHook.off('keyup', reportKeyEvent); +}; + +ipcMain.on('stop-listening-keys', () => { + if (ioHookTasks <= 1) { + ioHookTasks = 1; + ioHook.stop(); + } + ioHookTasks--; + + stopListeningKeys(); +}); + // Set the App User Model ID to match what the squirrel // installer uses for the shortcut icon. // This makes notifications work on windows 8.1 (and is diff --git a/package-lock.json b/package-lock.json index 903d8ccf..4444e149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6627,12 +6627,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6647,17 +6649,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6774,7 +6779,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6786,6 +6792,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6800,6 +6807,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6807,12 +6815,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6831,6 +6841,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6911,7 +6922,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6923,6 +6935,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7044,6 +7057,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9c467cfc..4840fccb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -17,5 +17,6 @@ "Explore rooms": "Explore rooms", "Room Directory": "Room Directory", "Search the room directory": "Search the room directory", - "Get started with some tips from Riot Bot!": "Get started with some tips from Riot Bot!" + "Get started with some tips from Riot Bot!": "Get started with some tips from Riot Bot!", + "Push-to-Talk": "Push-to-Talk" } diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js index 6e600cfc..c81f3687 100644 --- a/src/vector/platform/ElectronPlatform.js +++ b/src/vector/platform/ElectronPlatform.js @@ -25,6 +25,7 @@ import Promise from 'bluebird'; import rageshake from 'matrix-react-sdk/lib/rageshake/rageshake'; const ipcRenderer = window.ipcRenderer; +var globalKeybindings = {}; function platformFriendlyName(): string { // used to use window.process but the same info is available here @@ -99,6 +100,25 @@ export default class ElectronPlatform extends VectorBasePlatform { this.startUpdateCheck = this.startUpdateCheck.bind(this); this.stopUpdateCheck = this.stopUpdateCheck.bind(this); + + ipcRenderer.on('keybinding-pressed', (event, keybindName) => { + // Prevent holding down a shortcut meaning multiple presses + if (globalKeybindings[keybindName].pressed) { + return; + } + globalKeybindings[keybindName].pressed = true; + + // Run the callback + globalKeybindings[keybindName].callback(); + }); + + ipcRenderer.on('keybinding-released', (event, keybindName) => { + // Keybinding is no longer pressed + globalKeybindings[keybindName].pressed = false; + + // Run the callback + globalKeybindings[keybindName].releaseCallback(); + }); } async onUpdateDownloaded(ev, updateInfo) { @@ -198,6 +218,26 @@ export default class ElectronPlatform extends VectorBasePlatform { ipcRenderer.send('check_updates'); } + addGlobalKeybinding(keybindName: string, keybindCode: string, callback: () => void, releaseCallback: () => void) { + // Add a keybinding that works even when the app is minimized + const keybinding = {name: keybindName, code: keybindCode}; + + ipcRenderer.send('register-keybinding', keybinding); + globalKeybindings[keybindName] = {callback, releaseCallback}; + + console.warn("Adding global keybinding:", keybindName, "with code:", keybindCode); + } + + removeGlobalKeybinding(keybindName: string, keybindCode: string) { + // Unbind a global keybinding + ipcRenderer.send('unregister-keybinding', keybindCode); + + // Remove the callback + delete globalKeybindings[keybindName]; + + console.warn("Removing global keybinding:", keybindName); + } + installUpdate() { // IPC to the main process to install the update, since quitAndInstall // doesn't fire the before-quit event so the main process needs to know @@ -232,7 +272,7 @@ export default class ElectronPlatform extends VectorBasePlatform { const ipcCallId = ++this._nextIpcCallId; return new Promise((resolve, reject) => { this._pendingIpcCalls[ipcCallId] = {resolve, reject}; - window.ipcRenderer.send('ipcCall', {id: ipcCallId, name, args}); + ipcRenderer.send('ipcCall', {id: ipcCallId, name, args}); // Maybe add a timeout to these? Probably not necessary. }); }