Add screensharing behind a feature flag
This commit is contained in:
parent
7c7f7ee5a0
commit
ceffc2380c
49 changed files with 2044 additions and 164 deletions
|
@ -1729,6 +1729,10 @@ Signal Desktop makes use of the following open source projects.
|
||||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
## mac-screen-capture-permissions
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
|
||||||
## memoizee
|
## memoizee
|
||||||
|
|
||||||
ISC License
|
ISC License
|
||||||
|
|
|
@ -691,6 +691,10 @@
|
||||||
"message": "About Signal Desktop",
|
"message": "About Signal Desktop",
|
||||||
"description": "Item under the Help menu, which opens a small about window"
|
"description": "Item under the Help menu, which opens a small about window"
|
||||||
},
|
},
|
||||||
|
"screenShareWindow": {
|
||||||
|
"message": "Sharing screen",
|
||||||
|
"description": "Title for screen sharing window"
|
||||||
|
},
|
||||||
"speech": {
|
"speech": {
|
||||||
"message": "Speech",
|
"message": "Speech",
|
||||||
"description": "Item under the Edit menu, with 'start/stop speaking' items below it"
|
"description": "Item under the Edit menu, with 'start/stop speaking' items below it"
|
||||||
|
@ -1249,6 +1253,18 @@
|
||||||
"message": "Unmute mic",
|
"message": "Unmute mic",
|
||||||
"description": "Button tooltip label for turning on the microphone"
|
"description": "Button tooltip label for turning on the microphone"
|
||||||
},
|
},
|
||||||
|
"calling__button--presenting-disabled": {
|
||||||
|
"message": "Presenting disabled",
|
||||||
|
"description": "Button tooltip label for when screen sharing is disabled"
|
||||||
|
},
|
||||||
|
"calling__button--presenting-on": {
|
||||||
|
"message": "Start presenting",
|
||||||
|
"description": "Button tooltip label for starting to share screen"
|
||||||
|
},
|
||||||
|
"calling__button--presenting-off": {
|
||||||
|
"message": "Stop presenting",
|
||||||
|
"description": "Button tooltip label for stopping screen sharing"
|
||||||
|
},
|
||||||
"calling__your-video-is-off": {
|
"calling__your-video-is-off": {
|
||||||
"message": "Your camera is off",
|
"message": "Your camera is off",
|
||||||
"description": "Label in the calling lobby indicating that your camera is off"
|
"description": "Label in the calling lobby indicating that your camera is off"
|
||||||
|
@ -1361,6 +1377,84 @@
|
||||||
"message": "Scroll down",
|
"message": "Scroll down",
|
||||||
"description": "Label for the \"scroll down\" button in a call's overflow area"
|
"description": "Label for the \"scroll down\" button in a call's overflow area"
|
||||||
},
|
},
|
||||||
|
"calling__presenting--notification-title": {
|
||||||
|
"message": "You're presenting to everyone.",
|
||||||
|
"description": "Title for the share screen notification"
|
||||||
|
},
|
||||||
|
"calling__presenting--notification-body": {
|
||||||
|
"message": "Click here to return to the call when you're ready to stop presenting.",
|
||||||
|
"description": "Body text for the share screen notification"
|
||||||
|
},
|
||||||
|
"calling__presenting--info": {
|
||||||
|
"message": "Signal is sharing $window$.",
|
||||||
|
"description": "Text that appears in the screen sharing controller to inform person that they are presenting",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Application"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"calling__presenting--stop": {
|
||||||
|
"message": "Stop sharing",
|
||||||
|
"description": "Button for stopping screen sharing"
|
||||||
|
},
|
||||||
|
"calling__presenting--you-stopped": {
|
||||||
|
"message": "You stopped presenting",
|
||||||
|
"description": "Toast that appears when someone stops presenting"
|
||||||
|
},
|
||||||
|
"calling__presenting--person-ongoing": {
|
||||||
|
"message": "$name$ is presenting",
|
||||||
|
"description": "Title of call when someone is presenting",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Maddie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"calling__presenting--person-stopped": {
|
||||||
|
"message": "$name$ stopped presenting",
|
||||||
|
"description": "Toast that appears when someone stops presenting",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Maddie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"calling__presenting--permission-title": {
|
||||||
|
"message": "Permission needed",
|
||||||
|
"description": "Shown as the title for the modal that requests screen recording permissions"
|
||||||
|
},
|
||||||
|
"calling__presenting--macos-permission-description": {
|
||||||
|
"message": "On an Apple Mac computer using macOS Catalina version 10.15 or later, Signal needs permission to access your computer's screen recording.",
|
||||||
|
"description": "Shown as the description for the modal that requests screen recording permissions"
|
||||||
|
},
|
||||||
|
"calling__presenting--permission-instruction-step1": {
|
||||||
|
"message": "Go to System Preferences and then click Security & Privacy.",
|
||||||
|
"description": "Shown as the description for the modal that requests screen recording permissions"
|
||||||
|
},
|
||||||
|
"calling__presenting--permission-instruction-step2": {
|
||||||
|
"message": "Click Privacy.",
|
||||||
|
"description": "Shown as the description for the modal that requests screen recording permissions"
|
||||||
|
},
|
||||||
|
"calling__presenting--permission-instruction-step3": {
|
||||||
|
"message": "On the left, click Screen Recording.",
|
||||||
|
"description": "Shown as the description for the modal that requests screen recording permissions"
|
||||||
|
},
|
||||||
|
"calling__presenting--permission-instruction-step4": {
|
||||||
|
"message": "On the right, check the Signal box.",
|
||||||
|
"description": "Shown as the description for the modal that requests screen recording permissions"
|
||||||
|
},
|
||||||
|
"calling__presenting--permission-open": {
|
||||||
|
"message": "Open System Preferences",
|
||||||
|
"description": "The button that opens your system preferences for the needs screen record permissions modal"
|
||||||
|
},
|
||||||
|
"calling__presenting--permission-cancel": {
|
||||||
|
"message": "Dismiss",
|
||||||
|
"description": "The cancel button for the needs screen record permissions modal"
|
||||||
|
},
|
||||||
"alwaysRelayCallsDescription": {
|
"alwaysRelayCallsDescription": {
|
||||||
"message": "Always relay calls",
|
"message": "Always relay calls",
|
||||||
"description": "Description of the always relay calls setting"
|
"description": "Description of the always relay calls setting"
|
||||||
|
@ -3240,6 +3334,22 @@
|
||||||
"message": "Leave call",
|
"message": "Leave call",
|
||||||
"description": "Title for hang up button"
|
"description": "Title for hang up button"
|
||||||
},
|
},
|
||||||
|
"calling__SelectPresentingSourcesModal--title": {
|
||||||
|
"message": "Share your screen",
|
||||||
|
"description": "Title for the select your screen sharing sources modal"
|
||||||
|
},
|
||||||
|
"calling__SelectPresentingSourcesModal--confirm": {
|
||||||
|
"message": "Share screen",
|
||||||
|
"description": "Confirm button for sharing screen modal"
|
||||||
|
},
|
||||||
|
"calling__SelectPresentingSourcesModal--entireScreen": {
|
||||||
|
"message": "Entire screen",
|
||||||
|
"description": "Title for the select your screen sharing sources modal"
|
||||||
|
},
|
||||||
|
"calling__SelectPresentingSourcesModal--window": {
|
||||||
|
"message": "A window",
|
||||||
|
"description": "Title for the select your screen sharing sources modal"
|
||||||
|
},
|
||||||
"callingDeviceSelection__label--video": {
|
"callingDeviceSelection__label--video": {
|
||||||
"message": "Video",
|
"message": "Video",
|
||||||
"description": "Label for video input selector"
|
"description": "Label for video input selector"
|
||||||
|
|
|
@ -23,18 +23,29 @@ function _createPermissionHandler(userConfig) {
|
||||||
return (webContents, permission, callback, details) => {
|
return (webContents, permission, callback, details) => {
|
||||||
// We default 'media' permission to false, but the user can override that for
|
// We default 'media' permission to false, but the user can override that for
|
||||||
// the microphone and camera.
|
// the microphone and camera.
|
||||||
if (
|
if (permission === 'media') {
|
||||||
permission === 'media' &&
|
if (
|
||||||
details.mediaTypes.includes('audio') &&
|
details.mediaTypes.includes('audio') ||
|
||||||
userConfig.get('mediaPermissions')
|
details.mediaTypes.includes('video')
|
||||||
) {
|
) {
|
||||||
return callback(true);
|
if (
|
||||||
}
|
details.mediaTypes.includes('audio') &&
|
||||||
if (
|
userConfig.get('mediaPermissions')
|
||||||
permission === 'media' &&
|
) {
|
||||||
details.mediaTypes.includes('video') &&
|
return callback(true);
|
||||||
userConfig.get('mediaCameraPermissions')
|
}
|
||||||
) {
|
if (
|
||||||
|
details.mediaTypes.includes('video') &&
|
||||||
|
userConfig.get('mediaCameraPermissions')
|
||||||
|
) {
|
||||||
|
return callback(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it doesn't have 'video' or 'audio', it's probably screenshare.
|
||||||
|
// TODO: DESKTOP-1611
|
||||||
return callback(true);
|
return callback(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1
images/icons/v2/share-screen-solid-28.svg
Normal file
1
images/icons/v2/share-screen-solid-28.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><path d="m23 4.5h-18a3 3 0 0 0 -3 3v13a3 3 0 0 0 3 3h18a3 3 0 0 0 3-3v-13a3 3 0 0 0 -3-3zm-5 10-2.6-2.6-.65-.9v9h-1.5v-9l-.62.93-2.63 2.6-1-1.06 5-5 5 5z"/></svg>
|
After Width: | Height: | Size: 223 B |
71
main.js
71
main.js
|
@ -753,6 +753,61 @@ function setupAsStandalone() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let screenShareWindow;
|
||||||
|
function showScreenShareWindow(sourceName) {
|
||||||
|
if (screenShareWindow) {
|
||||||
|
screenShareWindow.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = 480;
|
||||||
|
|
||||||
|
const { screen } = electron;
|
||||||
|
const display = screen.getPrimaryDisplay();
|
||||||
|
const options = {
|
||||||
|
alwaysOnTop: true,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
backgroundColor: '#2e2e2e',
|
||||||
|
darkTheme: true,
|
||||||
|
frame: false,
|
||||||
|
fullscreenable: false,
|
||||||
|
height: 44,
|
||||||
|
maximizable: false,
|
||||||
|
minimizable: false,
|
||||||
|
resizable: false,
|
||||||
|
show: false,
|
||||||
|
title: locale.messages.screenShareWindow.message,
|
||||||
|
width,
|
||||||
|
webPreferences: {
|
||||||
|
...defaultWebPrefs,
|
||||||
|
nodeIntegration: false,
|
||||||
|
nodeIntegrationInWorker: false,
|
||||||
|
contextIsolation: false,
|
||||||
|
preload: path.join(__dirname, 'screenShare_preload.js'),
|
||||||
|
},
|
||||||
|
x: Math.floor(display.size.width / 2) - width / 2,
|
||||||
|
y: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
screenShareWindow = new BrowserWindow(options);
|
||||||
|
|
||||||
|
handleCommonWindowEvents(screenShareWindow);
|
||||||
|
|
||||||
|
screenShareWindow.loadURL(prepareFileUrl([__dirname, 'screenShare.html']));
|
||||||
|
|
||||||
|
screenShareWindow.on('closed', () => {
|
||||||
|
screenShareWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
screenShareWindow.once('ready-to-show', () => {
|
||||||
|
screenShareWindow.show();
|
||||||
|
screenShareWindow.webContents.send(
|
||||||
|
'render-screen-sharing-controller',
|
||||||
|
sourceName
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let aboutWindow;
|
let aboutWindow;
|
||||||
function showAbout() {
|
function showAbout() {
|
||||||
if (aboutWindow) {
|
if (aboutWindow) {
|
||||||
|
@ -1503,6 +1558,22 @@ ipc.on('close-about', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.on('close-screen-share-controller', () => {
|
||||||
|
if (screenShareWindow) {
|
||||||
|
screenShareWindow.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.on('stop-screen-share', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('stop-screen-share');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.on('show-screen-share', (event, sourceName) => {
|
||||||
|
showScreenShareWindow(sourceName);
|
||||||
|
});
|
||||||
|
|
||||||
ipc.on('update-tray-icon', (event, unreadCount) => {
|
ipc.on('update-tray-icon', (event, unreadCount) => {
|
||||||
if (tray) {
|
if (tray) {
|
||||||
tray.updateIcon(unreadCount);
|
tray.updateIcon(unreadCount);
|
||||||
|
|
|
@ -106,6 +106,7 @@
|
||||||
"linkify-it": "2.2.0",
|
"linkify-it": "2.2.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lru-cache": "6.0.0",
|
"lru-cache": "6.0.0",
|
||||||
|
"mac-screen-capture-permissions": "2.0.0",
|
||||||
"memoizee": "0.4.14",
|
"memoizee": "0.4.14",
|
||||||
"mkdirp": "0.5.2",
|
"mkdirp": "0.5.2",
|
||||||
"moment": "2.29.1",
|
"moment": "2.29.1",
|
||||||
|
@ -147,7 +148,7 @@
|
||||||
"redux-ts-utils": "3.2.2",
|
"redux-ts-utils": "3.2.2",
|
||||||
"reselect": "4.0.0",
|
"reselect": "4.0.0",
|
||||||
"rimraf": "2.6.2",
|
"rimraf": "2.6.2",
|
||||||
"ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#b43c6b728d62b6d386d95705e128f32f44edb650",
|
"ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#17b22fc9d47605867608193202c54be06bce6f56",
|
||||||
"rotating-file-stream": "2.1.5",
|
"rotating-file-stream": "2.1.5",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"sanitize.css": "11.0.0",
|
"sanitize.css": "11.0.0",
|
||||||
|
@ -302,7 +303,8 @@
|
||||||
"asarUnpack": [
|
"asarUnpack": [
|
||||||
"**/*.node",
|
"**/*.node",
|
||||||
"node_modules/zkgroup/libzkgroup.*",
|
"node_modules/zkgroup/libzkgroup.*",
|
||||||
"node_modules/@signalapp/signal-client/build/*.node"
|
"node_modules/@signalapp/signal-client/build/*.node",
|
||||||
|
"node_modules/mac-screen-capture-permissions/build/Release/*.node"
|
||||||
],
|
],
|
||||||
"artifactName": "${name}-mac-${version}.${ext}",
|
"artifactName": "${name}-mac-${version}.${ext}",
|
||||||
"category": "public.app-category.social-networking",
|
"category": "public.app-category.social-networking",
|
||||||
|
@ -392,6 +394,7 @@
|
||||||
"config/local-${env.SIGNAL_ENV}.json",
|
"config/local-${env.SIGNAL_ENV}.json",
|
||||||
"background.html",
|
"background.html",
|
||||||
"about.html",
|
"about.html",
|
||||||
|
"screenShare.html",
|
||||||
"settings.html",
|
"settings.html",
|
||||||
"permissions_popup.html",
|
"permissions_popup.html",
|
||||||
"debug_log.html",
|
"debug_log.html",
|
||||||
|
@ -408,6 +411,7 @@
|
||||||
"preload.bundle.js",
|
"preload.bundle.js",
|
||||||
"preload_utils.js",
|
"preload_utils.js",
|
||||||
"about_preload.js",
|
"about_preload.js",
|
||||||
|
"screenShare_preload.js",
|
||||||
"settings_preload.js",
|
"settings_preload.js",
|
||||||
"permissions_popup_preload.js",
|
"permissions_popup_preload.js",
|
||||||
"debug_log_preload.js",
|
"debug_log_preload.js",
|
||||||
|
@ -448,6 +452,7 @@
|
||||||
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
||||||
"node_modules/@signalapp/signal-client/build/*${platform}*.node",
|
"node_modules/@signalapp/signal-client/build/*${platform}*.node",
|
||||||
"node_modules/ringrtc/build/${platform}/**",
|
"node_modules/ringrtc/build/${platform}/**",
|
||||||
|
"node_modules/mac-screen-capture-permissions/build/Release/*.node",
|
||||||
"!**/node_modules/ffi-napi/deps",
|
"!**/node_modules/ffi-napi/deps",
|
||||||
"!**/node_modules/react-dom/*/*.development.js",
|
"!**/node_modules/react-dom/*/*.development.js",
|
||||||
"!node_modules/.cache"
|
"!node_modules/.cache"
|
||||||
|
|
22
patches/electron-util+0.13.1.patch
Normal file
22
patches/electron-util+0.13.1.patch
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
diff --git a/node_modules/electron-util/index.d.ts b/node_modules/electron-util/index.d.ts
|
||||||
|
index 8d493d5..3408e21 100644
|
||||||
|
--- a/node_modules/electron-util/index.d.ts
|
||||||
|
+++ b/node_modules/electron-util/index.d.ts
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
/// <reference lib="dom"/>
|
||||||
|
/// <reference types="electron"/>
|
||||||
|
/// <reference types="node"/>
|
||||||
|
-import {AllElectron, Remote, BrowserWindow, Size, Rectangle, Session, MenuItemConstructorOptions, MenuItem} from 'electron';
|
||||||
|
+import {RemoteMainInterface, BrowserWindow, Size, Rectangle, Session, MenuItemConstructorOptions, MenuItem} from 'electron';
|
||||||
|
import {Options as NewGithubIssueUrlOptions} from 'new-github-issue-url';
|
||||||
|
import {RequireAtLeastOne} from 'type-fest';
|
||||||
|
|
||||||
|
@@ -14,7 +14,7 @@ Access the Electron APIs in both the main and renderer process without having to
|
||||||
|
api.app.quit(); // The `app` API is usually only available in the main process.
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
-export const api: AllElectron | Remote;
|
||||||
|
+export const api: RemoteMainInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Check for various things.
|
13
patches/mac-screen-capture-permissions+2.0.0.patch
Normal file
13
patches/mac-screen-capture-permissions+2.0.0.patch
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
diff --git a/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m b/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
|
||||||
|
index d9d6a00..78fa83f 100644
|
||||||
|
--- a/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
|
||||||
|
+++ b/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
|
||||||
|
@@ -2,6 +2,8 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <node_api.h>
|
||||||
|
|
||||||
|
+CG_EXTERN bool CGPreflightScreenCaptureAccess(void) CG_AVAILABLE_STARTING(10.15);
|
||||||
|
+
|
||||||
|
static napi_value hasPermissions(napi_env env, napi_callback_info info) {
|
||||||
|
napi_status status;
|
||||||
|
bool hasPermissions;
|
22
screenShare.html
Normal file
22
screenShare.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<!-- Copyright 2021 Signal Messenger, LLC -->
|
||||||
|
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'none';
|
||||||
|
font-src 'self';
|
||||||
|
img-src 'self' blob: data:;
|
||||||
|
media-src 'self' blob:;
|
||||||
|
object-src 'none';
|
||||||
|
script-src 'self';
|
||||||
|
style-src 'self' 'unsafe-inline';"
|
||||||
|
>
|
||||||
|
<link href="node_modules/sanitize.css/sanitize.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type='application/javascript' src='ts/windows/screenShare.js'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
59
screenShare_preload.js
Normal file
59
screenShare_preload.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* global window */
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const ReactDOM = require('react-dom');
|
||||||
|
const url = require('url');
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
const i18n = require('./js/modules/i18n');
|
||||||
|
const {
|
||||||
|
getEnvironment,
|
||||||
|
setEnvironment,
|
||||||
|
parseEnvironment,
|
||||||
|
} = require('./ts/environment');
|
||||||
|
const {
|
||||||
|
CallingScreenSharingController,
|
||||||
|
} = require('./ts/components/CallingScreenSharingController');
|
||||||
|
|
||||||
|
const config = url.parse(window.location.toString(), true).query;
|
||||||
|
const { locale } = config;
|
||||||
|
const localeMessages = ipcRenderer.sendSync('locale-data');
|
||||||
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
||||||
|
window.React = React;
|
||||||
|
window.ReactDOM = ReactDOM;
|
||||||
|
window.getAppInstance = () => config.appInstance;
|
||||||
|
window.getEnvironment = getEnvironment;
|
||||||
|
window.getVersion = () => config.version;
|
||||||
|
window.i18n = i18n.setup(locale, localeMessages);
|
||||||
|
|
||||||
|
let renderComponent;
|
||||||
|
window.registerScreenShareControllerRenderer = f => {
|
||||||
|
renderComponent = f;
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderScreenSharingController(event, presentedSourceName) {
|
||||||
|
if (!renderComponent) {
|
||||||
|
setTimeout(renderScreenSharingController, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
i18n: window.i18n,
|
||||||
|
onCloseController: () => ipcRenderer.send('close-screen-share-controller'),
|
||||||
|
onStopSharing: () => ipcRenderer.send('stop-screen-share'),
|
||||||
|
presentedSourceName,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderComponent(CallingScreenSharingController, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.once(
|
||||||
|
'render-screen-sharing-controller',
|
||||||
|
renderScreenSharingController
|
||||||
|
);
|
||||||
|
|
||||||
|
require('./ts/logging/set_up_renderer_logging').initialize();
|
BIN
sounds/navigation_selection-complete-celebration.ogg
Executable file
BIN
sounds/navigation_selection-complete-celebration.ogg
Executable file
Binary file not shown.
|
@ -5989,6 +5989,19 @@ button.module-image__border-overlay:focus {
|
||||||
$color-white
|
$color-white
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--presenting {
|
||||||
|
$icon: '../images/icons/v2/share-screen-solid-28.svg';
|
||||||
|
&--on {
|
||||||
|
@include calling-button-icon-on($icon);
|
||||||
|
}
|
||||||
|
&--off {
|
||||||
|
@include calling-button-icon-off($icon);
|
||||||
|
}
|
||||||
|
&--disabled {
|
||||||
|
@include calling-button-icon-disabled($icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes module-ongoing-call__controls--fade-in {
|
@keyframes module-ongoing-call__controls--fade-in {
|
||||||
|
@ -6286,6 +6299,10 @@ button.module-image__border-overlay:focus {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transform: rotateY(180deg);
|
transform: rotateY(180deg);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&--presenting {
|
||||||
|
transform: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--audio-muted::before {
|
&--audio-muted::before {
|
||||||
|
@ -6323,6 +6340,7 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__toast {
|
&__toast {
|
||||||
|
@include button-reset();
|
||||||
@include font-body-1-bold;
|
@include font-body-1-bold;
|
||||||
background-color: $color-gray-75;
|
background-color: $color-gray-75;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -6649,6 +6667,17 @@ button.module-image__border-overlay:focus {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__presenting {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/share-screen-solid-28.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 18px;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-call-need-permission-screen {
|
.module-call-need-permission-screen {
|
||||||
|
|
35
stylesheets/components/CallingScreenSharingController.scss
Normal file
35
stylesheets/components/CallingScreenSharingController.scss
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.module-CallingScreenSharingController {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 16px;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
@include font-body-2;
|
||||||
|
color: $color-gray-05;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 212px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close {
|
||||||
|
@include button-reset;
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 12px;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.module-CallingSelectPresentingSourcesModal {
|
||||||
|
// specificity
|
||||||
|
&.module-Modal {
|
||||||
|
max-width: 665px;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
background-color: $color-gray-95;
|
||||||
|
bottom: 0;
|
||||||
|
margin-left: -16px;
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 16px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sources {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-left: -6px;
|
||||||
|
margin-right: -6px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__source {
|
||||||
|
@include button-reset();
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid $color-gray-60;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
width: 200px;
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background-color: $ultramarine-ui-dark;
|
||||||
|
border: 1px solid $ultramarine-ui-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__screenshot {
|
||||||
|
max-height: 102px;
|
||||||
|
max-width: 184px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
&--container {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--text {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,8 @@
|
||||||
@import './components/Avatar.scss';
|
@import './components/Avatar.scss';
|
||||||
@import './components/AvatarInput.scss';
|
@import './components/AvatarInput.scss';
|
||||||
@import './components/Button.scss';
|
@import './components/Button.scss';
|
||||||
|
@import './components/CallingScreenSharingController.scss';
|
||||||
|
@import './components/CallingSelectPresentingSourcesModal.scss';
|
||||||
@import './components/ContactPill.scss';
|
@import './components/ContactPill.scss';
|
||||||
@import './components/ContactPills.scss';
|
@import './components/ContactPills.scss';
|
||||||
@import './components/ContactSpoofingReviewDialog.scss';
|
@import './components/ContactSpoofingReviewDialog.scss';
|
||||||
|
|
|
@ -11,8 +11,10 @@ export type ConfigKeyType =
|
||||||
| 'desktop.gv2'
|
| 'desktop.gv2'
|
||||||
| 'desktop.mandatoryProfileSharing'
|
| 'desktop.mandatoryProfileSharing'
|
||||||
| 'desktop.messageRequests'
|
| 'desktop.messageRequests'
|
||||||
|
| 'desktop.screensharing'
|
||||||
| 'desktop.storage'
|
| 'desktop.storage'
|
||||||
| 'desktop.storageWrite3'
|
| 'desktop.storageWrite3'
|
||||||
|
| 'desktop.worksAtSignal'
|
||||||
| 'global.groupsv2.maxGroupSize'
|
| 'global.groupsv2.maxGroupSize'
|
||||||
| 'global.groupsv2.groupSizeHardLimit';
|
| 'global.groupsv2.groupSizeHardLimit';
|
||||||
type ConfigValueType = {
|
type ConfigValueType = {
|
||||||
|
|
|
@ -68,6 +68,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
declineCall: action('decline-call'),
|
declineCall: action('decline-call'),
|
||||||
getGroupCallVideoFrameSource: (_: string, demuxId: number) =>
|
getGroupCallVideoFrameSource: (_: string, demuxId: number) =>
|
||||||
fakeGetGroupCallVideoFrameSource(demuxId),
|
fakeGetGroupCallVideoFrameSource(demuxId),
|
||||||
|
getPresentingSources: action('get-presenting-sources'),
|
||||||
hangUp: action('hang-up'),
|
hangUp: action('hang-up'),
|
||||||
i18n,
|
i18n,
|
||||||
keyChangeOk: action('key-change-ok'),
|
keyChangeOk: action('key-change-ok'),
|
||||||
|
@ -78,16 +79,21 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
}),
|
}),
|
||||||
uuid: 'cb0dd0c8-7393-41e9-a0aa-d631c4109541',
|
uuid: 'cb0dd0c8-7393-41e9-a0aa-d631c4109541',
|
||||||
},
|
},
|
||||||
|
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||||
renderDeviceSelection: () => <div />,
|
renderDeviceSelection: () => <div />,
|
||||||
renderSafetyNumberViewer: (_: SafetyNumberViewerProps) => <div />,
|
renderSafetyNumberViewer: (_: SafetyNumberViewerProps) => <div />,
|
||||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
setLocalPreview: action('set-local-preview'),
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
|
setPresenting: action('toggle-presenting'),
|
||||||
setRendererCanvas: action('set-renderer-canvas'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
startCall: action('start-call'),
|
startCall: action('start-call'),
|
||||||
toggleParticipants: action('toggle-participants'),
|
toggleParticipants: action('toggle-participants'),
|
||||||
togglePip: action('toggle-pip'),
|
togglePip: action('toggle-pip'),
|
||||||
|
toggleScreenRecordingPermissionsDialog: action(
|
||||||
|
'toggle-screen-recording-permissions-dialog'
|
||||||
|
),
|
||||||
toggleSettings: action('toggle-settings'),
|
toggleSettings: action('toggle-settings'),
|
||||||
toggleSpeakerView: action('toggle-speaker-view'),
|
toggleSpeakerView: action('toggle-speaker-view'),
|
||||||
});
|
});
|
||||||
|
@ -104,7 +110,9 @@ story.add('Ongoing Direct Call', () => (
|
||||||
callMode: CallMode.Direct,
|
callMode: CallMode.Direct,
|
||||||
callState: CallState.Accepted,
|
callState: CallState.Accepted,
|
||||||
peekedParticipants: [],
|
peekedParticipants: [],
|
||||||
remoteParticipants: [{ hasRemoteVideo: true }],
|
remoteParticipants: [
|
||||||
|
{ hasRemoteVideo: true, presenting: false, title: 'Remy' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -148,7 +156,9 @@ story.add('Call Request Needed', () => (
|
||||||
callMode: CallMode.Direct,
|
callMode: CallMode.Direct,
|
||||||
callState: CallState.Accepted,
|
callState: CallState.Accepted,
|
||||||
peekedParticipants: [],
|
peekedParticipants: [],
|
||||||
remoteParticipants: [{ hasRemoteVideo: true }],
|
remoteParticipants: [
|
||||||
|
{ hasRemoteVideo: true, presenting: false, title: 'Mike' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { CallNeedPermissionScreen } from './CallNeedPermissionScreen';
|
||||||
import { CallScreen } from './CallScreen';
|
import { CallScreen } from './CallScreen';
|
||||||
import { CallingLobby } from './CallingLobby';
|
import { CallingLobby } from './CallingLobby';
|
||||||
import { CallingParticipantsList } from './CallingParticipantsList';
|
import { CallingParticipantsList } from './CallingParticipantsList';
|
||||||
|
import { CallingSelectPresentingSourcesModal } from './CallingSelectPresentingSourcesModal';
|
||||||
import { CallingPip } from './CallingPip';
|
import { CallingPip } from './CallingPip';
|
||||||
import { IncomingCallBar } from './IncomingCallBar';
|
import { IncomingCallBar } from './IncomingCallBar';
|
||||||
import {
|
import {
|
||||||
|
@ -19,6 +20,7 @@ import {
|
||||||
CallState,
|
CallState,
|
||||||
GroupCallJoinState,
|
GroupCallJoinState,
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
|
PresentedSource,
|
||||||
VideoFrameSource,
|
VideoFrameSource,
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
@ -52,6 +54,7 @@ export type PropsType = {
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
demuxId: number
|
demuxId: number
|
||||||
) => VideoFrameSource;
|
) => VideoFrameSource;
|
||||||
|
getPresentingSources: () => void;
|
||||||
incomingCall?: {
|
incomingCall?: {
|
||||||
call: DirectCallStateType;
|
call: DirectCallStateType;
|
||||||
conversation: ConversationType;
|
conversation: ConversationType;
|
||||||
|
@ -65,13 +68,16 @@ export type PropsType = {
|
||||||
declineCall: (_: DeclineCallType) => void;
|
declineCall: (_: DeclineCallType) => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
me: MeType;
|
me: MeType;
|
||||||
|
openSystemPreferencesAction: () => unknown;
|
||||||
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
||||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
|
setPresenting: (_?: PresentedSource) => void;
|
||||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
hangUp: (_: HangUpType) => void;
|
hangUp: (_: HangUpType) => void;
|
||||||
togglePip: () => void;
|
togglePip: () => void;
|
||||||
|
toggleScreenRecordingPermissionsDialog: () => unknown;
|
||||||
toggleSettings: () => void;
|
toggleSettings: () => void;
|
||||||
toggleSpeakerView: () => void;
|
toggleSpeakerView: () => void;
|
||||||
};
|
};
|
||||||
|
@ -89,17 +95,21 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
i18n,
|
i18n,
|
||||||
keyChangeOk,
|
keyChangeOk,
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
|
getPresentingSources,
|
||||||
me,
|
me,
|
||||||
|
openSystemPreferencesAction,
|
||||||
renderDeviceSelection,
|
renderDeviceSelection,
|
||||||
renderSafetyNumberViewer,
|
renderSafetyNumberViewer,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
|
setPresenting,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
startCall,
|
startCall,
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
togglePip,
|
togglePip,
|
||||||
|
toggleScreenRecordingPermissionsDialog,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
toggleSpeakerView,
|
toggleSpeakerView,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -110,6 +120,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
joinedAt,
|
joinedAt,
|
||||||
peekedParticipants,
|
peekedParticipants,
|
||||||
pip,
|
pip,
|
||||||
|
presentingSourcesAvailable,
|
||||||
settingsDialogOpen,
|
settingsDialogOpen,
|
||||||
showParticipantsList,
|
showParticipantsList,
|
||||||
} = activeCall;
|
} = activeCall;
|
||||||
|
@ -238,13 +249,15 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
? [
|
? [
|
||||||
...activeCall.remoteParticipants.map(participant => ({
|
...activeCall.remoteParticipants.map(participant => ({
|
||||||
...participant,
|
...participant,
|
||||||
hasAudio: participant.hasRemoteAudio,
|
hasRemoteAudio: participant.hasRemoteAudio,
|
||||||
hasVideo: participant.hasRemoteVideo,
|
hasRemoteVideo: participant.hasRemoteVideo,
|
||||||
|
presenting: participant.presenting,
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
...me,
|
...me,
|
||||||
hasAudio: hasLocalAudio,
|
hasRemoteAudio: hasLocalAudio,
|
||||||
hasVideo: hasLocalVideo,
|
hasRemoteVideo: hasLocalVideo,
|
||||||
|
presenting: Boolean(activeCall.presentingSource),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
@ -253,22 +266,35 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
<>
|
<>
|
||||||
<CallScreen
|
<CallScreen
|
||||||
activeCall={activeCall}
|
activeCall={activeCall}
|
||||||
|
getPresentingSources={getPresentingSources}
|
||||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
||||||
hangUp={hangUp}
|
hangUp={hangUp}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
joinedAt={joinedAt}
|
joinedAt={joinedAt}
|
||||||
me={me}
|
me={me}
|
||||||
|
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||||
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
|
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
|
||||||
setLocalPreview={setLocalPreview}
|
setLocalPreview={setLocalPreview}
|
||||||
setRendererCanvas={setRendererCanvas}
|
setRendererCanvas={setRendererCanvas}
|
||||||
setLocalAudio={setLocalAudio}
|
setLocalAudio={setLocalAudio}
|
||||||
setLocalVideo={setLocalVideo}
|
setLocalVideo={setLocalVideo}
|
||||||
|
setPresenting={setPresenting}
|
||||||
stickyControls={showParticipantsList}
|
stickyControls={showParticipantsList}
|
||||||
|
toggleScreenRecordingPermissionsDialog={
|
||||||
|
toggleScreenRecordingPermissionsDialog
|
||||||
|
}
|
||||||
toggleParticipants={toggleParticipants}
|
toggleParticipants={toggleParticipants}
|
||||||
togglePip={togglePip}
|
togglePip={togglePip}
|
||||||
toggleSettings={toggleSettings}
|
toggleSettings={toggleSettings}
|
||||||
toggleSpeakerView={toggleSpeakerView}
|
toggleSpeakerView={toggleSpeakerView}
|
||||||
/>
|
/>
|
||||||
|
{presentingSourcesAvailable && presentingSourcesAvailable.length ? (
|
||||||
|
<CallingSelectPresentingSourcesModal
|
||||||
|
i18n={i18n}
|
||||||
|
presentingSourcesAvailable={presentingSourcesAvailable}
|
||||||
|
setPresenting={setPresenting}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{settingsDialogOpen && renderDeviceSelection()}
|
{settingsDialogOpen && renderDeviceSelection()}
|
||||||
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
|
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
|
||||||
<CallingParticipantsList
|
<CallingParticipantsList
|
||||||
|
|
|
@ -74,10 +74,14 @@ const createActiveDirectCallProp = (
|
||||||
'hasRemoteVideo',
|
'hasRemoteVideo',
|
||||||
Boolean(overrideProps.hasRemoteVideo)
|
Boolean(overrideProps.hasRemoteVideo)
|
||||||
),
|
),
|
||||||
|
presenting: false,
|
||||||
|
title: 'test',
|
||||||
},
|
},
|
||||||
] as [
|
] as [
|
||||||
{
|
{
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
|
presenting: boolean;
|
||||||
|
title: string;
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -137,6 +141,7 @@ const createProps = (
|
||||||
): PropsType => ({
|
): PropsType => ({
|
||||||
activeCall: createActiveCallProp(overrideProps),
|
activeCall: createActiveCallProp(overrideProps),
|
||||||
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
|
||||||
|
getPresentingSources: action('get-presenting-sources'),
|
||||||
hangUp: action('hang-up'),
|
hangUp: action('hang-up'),
|
||||||
i18n,
|
i18n,
|
||||||
me: {
|
me: {
|
||||||
|
@ -145,14 +150,19 @@ const createProps = (
|
||||||
profileName: 'Morty Smith',
|
profileName: 'Morty Smith',
|
||||||
title: 'Morty Smith',
|
title: 'Morty Smith',
|
||||||
},
|
},
|
||||||
|
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
setLocalPreview: action('set-local-preview'),
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
|
setPresenting: action('toggle-presenting'),
|
||||||
setRendererCanvas: action('set-renderer-canvas'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
stickyControls: boolean('stickyControls', false),
|
stickyControls: boolean('stickyControls', false),
|
||||||
toggleParticipants: action('toggle-participants'),
|
toggleParticipants: action('toggle-participants'),
|
||||||
togglePip: action('toggle-pip'),
|
togglePip: action('toggle-pip'),
|
||||||
|
toggleScreenRecordingPermissionsDialog: action(
|
||||||
|
'toggle-screen-recording-permissions-dialog'
|
||||||
|
),
|
||||||
toggleSettings: action('toggle-settings'),
|
toggleSettings: action('toggle-settings'),
|
||||||
toggleSpeakerView: action('toggle-speaker-view'),
|
toggleSpeakerView: action('toggle-speaker-view'),
|
||||||
});
|
});
|
||||||
|
@ -249,6 +259,8 @@ story.add('Group call - 1', () => (
|
||||||
demuxId: 0,
|
demuxId: 0,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
...getDefaultConversation({
|
...getDefaultConversation({
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
|
@ -266,6 +278,8 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
|
||||||
demuxId: index,
|
demuxId: index,
|
||||||
hasRemoteAudio: index % 3 !== 0,
|
hasRemoteAudio: index % 3 !== 0,
|
||||||
hasRemoteVideo: index % 4 !== 0,
|
hasRemoteVideo: index % 4 !== 0,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
...getDefaultConversation({
|
...getDefaultConversation({
|
||||||
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
|
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
|
||||||
|
@ -303,6 +317,8 @@ story.add('Group call - reconnecting', () => (
|
||||||
demuxId: 0,
|
demuxId: 0,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
...getDefaultConversation({
|
...getDefaultConversation({
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
|
|
|
@ -21,18 +21,23 @@ import {
|
||||||
CallState,
|
CallState,
|
||||||
GroupCallConnectionState,
|
GroupCallConnectionState,
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
|
PresentedSource,
|
||||||
VideoFrameSource,
|
VideoFrameSource,
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
|
import { CallingToastManager } from './CallingToastManager';
|
||||||
import { ColorType } from '../types/Colors';
|
import { ColorType } from '../types/Colors';
|
||||||
import { LocalizerType } from '../types/Util';
|
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
|
||||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||||
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
|
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
|
||||||
import { GroupCallToastManager } from './GroupCallToastManager';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { isScreenSharingEnabled } from '../util/isScreenSharingEnabled';
|
||||||
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting';
|
||||||
|
import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||||
|
getPresentingSources: () => void;
|
||||||
hangUp: (_: HangUpType) => void;
|
hangUp: (_: HangUpType) => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
joinedAt?: number;
|
joinedAt?: number;
|
||||||
|
@ -44,14 +49,17 @@ export type PropsType = {
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
openSystemPreferencesAction: () => unknown;
|
||||||
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
|
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
|
||||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
|
setPresenting: (_?: PresentedSource) => void;
|
||||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
stickyControls: boolean;
|
stickyControls: boolean;
|
||||||
toggleParticipants: () => void;
|
toggleParticipants: () => void;
|
||||||
togglePip: () => void;
|
togglePip: () => void;
|
||||||
|
toggleScreenRecordingPermissionsDialog: () => unknown;
|
||||||
toggleSettings: () => void;
|
toggleSettings: () => void;
|
||||||
toggleSpeakerView: () => void;
|
toggleSpeakerView: () => void;
|
||||||
};
|
};
|
||||||
|
@ -59,18 +67,22 @@ export type PropsType = {
|
||||||
export const CallScreen: React.FC<PropsType> = ({
|
export const CallScreen: React.FC<PropsType> = ({
|
||||||
activeCall,
|
activeCall,
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
|
getPresentingSources,
|
||||||
hangUp,
|
hangUp,
|
||||||
i18n,
|
i18n,
|
||||||
joinedAt,
|
joinedAt,
|
||||||
me,
|
me,
|
||||||
|
openSystemPreferencesAction,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
|
setPresenting,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
stickyControls,
|
stickyControls,
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
togglePip,
|
togglePip,
|
||||||
|
toggleScreenRecordingPermissionsDialog,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
toggleSpeakerView,
|
toggleSpeakerView,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -78,9 +90,19 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
conversation,
|
conversation,
|
||||||
hasLocalAudio,
|
hasLocalAudio,
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
|
isInSpeakerView,
|
||||||
|
presentingSource,
|
||||||
|
remoteParticipants,
|
||||||
|
showNeedsScreenRecordingPermissionsWarning,
|
||||||
showParticipantsList,
|
showParticipantsList,
|
||||||
} = activeCall;
|
} = activeCall;
|
||||||
|
|
||||||
|
useActivateSpeakerViewOnPresenting(
|
||||||
|
remoteParticipants,
|
||||||
|
isInSpeakerView,
|
||||||
|
toggleSpeakerView
|
||||||
|
);
|
||||||
|
|
||||||
const toggleAudio = useCallback(() => {
|
const toggleAudio = useCallback(() => {
|
||||||
setLocalAudio({
|
setLocalAudio({
|
||||||
enabled: !hasLocalAudio,
|
enabled: !hasLocalAudio,
|
||||||
|
@ -93,6 +115,14 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
});
|
});
|
||||||
}, [setLocalVideo, hasLocalVideo]);
|
}, [setLocalVideo, hasLocalVideo]);
|
||||||
|
|
||||||
|
const togglePresenting = useCallback(() => {
|
||||||
|
if (presentingSource) {
|
||||||
|
setPresenting();
|
||||||
|
} else {
|
||||||
|
getPresentingSources();
|
||||||
|
}
|
||||||
|
}, [getPresentingSources, presentingSource, setPresenting]);
|
||||||
|
|
||||||
const [acceptedDuration, setAcceptedDuration] = useState<number | null>(null);
|
const [acceptedDuration, setAcceptedDuration] = useState<number | null>(null);
|
||||||
const [showControls, setShowControls] = useState(true);
|
const [showControls, setShowControls] = useState(true);
|
||||||
|
|
||||||
|
@ -151,7 +181,11 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
};
|
};
|
||||||
}, [toggleAudio, toggleVideo]);
|
}, [toggleAudio, toggleVideo]);
|
||||||
|
|
||||||
const hasRemoteVideo = activeCall.remoteParticipants.some(
|
const currentPresenter = remoteParticipants.find(
|
||||||
|
participant => participant.presenting
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasRemoteVideo = remoteParticipants.some(
|
||||||
remoteParticipant => remoteParticipant.hasRemoteVideo
|
remoteParticipant => remoteParticipant.hasRemoteVideo
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -183,16 +217,22 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
participantCount = activeCall.remoteParticipants.length + 1;
|
participantCount = activeCall.remoteParticipants.length + 1;
|
||||||
headerMessage = undefined;
|
headerMessage = undefined;
|
||||||
headerTitle = activeCall.remoteParticipants.length
|
|
||||||
? undefined
|
if (currentPresenter) {
|
||||||
: i18n('calling__in-this-call--zero');
|
headerTitle = i18n('calling__presenting--person-ongoing', [
|
||||||
|
currentPresenter.title,
|
||||||
|
]);
|
||||||
|
} else if (!activeCall.remoteParticipants.length) {
|
||||||
|
headerTitle = i18n('calling__in-this-call--zero');
|
||||||
|
}
|
||||||
|
|
||||||
isConnected =
|
isConnected =
|
||||||
activeCall.connectionState === GroupCallConnectionState.Connected;
|
activeCall.connectionState === GroupCallConnectionState.Connected;
|
||||||
remoteParticipantsElement = (
|
remoteParticipantsElement = (
|
||||||
<GroupCallRemoteParticipants
|
<GroupCallRemoteParticipants
|
||||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isInSpeakerView={activeCall.isInSpeakerView}
|
isInSpeakerView={isInSpeakerView}
|
||||||
remoteParticipants={activeCall.remoteParticipants}
|
remoteParticipants={activeCall.remoteParticipants}
|
||||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||||
/>
|
/>
|
||||||
|
@ -206,9 +246,15 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
activeCall.callMode === CallMode.Group &&
|
activeCall.callMode === CallMode.Group &&
|
||||||
!activeCall.remoteParticipants.length;
|
!activeCall.remoteParticipants.length;
|
||||||
|
|
||||||
const videoButtonType = hasLocalVideo
|
let videoButtonType: CallingButtonType;
|
||||||
? CallingButtonType.VIDEO_ON
|
if (presentingSource) {
|
||||||
: CallingButtonType.VIDEO_OFF;
|
videoButtonType = CallingButtonType.VIDEO_DISABLED;
|
||||||
|
} else if (hasLocalVideo) {
|
||||||
|
videoButtonType = CallingButtonType.VIDEO_ON;
|
||||||
|
} else {
|
||||||
|
videoButtonType = CallingButtonType.VIDEO_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
const audioButtonType = hasLocalAudio
|
const audioButtonType = hasLocalAudio
|
||||||
? CallingButtonType.AUDIO_ON
|
? CallingButtonType.AUDIO_ON
|
||||||
: CallingButtonType.AUDIO_OFF;
|
: CallingButtonType.AUDIO_OFF;
|
||||||
|
@ -222,6 +268,23 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
!showControls && !isAudioOnly && isConnected,
|
!showControls && !isAudioOnly && isConnected,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isGroupCall = activeCall.callMode === CallMode.Group;
|
||||||
|
const localPreviewVideoClass = classNames({
|
||||||
|
'module-ongoing-call__footer__local-preview__video': true,
|
||||||
|
'module-ongoing-call__footer__local-preview__video--presenting': Boolean(
|
||||||
|
presentingSource
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
let presentingButtonType: CallingButtonType;
|
||||||
|
if (presentingSource) {
|
||||||
|
presentingButtonType = CallingButtonType.PRESENTING_ON;
|
||||||
|
} else if (currentPresenter) {
|
||||||
|
presentingButtonType = CallingButtonType.PRESENTING_DISABLED;
|
||||||
|
} else {
|
||||||
|
presentingButtonType = CallingButtonType.PRESENTING_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -235,20 +298,24 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
}}
|
}}
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
{activeCall.callMode === CallMode.Group ? (
|
{showNeedsScreenRecordingPermissionsWarning ? (
|
||||||
<GroupCallToastManager
|
<NeedsScreenRecordingPermissionsModal
|
||||||
connectionState={activeCall.connectionState}
|
toggleScreenRecordingPermissionsDialog={
|
||||||
|
toggleScreenRecordingPermissionsDialog
|
||||||
|
}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<CallingToastManager activeCall={activeCall} i18n={i18n} />
|
||||||
<div
|
<div
|
||||||
className={classNames('module-ongoing-call__header', controlsFadeClass)}
|
className={classNames('module-ongoing-call__header', controlsFadeClass)}
|
||||||
>
|
>
|
||||||
<CallingHeader
|
<CallingHeader
|
||||||
canPip
|
canPip
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isInSpeakerView={activeCall.isInSpeakerView}
|
isInSpeakerView={isInSpeakerView}
|
||||||
isGroupCall={activeCall.callMode === CallMode.Group}
|
isGroupCall={isGroupCall}
|
||||||
message={headerMessage}
|
message={headerMessage}
|
||||||
participantCount={participantCount}
|
participantCount={participantCount}
|
||||||
showParticipantsList={showParticipantsList}
|
showParticipantsList={showParticipantsList}
|
||||||
|
@ -263,7 +330,7 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
{hasLocalVideo && isLonelyInGroup ? (
|
{hasLocalVideo && isLonelyInGroup ? (
|
||||||
<div className="module-ongoing-call__local-preview-fullsize">
|
<div className="module-ongoing-call__local-preview-fullsize">
|
||||||
<video
|
<video
|
||||||
className="module-ongoing-call__footer__local-preview__video"
|
className={localPreviewVideoClass}
|
||||||
ref={localVideoRef}
|
ref={localVideoRef}
|
||||||
autoPlay
|
autoPlay
|
||||||
/>
|
/>
|
||||||
|
@ -308,6 +375,13 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
controlsFadeClass
|
controlsFadeClass
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{isScreenSharingEnabled() ? (
|
||||||
|
<CallingButton
|
||||||
|
buttonType={presentingButtonType}
|
||||||
|
i18n={i18n}
|
||||||
|
onClick={togglePresenting}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<CallingButton
|
<CallingButton
|
||||||
buttonType={videoButtonType}
|
buttonType={videoButtonType}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
@ -333,7 +407,7 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
>
|
>
|
||||||
{hasLocalVideo && !isLonelyInGroup ? (
|
{hasLocalVideo && !isLonelyInGroup ? (
|
||||||
<video
|
<video
|
||||||
className="module-ongoing-call__footer__local-preview__video"
|
className={localPreviewVideoClass}
|
||||||
ref={localVideoRef}
|
ref={localVideoRef}
|
||||||
autoPlay
|
autoPlay
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -14,11 +14,9 @@ import enMessages from '../../_locales/en/messages.json';
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
buttonType: select(
|
buttonType:
|
||||||
'buttonType',
|
overrideProps.buttonType ||
|
||||||
CallingButtonType,
|
select('buttonType', CallingButtonType, CallingButtonType.HANG_UP),
|
||||||
overrideProps.buttonType || CallingButtonType.HANG_UP
|
|
||||||
),
|
|
||||||
i18n,
|
i18n,
|
||||||
onClick: action('on-click'),
|
onClick: action('on-click'),
|
||||||
tooltipDirection: select(
|
tooltipDirection: select(
|
||||||
|
@ -30,9 +28,16 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
|
||||||
const story = storiesOf('Components/CallingButton', module);
|
const story = storiesOf('Components/CallingButton', module);
|
||||||
|
|
||||||
story.add('Default', () => {
|
story.add('Kitchen Sink', () => {
|
||||||
const props = createProps();
|
return (
|
||||||
return <CallingButton {...props} />;
|
<>
|
||||||
|
{Object.keys(CallingButtonType).map(buttonType => (
|
||||||
|
<CallingButton
|
||||||
|
{...createProps({ buttonType: buttonType as CallingButtonType })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Audio On', () => {
|
story.add('Audio On', () => {
|
||||||
|
@ -83,3 +88,17 @@ story.add('Tooltip right', () => {
|
||||||
});
|
});
|
||||||
return <CallingButton {...props} />;
|
return <CallingButton {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Presenting On', () => {
|
||||||
|
const props = createProps({
|
||||||
|
buttonType: CallingButtonType.PRESENTING_ON,
|
||||||
|
});
|
||||||
|
return <CallingButton {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Presenting Off', () => {
|
||||||
|
const props = createProps({
|
||||||
|
buttonType: CallingButtonType.PRESENTING_OFF,
|
||||||
|
});
|
||||||
|
return <CallingButton {...props} />;
|
||||||
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -12,6 +12,9 @@ export enum CallingButtonType {
|
||||||
AUDIO_OFF = 'AUDIO_OFF',
|
AUDIO_OFF = 'AUDIO_OFF',
|
||||||
AUDIO_ON = 'AUDIO_ON',
|
AUDIO_ON = 'AUDIO_ON',
|
||||||
HANG_UP = 'HANG_UP',
|
HANG_UP = 'HANG_UP',
|
||||||
|
PRESENTING_DISABLED = 'PRESENTING_DISABLED',
|
||||||
|
PRESENTING_OFF = 'PRESENTING_OFF',
|
||||||
|
PRESENTING_ON = 'PRESENTING_ON',
|
||||||
VIDEO_DISABLED = 'VIDEO_DISABLED',
|
VIDEO_DISABLED = 'VIDEO_DISABLED',
|
||||||
VIDEO_OFF = 'VIDEO_OFF',
|
VIDEO_OFF = 'VIDEO_OFF',
|
||||||
VIDEO_ON = 'VIDEO_ON',
|
VIDEO_ON = 'VIDEO_ON',
|
||||||
|
@ -32,9 +35,11 @@ export const CallingButton = ({
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
let classNameSuffix = '';
|
let classNameSuffix = '';
|
||||||
let tooltipContent = '';
|
let tooltipContent = '';
|
||||||
|
let disabled = false;
|
||||||
if (buttonType === CallingButtonType.AUDIO_DISABLED) {
|
if (buttonType === CallingButtonType.AUDIO_DISABLED) {
|
||||||
classNameSuffix = 'audio--disabled';
|
classNameSuffix = 'audio--disabled';
|
||||||
tooltipContent = i18n('calling__button--audio-disabled');
|
tooltipContent = i18n('calling__button--audio-disabled');
|
||||||
|
disabled = true;
|
||||||
} else if (buttonType === CallingButtonType.AUDIO_OFF) {
|
} else if (buttonType === CallingButtonType.AUDIO_OFF) {
|
||||||
classNameSuffix = 'audio--off';
|
classNameSuffix = 'audio--off';
|
||||||
tooltipContent = i18n('calling__button--audio-on');
|
tooltipContent = i18n('calling__button--audio-on');
|
||||||
|
@ -44,6 +49,7 @@ export const CallingButton = ({
|
||||||
} else if (buttonType === CallingButtonType.VIDEO_DISABLED) {
|
} else if (buttonType === CallingButtonType.VIDEO_DISABLED) {
|
||||||
classNameSuffix = 'video--disabled';
|
classNameSuffix = 'video--disabled';
|
||||||
tooltipContent = i18n('calling__button--video-disabled');
|
tooltipContent = i18n('calling__button--video-disabled');
|
||||||
|
disabled = true;
|
||||||
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
|
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
|
||||||
classNameSuffix = 'video--off';
|
classNameSuffix = 'video--off';
|
||||||
tooltipContent = i18n('calling__button--video-on');
|
tooltipContent = i18n('calling__button--video-on');
|
||||||
|
@ -53,6 +59,16 @@ export const CallingButton = ({
|
||||||
} else if (buttonType === CallingButtonType.HANG_UP) {
|
} else if (buttonType === CallingButtonType.HANG_UP) {
|
||||||
classNameSuffix = 'hangup';
|
classNameSuffix = 'hangup';
|
||||||
tooltipContent = i18n('calling__hangup');
|
tooltipContent = i18n('calling__hangup');
|
||||||
|
} else if (buttonType === CallingButtonType.PRESENTING_DISABLED) {
|
||||||
|
classNameSuffix = 'presenting--disabled';
|
||||||
|
tooltipContent = i18n('calling__button--presenting-disabled');
|
||||||
|
disabled = true;
|
||||||
|
} else if (buttonType === CallingButtonType.PRESENTING_ON) {
|
||||||
|
classNameSuffix = 'presenting--on';
|
||||||
|
tooltipContent = i18n('calling__button--presenting-off');
|
||||||
|
} else if (buttonType === CallingButtonType.PRESENTING_OFF) {
|
||||||
|
classNameSuffix = 'presenting--off';
|
||||||
|
tooltipContent = i18n('calling__button--presenting-on');
|
||||||
}
|
}
|
||||||
|
|
||||||
const className = classNames(
|
const className = classNames(
|
||||||
|
@ -68,9 +84,10 @@ export const CallingButton = ({
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label={tooltipContent}
|
aria-label={tooltipContent}
|
||||||
type="button"
|
|
||||||
className={className}
|
className={className}
|
||||||
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -23,6 +23,8 @@ function createParticipant(
|
||||||
demuxId: 2,
|
demuxId: 2,
|
||||||
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||||
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||||
|
presenting: Boolean(participantProps.presenting),
|
||||||
|
sharingScreen: Boolean(participantProps.sharingScreen),
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
...getDefaultConversation({
|
...getDefaultConversation({
|
||||||
avatarPath: participantProps.avatarPath,
|
avatarPath: participantProps.avatarPath,
|
||||||
|
@ -69,7 +71,7 @@ story.add('Many Participants', () => {
|
||||||
}),
|
}),
|
||||||
createParticipant({
|
createParticipant({
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
presenting: true,
|
||||||
name: 'Rage Trunks',
|
name: 'Rage Trunks',
|
||||||
title: 'Rage Trunks',
|
title: 'Rage Trunks',
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -13,8 +13,9 @@ import { sortByTitle } from '../util/sortByTitle';
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
type ParticipantType = ConversationType & {
|
type ParticipantType = ConversationType & {
|
||||||
hasAudio?: boolean;
|
hasRemoteAudio?: boolean;
|
||||||
hasVideo?: boolean;
|
hasRemoteVideo?: boolean;
|
||||||
|
presenting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
|
@ -130,12 +131,15 @@ export const CallingParticipantsList = React.memo(
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{participant.hasAudio === false ? (
|
{participant.hasRemoteAudio === false ? (
|
||||||
<span className="module-calling-participants-list__muted--audio" />
|
<span className="module-calling-participants-list__muted--audio" />
|
||||||
) : null}
|
) : null}
|
||||||
{participant.hasVideo === false ? (
|
{participant.hasRemoteVideo === false ? (
|
||||||
<span className="module-calling-participants-list__muted--video" />
|
<span className="module-calling-participants-list__muted--video" />
|
||||||
) : null}
|
) : null}
|
||||||
|
{participant.presenting ? (
|
||||||
|
<span className="module-calling-participants-list__presenting" />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
|
@ -49,7 +49,9 @@ const defaultCall: ActiveCallType = {
|
||||||
callMode: CallMode.Direct as CallMode.Direct,
|
callMode: CallMode.Direct as CallMode.Direct,
|
||||||
callState: CallState.Accepted,
|
callState: CallState.Accepted,
|
||||||
peekedParticipants: [],
|
peekedParticipants: [],
|
||||||
remoteParticipants: [{ hasRemoteVideo: true }],
|
remoteParticipants: [
|
||||||
|
{ hasRemoteVideo: true, presenting: false, title: 'Arsene' },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
@ -79,7 +81,9 @@ story.add('Contact (with avatar and no video)', () => {
|
||||||
...conversation,
|
...conversation,
|
||||||
avatarPath: 'https://www.fillmurray.com/64/64',
|
avatarPath: 'https://www.fillmurray.com/64/64',
|
||||||
},
|
},
|
||||||
remoteParticipants: [{ hasRemoteVideo: false }],
|
remoteParticipants: [
|
||||||
|
{ hasRemoteVideo: false, presenting: false, title: 'Julian' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return <CallingPip {...props} />;
|
return <CallingPip {...props} />;
|
||||||
|
|
|
@ -96,9 +96,8 @@ export const CallingPipRemoteVideo = ({
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return maxBy(
|
return maxBy(activeCall.remoteParticipants, participant =>
|
||||||
activeCall.remoteParticipants,
|
participant.presenting ? Infinity : participant.speakerTime || -Infinity
|
||||||
participant => participant.speakerTime || -Infinity
|
|
||||||
);
|
);
|
||||||
}, [activeCall.callMode, activeCall.remoteParticipants]);
|
}, [activeCall.callMode, activeCall.remoteParticipants]);
|
||||||
|
|
||||||
|
|
29
ts/components/CallingScreenSharingController.stories.tsx
Normal file
29
ts/components/CallingScreenSharingController.stories.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CallingScreenSharingController,
|
||||||
|
PropsType,
|
||||||
|
} from './CallingScreenSharingController';
|
||||||
|
|
||||||
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const createProps = (): PropsType => ({
|
||||||
|
i18n,
|
||||||
|
onCloseController: action('on-close-controller'),
|
||||||
|
onStopSharing: action('on-stop-sharing'),
|
||||||
|
presentedSourceName: 'Application',
|
||||||
|
});
|
||||||
|
|
||||||
|
const story = storiesOf('Components/CallingScreenSharingController', module);
|
||||||
|
|
||||||
|
story.add('Controller', () => {
|
||||||
|
return <CallingScreenSharingController {...createProps()} />;
|
||||||
|
});
|
39
ts/components/CallingScreenSharingController.tsx
Normal file
39
ts/components/CallingScreenSharingController.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
onCloseController: () => unknown;
|
||||||
|
onStopSharing: () => unknown;
|
||||||
|
presentedSourceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CallingScreenSharingController = ({
|
||||||
|
i18n,
|
||||||
|
onCloseController,
|
||||||
|
onStopSharing,
|
||||||
|
presentedSourceName,
|
||||||
|
}: PropsType): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="module-CallingScreenSharingController">
|
||||||
|
<div className="module-CallingScreenSharingController__text">
|
||||||
|
{i18n('calling__presenting--info', [presentedSourceName])}
|
||||||
|
</div>
|
||||||
|
<div className="module-CallingScreenSharingController__buttons">
|
||||||
|
<Button onClick={onStopSharing} variant={ButtonVariant.Destructive}>
|
||||||
|
{i18n('calling__presenting--stop')}
|
||||||
|
</Button>
|
||||||
|
<button
|
||||||
|
aria-label={i18n('close')}
|
||||||
|
className="module-CallingScreenSharingController__close"
|
||||||
|
onClick={onCloseController}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CallingSelectPresentingSourcesModal,
|
||||||
|
PropsType,
|
||||||
|
} from './CallingSelectPresentingSourcesModal';
|
||||||
|
|
||||||
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const createProps = (): PropsType => ({
|
||||||
|
i18n,
|
||||||
|
presentingSourcesAvailable: [
|
||||||
|
{
|
||||||
|
id: 'screen',
|
||||||
|
name: 'Entire Screen',
|
||||||
|
thumbnail:
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P/1PwAF8AL1sEVIPAAAAABJRU5ErkJggg==',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'window:123',
|
||||||
|
name: 'Bozirro Airhorse',
|
||||||
|
thumbnail:
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z1D4HwAF5wJxzsNOIAAAAABJRU5ErkJggg==',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'window:456',
|
||||||
|
name: 'Discoverer',
|
||||||
|
thumbnail:
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8HwHwAFHQIIj4yLtgAAAABJRU5ErkJggg==',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'window:789',
|
||||||
|
name: 'Signal Beta',
|
||||||
|
thumbnail: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'window:xyz',
|
||||||
|
name: 'Window that has a really long name and overflows',
|
||||||
|
thumbnail:
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+O/wHwAEhgJAyqFnAgAAAABJRU5ErkJggg==',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
setPresenting: action('set-presenting'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const story = storiesOf(
|
||||||
|
'Components/CallingSelectPresentingSourcesModal',
|
||||||
|
module
|
||||||
|
);
|
||||||
|
|
||||||
|
story.add('Modal', () => {
|
||||||
|
return <CallingSelectPresentingSourcesModal {...createProps()} />;
|
||||||
|
});
|
137
ts/components/CallingSelectPresentingSourcesModal.tsx
Normal file
137
ts/components/CallingSelectPresentingSourcesModal.tsx
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { groupBy } from 'lodash';
|
||||||
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { PresentedSource, PresentableSource } from '../types/Calling';
|
||||||
|
import { Theme } from '../util/theme';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
presentingSourcesAvailable: Array<PresentableSource>;
|
||||||
|
setPresenting: (_?: PresentedSource) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Source = ({
|
||||||
|
onSourceClick,
|
||||||
|
source,
|
||||||
|
sourceToPresent,
|
||||||
|
}: {
|
||||||
|
onSourceClick: (source: PresentedSource) => void;
|
||||||
|
source: PresentableSource;
|
||||||
|
sourceToPresent?: PresentedSource;
|
||||||
|
}): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames({
|
||||||
|
'module-CallingSelectPresentingSourcesModal__source': true,
|
||||||
|
'module-CallingSelectPresentingSourcesModal__source--selected':
|
||||||
|
sourceToPresent?.id === source.id,
|
||||||
|
})}
|
||||||
|
key={source.id}
|
||||||
|
onClick={() => {
|
||||||
|
onSourceClick({
|
||||||
|
id: source.id,
|
||||||
|
name: source.name,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={source.name}
|
||||||
|
className="module-CallingSelectPresentingSourcesModal__name--screenshot"
|
||||||
|
src={source.thumbnail}
|
||||||
|
/>
|
||||||
|
<div className="module-CallingSelectPresentingSourcesModal__name--container">
|
||||||
|
{source.appIcon ? (
|
||||||
|
<img
|
||||||
|
alt={source.name}
|
||||||
|
className="module-CallingSelectPresentingSourcesModal__name--icon"
|
||||||
|
height={16}
|
||||||
|
src={source.appIcon}
|
||||||
|
width={16}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className="module-CallingSelectPresentingSourcesModal__name--text">
|
||||||
|
{source.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CallingSelectPresentingSourcesModal = ({
|
||||||
|
i18n,
|
||||||
|
presentingSourcesAvailable,
|
||||||
|
setPresenting,
|
||||||
|
}: PropsType): JSX.Element | null => {
|
||||||
|
const [sourceToPresent, setSourceToPresent] = useState<
|
||||||
|
PresentedSource | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
if (!presentingSourcesAvailable.length) {
|
||||||
|
throw new Error('No sources available for presenting');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = groupBy(presentingSourcesAvailable, source =>
|
||||||
|
source.id.startsWith('screen')
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
hasXButton
|
||||||
|
i18n={i18n}
|
||||||
|
moduleClassName="module-CallingSelectPresentingSourcesModal"
|
||||||
|
onClose={() => {
|
||||||
|
setPresenting(sourceToPresent);
|
||||||
|
}}
|
||||||
|
theme={Theme.Dark}
|
||||||
|
title={i18n('calling__SelectPresentingSourcesModal--title')}
|
||||||
|
>
|
||||||
|
<div className="module-CallingSelectPresentingSourcesModal__title">
|
||||||
|
{i18n('calling__SelectPresentingSourcesModal--entireScreen')}
|
||||||
|
</div>
|
||||||
|
<div className="module-CallingSelectPresentingSourcesModal__sources">
|
||||||
|
{sources.true.map(source => (
|
||||||
|
<Source
|
||||||
|
key={source.id}
|
||||||
|
onSourceClick={selectedSource => setSourceToPresent(selectedSource)}
|
||||||
|
source={source}
|
||||||
|
sourceToPresent={sourceToPresent}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="module-CallingSelectPresentingSourcesModal__title">
|
||||||
|
{i18n('calling__SelectPresentingSourcesModal--window')}
|
||||||
|
</div>
|
||||||
|
<div className="module-CallingSelectPresentingSourcesModal__sources">
|
||||||
|
{sources.false.map(source => (
|
||||||
|
<Source
|
||||||
|
key={source.id}
|
||||||
|
onSourceClick={selectedSource => setSourceToPresent(selectedSource)}
|
||||||
|
source={source}
|
||||||
|
sourceToPresent={sourceToPresent}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Modal.Footer moduleClassName="module-CallingSelectPresentingSourcesModal">
|
||||||
|
<Button
|
||||||
|
onClick={() => setPresenting()}
|
||||||
|
variant={ButtonVariant.Secondary}
|
||||||
|
>
|
||||||
|
{i18n('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!sourceToPresent}
|
||||||
|
onClick={() => setPresenting(sourceToPresent)}
|
||||||
|
>
|
||||||
|
{i18n('calling__SelectPresentingSourcesModal--confirm')}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
163
ts/components/CallingToastManager.tsx
Normal file
163
ts/components/CallingToastManager.tsx
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
ActiveCallType,
|
||||||
|
CallMode,
|
||||||
|
GroupCallConnectionState,
|
||||||
|
} from '../types/Calling';
|
||||||
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
activeCall: ActiveCallType;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToastType =
|
||||||
|
| {
|
||||||
|
message: string;
|
||||||
|
type: 'dismissable' | 'static';
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
function getReconnectingToast({ activeCall, i18n }: PropsType): ToastType {
|
||||||
|
if (
|
||||||
|
activeCall.callMode === CallMode.Group &&
|
||||||
|
activeCall.connectionState === GroupCallConnectionState.Reconnecting
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
message: i18n('callReconnecting'),
|
||||||
|
type: 'static',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ME = Symbol('me');
|
||||||
|
|
||||||
|
function getCurrentPresenter(
|
||||||
|
activeCall: Readonly<ActiveCallType>
|
||||||
|
): ConversationType | typeof ME | undefined {
|
||||||
|
if (activeCall.presentingSource) {
|
||||||
|
return ME;
|
||||||
|
}
|
||||||
|
if (activeCall.callMode === CallMode.Direct) {
|
||||||
|
const isOtherPersonPresenting = activeCall.remoteParticipants.some(
|
||||||
|
participant => participant.presenting
|
||||||
|
);
|
||||||
|
return isOtherPersonPresenting ? activeCall.conversation : undefined;
|
||||||
|
}
|
||||||
|
if (activeCall.callMode === CallMode.Group) {
|
||||||
|
return activeCall.remoteParticipants.find(
|
||||||
|
participant => participant.presenting
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useScreenSharingToast({ activeCall, i18n }: PropsType): ToastType {
|
||||||
|
const [result, setResult] = useState<undefined | ToastType>(undefined);
|
||||||
|
|
||||||
|
const [previousPresenter, setPreviousPresenter] = useState<
|
||||||
|
undefined | { id: string | typeof ME; title?: string }
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const previousPresenterId = previousPresenter?.id;
|
||||||
|
const previousPresenterTitle = previousPresenter?.title;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentPresenter = getCurrentPresenter(activeCall);
|
||||||
|
if (!currentPresenter && previousPresenterId) {
|
||||||
|
if (previousPresenterId === ME) {
|
||||||
|
setResult({
|
||||||
|
type: 'dismissable',
|
||||||
|
message: i18n('calling__presenting--you-stopped'),
|
||||||
|
});
|
||||||
|
} else if (previousPresenterTitle) {
|
||||||
|
setResult({
|
||||||
|
type: 'dismissable',
|
||||||
|
message: i18n('calling__presenting--person-stopped', [
|
||||||
|
previousPresenterTitle,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeCall, i18n, previousPresenterId, previousPresenterTitle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentPresenter = getCurrentPresenter(activeCall);
|
||||||
|
if (currentPresenter === ME) {
|
||||||
|
setPreviousPresenter({
|
||||||
|
id: ME,
|
||||||
|
});
|
||||||
|
} else if (!currentPresenter) {
|
||||||
|
setPreviousPresenter(undefined);
|
||||||
|
} else {
|
||||||
|
const { id, title } = currentPresenter;
|
||||||
|
setPreviousPresenter({ id, title });
|
||||||
|
}
|
||||||
|
}, [activeCall]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_DELAY = 5000;
|
||||||
|
|
||||||
|
// In the future, this component should show toasts when users join or leave. See
|
||||||
|
// DESKTOP-902.
|
||||||
|
export const CallingToastManager: React.FC<PropsType> = props => {
|
||||||
|
const reconnectingToast = getReconnectingToast(props);
|
||||||
|
const screenSharingToast = useScreenSharingToast(props);
|
||||||
|
|
||||||
|
let toast: ToastType;
|
||||||
|
if (reconnectingToast) {
|
||||||
|
toast = reconnectingToast;
|
||||||
|
} else if (screenSharingToast) {
|
||||||
|
toast = screenSharingToast;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [toastMessage, setToastMessage] = useState('');
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const dismissToast = useCallback(() => {
|
||||||
|
if (timeoutRef) {
|
||||||
|
setToastMessage('');
|
||||||
|
}
|
||||||
|
}, [setToastMessage, timeoutRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (toast) {
|
||||||
|
if (toast.type === 'dismissable') {
|
||||||
|
if (timeoutRef && timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
timeoutRef.current = setTimeout(dismissToast, DEFAULT_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
setToastMessage(toast.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef && timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [dismissToast, setToastMessage, timeoutRef, toast]);
|
||||||
|
|
||||||
|
const isVisible = Boolean(toastMessage);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames('module-ongoing-call__toast', {
|
||||||
|
'module-ongoing-call__toast--hidden': !isVisible,
|
||||||
|
})}
|
||||||
|
type="button"
|
||||||
|
onClick={dismissToast}
|
||||||
|
>
|
||||||
|
{toastMessage}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
|
@ -22,6 +22,8 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
|
||||||
demuxId: index,
|
demuxId: index,
|
||||||
hasRemoteAudio: index % 3 !== 0,
|
hasRemoteAudio: index % 3 !== 0,
|
||||||
hasRemoteVideo: index % 4 !== 0,
|
hasRemoteVideo: index % 4 !== 0,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
...getDefaultConversation({
|
...getDefaultConversation({
|
||||||
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
|
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
|
||||||
|
|
|
@ -42,6 +42,8 @@ const createProps = (
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio: false,
|
hasRemoteAudio: false,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
...getDefaultConversation({
|
...getDefaultConversation({
|
||||||
isBlocked: Boolean(isBlocked),
|
isBlocked: Boolean(isBlocked),
|
||||||
|
|
|
@ -105,7 +105,8 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
||||||
// 2. Split participants into two groups: ones in the main grid and ones in the overflow
|
// 2. Split participants into two groups: ones in the main grid and ones in the overflow
|
||||||
// sidebar.
|
// sidebar.
|
||||||
//
|
//
|
||||||
// We start by sorting by `speakerTime` so that the most recent speakers are first in
|
// We start by sorting by `presenting` first since presenters should be on the main grid
|
||||||
|
// then we sort by `speakerTime` so that the most recent speakers are next in
|
||||||
// line for the main grid. Then we split the list in two: one for the grid and one for
|
// line for the main grid. Then we split the list in two: one for the grid and one for
|
||||||
// the overflow area.
|
// the overflow area.
|
||||||
//
|
//
|
||||||
|
@ -119,7 +120,9 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
||||||
remoteParticipants
|
remoteParticipants
|
||||||
.concat()
|
.concat()
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) => (b.speakerTime || -Infinity) - (a.speakerTime || -Infinity)
|
(a, b) =>
|
||||||
|
Number(b.presenting || 0) - Number(a.presenting || 0) ||
|
||||||
|
(b.speakerTime || -Infinity) - (a.speakerTime || -Infinity)
|
||||||
),
|
),
|
||||||
[remoteParticipants]
|
[remoteParticipants]
|
||||||
);
|
);
|
||||||
|
@ -275,18 +278,23 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
||||||
if (isPageVisible) {
|
if (isPageVisible) {
|
||||||
setGroupCallVideoRequest([
|
setGroupCallVideoRequest([
|
||||||
...gridParticipants.map(participant => {
|
...gridParticipants.map(participant => {
|
||||||
if (participant.hasRemoteVideo) {
|
let scalar: number;
|
||||||
return {
|
if (participant.sharingScreen) {
|
||||||
demuxId: participant.demuxId,
|
// We want best-resolution video if someone is sharing their screen. This code
|
||||||
width: Math.floor(
|
// is extra-defensive against strange devicePixelRatios.
|
||||||
gridParticipantHeight *
|
scalar = Math.max(window.devicePixelRatio || 1, 1);
|
||||||
participant.videoAspectRatio *
|
} else if (participant.hasRemoteVideo) {
|
||||||
VIDEO_REQUEST_SCALAR
|
scalar = VIDEO_REQUEST_SCALAR;
|
||||||
),
|
} else {
|
||||||
height: Math.floor(gridParticipantHeight * VIDEO_REQUEST_SCALAR),
|
scalar = 0;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return nonRenderedRemoteParticipant(participant);
|
return {
|
||||||
|
demuxId: participant.demuxId,
|
||||||
|
width: Math.floor(
|
||||||
|
gridParticipantHeight * participant.videoAspectRatio * scalar
|
||||||
|
),
|
||||||
|
height: Math.floor(gridParticipantHeight * scalar),
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
...overflowedParticipants.map(participant => {
|
...overflowedParticipants.map(participant => {
|
||||||
if (participant.hasRemoteVideo) {
|
if (participant.hasRemoteVideo) {
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { GroupCallConnectionState } from '../types/Calling';
|
|
||||||
import { LocalizerType } from '../types/Util';
|
|
||||||
|
|
||||||
type PropsType = {
|
|
||||||
connectionState: GroupCallConnectionState;
|
|
||||||
i18n: LocalizerType;
|
|
||||||
};
|
|
||||||
|
|
||||||
// In the future, this component should show toasts when users join or leave. See
|
|
||||||
// DESKTOP-902.
|
|
||||||
export const GroupCallToastManager: React.FC<PropsType> = ({
|
|
||||||
connectionState,
|
|
||||||
i18n,
|
|
||||||
}) => {
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsVisible(connectionState === GroupCallConnectionState.Reconnecting);
|
|
||||||
}, [connectionState, setIsVisible]);
|
|
||||||
|
|
||||||
const message = i18n('callReconnecting');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames('module-ongoing-call__toast', {
|
|
||||||
'module-ongoing-call__toast--hidden': !isVisible,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
60
ts/components/NeedsScreenRecordingPermissionsModal.tsx
Normal file
60
ts/components/NeedsScreenRecordingPermissionsModal.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { Theme } from '../util/theme';
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
openSystemPreferencesAction: () => unknown;
|
||||||
|
toggleScreenRecordingPermissionsDialog: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function focusRef(el: HTMLElement | null) {
|
||||||
|
if (el) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NeedsScreenRecordingPermissionsModal = ({
|
||||||
|
i18n,
|
||||||
|
openSystemPreferencesAction,
|
||||||
|
toggleScreenRecordingPermissionsDialog,
|
||||||
|
}: PropsType): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
i18n={i18n}
|
||||||
|
title={i18n('calling__presenting--permission-title')}
|
||||||
|
theme={Theme.Dark}
|
||||||
|
>
|
||||||
|
<p>{i18n('calling__presenting--macos-permission-description')}</p>
|
||||||
|
<ol style={{ paddingLeft: 16 }}>
|
||||||
|
<li>{i18n('calling__presenting--permission-instruction-step1')}</li>
|
||||||
|
<li>{i18n('calling__presenting--permission-instruction-step2')}</li>
|
||||||
|
<li>{i18n('calling__presenting--permission-instruction-step3')}</li>
|
||||||
|
<li>{i18n('calling__presenting--permission-instruction-step4')}</li>
|
||||||
|
</ol>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button
|
||||||
|
onClick={toggleScreenRecordingPermissionsDialog}
|
||||||
|
ref={focusRef}
|
||||||
|
variant={ButtonVariant.Secondary}
|
||||||
|
>
|
||||||
|
{i18n('calling__presenting--permission-cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
openSystemPreferencesAction();
|
||||||
|
toggleScreenRecordingPermissionsDialog();
|
||||||
|
}}
|
||||||
|
variant={ButtonVariant.Primary}
|
||||||
|
>
|
||||||
|
{i18n('calling__presenting--permission-open')}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
29
ts/hooks/useActivateSpeakerViewOnPresenting.ts
Normal file
29
ts/hooks/useActivateSpeakerViewOnPresenting.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { usePrevious } from '../util/hooks';
|
||||||
|
|
||||||
|
type RemoteParticipant = {
|
||||||
|
hasRemoteVideo: boolean;
|
||||||
|
presenting: boolean;
|
||||||
|
title: string;
|
||||||
|
uuid?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useActivateSpeakerViewOnPresenting(
|
||||||
|
remoteParticipants: ReadonlyArray<RemoteParticipant>,
|
||||||
|
isInSpeakerView: boolean,
|
||||||
|
toggleSpeakerView: () => void
|
||||||
|
): void {
|
||||||
|
const presenterUuid = remoteParticipants.find(
|
||||||
|
participant => participant.presenting
|
||||||
|
)?.uuid;
|
||||||
|
const prevPresenterUuid = usePrevious(presenterUuid, presenterUuid);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevPresenterUuid !== presenterUuid && !isInSpeakerView) {
|
||||||
|
toggleSpeakerView();
|
||||||
|
}
|
||||||
|
}, [isInSpeakerView, presenterUuid, prevPresenterUuid, toggleSpeakerView]);
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
|
|
||||||
|
import { desktopCapturer, ipcRenderer } from 'electron';
|
||||||
import {
|
import {
|
||||||
Call,
|
Call,
|
||||||
CallEndedReason,
|
CallEndedReason,
|
||||||
|
@ -44,6 +45,8 @@ import {
|
||||||
MediaDeviceSettings,
|
MediaDeviceSettings,
|
||||||
GroupCallConnectionState,
|
GroupCallConnectionState,
|
||||||
GroupCallJoinState,
|
GroupCallJoinState,
|
||||||
|
PresentableSource,
|
||||||
|
PresentedSource,
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
import { ConversationModel } from '../models/conversations';
|
import { ConversationModel } from '../models/conversations';
|
||||||
import {
|
import {
|
||||||
|
@ -64,6 +67,7 @@ import {
|
||||||
REQUESTED_VIDEO_HEIGHT,
|
REQUESTED_VIDEO_HEIGHT,
|
||||||
REQUESTED_VIDEO_FRAMERATE,
|
REQUESTED_VIDEO_FRAMERATE,
|
||||||
} from '../calling/constants';
|
} from '../calling/constants';
|
||||||
|
import { notify } from './notify';
|
||||||
|
|
||||||
const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map<
|
const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map<
|
||||||
HttpMethod,
|
HttpMethod,
|
||||||
|
@ -100,12 +104,14 @@ export class CallingClass {
|
||||||
|
|
||||||
private callsByConversation: { [conversationId: string]: Call | GroupCall };
|
private callsByConversation: { [conversationId: string]: Call | GroupCall };
|
||||||
|
|
||||||
|
private hadLocalVideoBeforePresenting?: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.videoCapturer = new GumVideoCapturer(
|
this.videoCapturer = new GumVideoCapturer({
|
||||||
REQUESTED_VIDEO_WIDTH,
|
maxWidth: REQUESTED_VIDEO_WIDTH,
|
||||||
REQUESTED_VIDEO_HEIGHT,
|
maxHeight: REQUESTED_VIDEO_HEIGHT,
|
||||||
REQUESTED_VIDEO_FRAMERATE
|
maxFramerate: REQUESTED_VIDEO_FRAMERATE,
|
||||||
);
|
});
|
||||||
this.videoRenderer = new CanvasVideoRenderer();
|
this.videoRenderer = new CanvasVideoRenderer();
|
||||||
|
|
||||||
this.callsByConversation = {};
|
this.callsByConversation = {};
|
||||||
|
@ -127,6 +133,10 @@ export class CallingClass {
|
||||||
RingRTC.handleLogMessage = this.handleLogMessage.bind(this);
|
RingRTC.handleLogMessage = this.handleLogMessage.bind(this);
|
||||||
RingRTC.handleSendHttpRequest = this.handleSendHttpRequest.bind(this);
|
RingRTC.handleSendHttpRequest = this.handleSendHttpRequest.bind(this);
|
||||||
RingRTC.handleSendCallMessage = this.handleSendCallMessage.bind(this);
|
RingRTC.handleSendCallMessage = this.handleSendCallMessage.bind(this);
|
||||||
|
|
||||||
|
ipcRenderer.on('stop-screen-share', () => {
|
||||||
|
uxActions.setPresenting();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async startCallingLobby(
|
async startCallingLobby(
|
||||||
|
@ -247,7 +257,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
stopCallingLobby(conversationId?: string): void {
|
stopCallingLobby(conversationId?: string): void {
|
||||||
this.disableLocalCamera();
|
this.disableLocalVideo();
|
||||||
this.stopDeviceReselectionTimer();
|
this.stopDeviceReselectionTimer();
|
||||||
this.lastMediaDeviceSettings = undefined;
|
this.lastMediaDeviceSettings = undefined;
|
||||||
|
|
||||||
|
@ -441,7 +451,7 @@ export class CallingClass {
|
||||||
// NOTE: This assumes that only one call is active at a time. For example, if
|
// NOTE: This assumes that only one call is active at a time. For example, if
|
||||||
// there are two calls using the camera, this will disable both of them.
|
// there are two calls using the camera, this will disable both of them.
|
||||||
// That's fine for now, but this will break if that assumption changes.
|
// That's fine for now, but this will break if that assumption changes.
|
||||||
this.disableLocalCamera();
|
this.disableLocalVideo();
|
||||||
|
|
||||||
delete this.callsByConversation[conversationId];
|
delete this.callsByConversation[conversationId];
|
||||||
|
|
||||||
|
@ -457,7 +467,7 @@ export class CallingClass {
|
||||||
|
|
||||||
// NOTE: This assumes only one active call at a time. See comment above.
|
// NOTE: This assumes only one active call at a time. See comment above.
|
||||||
if (localDeviceState.videoMuted) {
|
if (localDeviceState.videoMuted) {
|
||||||
this.disableLocalCamera();
|
this.disableLocalVideo();
|
||||||
} else {
|
} else {
|
||||||
this.videoCapturer.enableCaptureAndSend(groupCall);
|
this.videoCapturer.enableCaptureAndSend(groupCall);
|
||||||
}
|
}
|
||||||
|
@ -689,6 +699,8 @@ export class CallingClass {
|
||||||
demuxId: remoteDeviceState.demuxId,
|
demuxId: remoteDeviceState.demuxId,
|
||||||
hasRemoteAudio: !remoteDeviceState.audioMuted,
|
hasRemoteAudio: !remoteDeviceState.audioMuted,
|
||||||
hasRemoteVideo: !remoteDeviceState.videoMuted,
|
hasRemoteVideo: !remoteDeviceState.videoMuted,
|
||||||
|
presenting: Boolean(remoteDeviceState.presenting),
|
||||||
|
sharingScreen: Boolean(remoteDeviceState.sharingScreen),
|
||||||
speakerTime: normalizeGroupCallTimestamp(
|
speakerTime: normalizeGroupCallTimestamp(
|
||||||
remoteDeviceState.speakerTime
|
remoteDeviceState.speakerTime
|
||||||
),
|
),
|
||||||
|
@ -807,6 +819,8 @@ export class CallingClass {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipcRenderer.send('close-screen-share-controller');
|
||||||
|
|
||||||
if (call instanceof Call) {
|
if (call instanceof Call) {
|
||||||
RingRTC.hangup(call.callId);
|
RingRTC.hangup(call.callId);
|
||||||
} else if (call instanceof GroupCall) {
|
} else if (call instanceof GroupCall) {
|
||||||
|
@ -851,6 +865,101 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setOutgoingVideoIsScreenShare(
|
||||||
|
call: Call | GroupCall,
|
||||||
|
enabled: boolean
|
||||||
|
): void {
|
||||||
|
if (call instanceof Call) {
|
||||||
|
RingRTC.setOutgoingVideoIsScreenShare(call.callId, enabled);
|
||||||
|
// Note: there is no "presenting" API for direct calls.
|
||||||
|
} else if (call instanceof GroupCall) {
|
||||||
|
call.setOutgoingVideoIsScreenShare(enabled);
|
||||||
|
call.setPresenting(enabled);
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPresentingSources(): Promise<Array<PresentableSource>> {
|
||||||
|
const sources = await desktopCapturer.getSources({
|
||||||
|
fetchWindowIcons: true,
|
||||||
|
thumbnailSize: { height: 102, width: 184 },
|
||||||
|
types: ['window', 'screen'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const presentableSources: Array<PresentableSource> = [];
|
||||||
|
|
||||||
|
sources.forEach(source => {
|
||||||
|
// If electron can't retrieve a thumbnail then it won't be able to
|
||||||
|
// present this source so we filter these out.
|
||||||
|
if (source.thumbnail.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
presentableSources.push({
|
||||||
|
appIcon:
|
||||||
|
source.appIcon && !source.appIcon.isEmpty()
|
||||||
|
? source.appIcon.toDataURL()
|
||||||
|
: undefined,
|
||||||
|
id: source.id,
|
||||||
|
name: source.name,
|
||||||
|
thumbnail: source.thumbnail.toDataURL(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return presentableSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPresenting(
|
||||||
|
conversationId: string,
|
||||||
|
hasLocalVideo: boolean,
|
||||||
|
source?: PresentedSource
|
||||||
|
): void {
|
||||||
|
const call = getOwn(this.callsByConversation, conversationId);
|
||||||
|
if (!call) {
|
||||||
|
window.log.warn('Trying to set presenting for a non-existent call');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.videoCapturer.disable();
|
||||||
|
if (source) {
|
||||||
|
this.hadLocalVideoBeforePresenting = hasLocalVideo;
|
||||||
|
this.videoCapturer.enableCaptureAndSend(call, {
|
||||||
|
// 15fps is much nicer but takes up a lot more CPU.
|
||||||
|
maxFramerate: 5,
|
||||||
|
maxHeight: 1080,
|
||||||
|
maxWidth: 1920,
|
||||||
|
screenShareSourceId: source.id,
|
||||||
|
});
|
||||||
|
this.setOutgoingVideo(conversationId, true);
|
||||||
|
} else {
|
||||||
|
this.setOutgoingVideo(
|
||||||
|
conversationId,
|
||||||
|
Boolean(this.hadLocalVideoBeforePresenting) || hasLocalVideo
|
||||||
|
);
|
||||||
|
this.hadLocalVideoBeforePresenting = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPresenting = Boolean(source);
|
||||||
|
this.setOutgoingVideoIsScreenShare(call, isPresenting);
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
ipcRenderer.send('show-screen-share', source.name);
|
||||||
|
notify({
|
||||||
|
icon: 'images/icons/v2/video-solid-24.svg',
|
||||||
|
message: window.i18n('calling__presenting--notification-body'),
|
||||||
|
onNotificationClick: () => {
|
||||||
|
if (this.uxActions) {
|
||||||
|
this.uxActions.setPresenting();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
silent: true,
|
||||||
|
title: window.i18n('calling__presenting--notification-title'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ipcRenderer.send('close-screen-share-controller');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async startDeviceReselectionTimer(): Promise<void> {
|
private async startDeviceReselectionTimer(): Promise<void> {
|
||||||
// Poll once
|
// Poll once
|
||||||
await this.pollForMediaDevices();
|
await this.pollForMediaDevices();
|
||||||
|
@ -1066,7 +1175,7 @@ export class CallingClass {
|
||||||
this.videoCapturer.enableCapture();
|
this.videoCapturer.enableCapture();
|
||||||
}
|
}
|
||||||
|
|
||||||
disableLocalCamera(): void {
|
disableLocalVideo(): void {
|
||||||
this.videoCapturer.disable();
|
this.videoCapturer.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1387,6 +1496,14 @@ export class CallingClass {
|
||||||
hasVideo: call.remoteVideoEnabled,
|
hasVideo: call.remoteVideoEnabled,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
call.handleRemoteSharingScreen = () => {
|
||||||
|
uxActions.remoteSharingScreenChange({
|
||||||
|
conversationId: conversation.id,
|
||||||
|
isSharingScreen: Boolean(call.remoteSharingScreen),
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleLogMessage(
|
private async handleLogMessage(
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
import { CallEndedReason } from 'ringrtc';
|
import { CallEndedReason } from 'ringrtc';
|
||||||
|
import {
|
||||||
|
hasScreenCapturePermission,
|
||||||
|
openSystemPreferences,
|
||||||
|
} from 'mac-screen-capture-permissions';
|
||||||
import { has, omit } from 'lodash';
|
import { has, omit } from 'lodash';
|
||||||
import { getOwn } from '../../util/getOwn';
|
import { getOwn } from '../../util/getOwn';
|
||||||
|
import { getPlatform } from '../selectors/user';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { notify } from '../../services/notify';
|
import { notify } from '../../services/notify';
|
||||||
import { calling } from '../../services/calling';
|
import { calling } from '../../services/calling';
|
||||||
|
@ -18,6 +24,8 @@ import {
|
||||||
GroupCallJoinState,
|
GroupCallJoinState,
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
MediaDeviceSettings,
|
MediaDeviceSettings,
|
||||||
|
PresentedSource,
|
||||||
|
PresentableSource,
|
||||||
} from '../../types/Calling';
|
} from '../../types/Calling';
|
||||||
import { callingTones } from '../../util/callingTones';
|
import { callingTones } from '../../util/callingTones';
|
||||||
import { requestCameraPermissions } from '../../util/callingPermissions';
|
import { requestCameraPermissions } from '../../util/callingPermissions';
|
||||||
|
@ -43,6 +51,8 @@ export type GroupCallParticipantInfoType = {
|
||||||
demuxId: number;
|
demuxId: number;
|
||||||
hasRemoteAudio: boolean;
|
hasRemoteAudio: boolean;
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
|
presenting: boolean;
|
||||||
|
sharingScreen: boolean;
|
||||||
speakerTime?: number;
|
speakerTime?: number;
|
||||||
videoAspectRatio: number;
|
videoAspectRatio: number;
|
||||||
};
|
};
|
||||||
|
@ -53,6 +63,7 @@ export type DirectCallStateType = {
|
||||||
callState?: CallState;
|
callState?: CallState;
|
||||||
callEndedReason?: CallEndedReason;
|
callEndedReason?: CallEndedReason;
|
||||||
isIncoming: boolean;
|
isIncoming: boolean;
|
||||||
|
isSharingScreen?: boolean;
|
||||||
isVideoCall: boolean;
|
isVideoCall: boolean;
|
||||||
hasRemoteVideo?: boolean;
|
hasRemoteVideo?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -73,8 +84,11 @@ export type ActiveCallStateType = {
|
||||||
isInSpeakerView: boolean;
|
isInSpeakerView: boolean;
|
||||||
joinedAt?: number;
|
joinedAt?: number;
|
||||||
pip: boolean;
|
pip: boolean;
|
||||||
|
presentingSource?: PresentedSource;
|
||||||
|
presentingSourcesAvailable?: Array<PresentableSource>;
|
||||||
safetyNumberChangedUuids: Array<string>;
|
safetyNumberChangedUuids: Array<string>;
|
||||||
settingsDialogOpen: boolean;
|
settingsDialogOpen: boolean;
|
||||||
|
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
||||||
showParticipantsList: boolean;
|
showParticipantsList: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -160,6 +174,11 @@ export type RemoteVideoChangeType = {
|
||||||
hasVideo: boolean;
|
hasVideo: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RemoteSharingScreenChangeType = {
|
||||||
|
conversationId: string;
|
||||||
|
isSharingScreen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type SetLocalAudioType = {
|
export type SetLocalAudioType = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
@ -236,10 +255,15 @@ const OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
||||||
const PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED =
|
const PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED =
|
||||||
'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
|
'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
|
||||||
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
|
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
|
||||||
|
const REMOTE_SHARING_SCREEN_CHANGE = 'calling/REMOTE_SHARING_SCREEN_CHANGE';
|
||||||
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||||
const RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
|
const RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
|
||||||
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||||
|
const SET_PRESENTING = 'calling/SET_PRESENTING';
|
||||||
|
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
|
||||||
|
const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
|
||||||
|
'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
|
||||||
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
|
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
|
||||||
const TOGGLE_PARTICIPANTS = 'calling/TOGGLE_PARTICIPANTS';
|
const TOGGLE_PARTICIPANTS = 'calling/TOGGLE_PARTICIPANTS';
|
||||||
const TOGGLE_PIP = 'calling/TOGGLE_PIP';
|
const TOGGLE_PIP = 'calling/TOGGLE_PIP';
|
||||||
|
@ -326,6 +350,11 @@ type RefreshIODevicesActionType = {
|
||||||
payload: MediaDeviceSettings;
|
payload: MediaDeviceSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RemoteSharingScreenChangeActionType = {
|
||||||
|
type: 'calling/REMOTE_SHARING_SCREEN_CHANGE';
|
||||||
|
payload: RemoteSharingScreenChangeType;
|
||||||
|
};
|
||||||
|
|
||||||
type RemoteVideoChangeActionType = {
|
type RemoteVideoChangeActionType = {
|
||||||
type: 'calling/REMOTE_VIDEO_CHANGE';
|
type: 'calling/REMOTE_VIDEO_CHANGE';
|
||||||
payload: RemoteVideoChangeType;
|
payload: RemoteVideoChangeType;
|
||||||
|
@ -345,6 +374,16 @@ type SetLocalVideoFulfilledActionType = {
|
||||||
payload: SetLocalVideoType;
|
payload: SetLocalVideoType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SetPresentingFulfilledActionType = {
|
||||||
|
type: 'calling/SET_PRESENTING';
|
||||||
|
payload?: PresentedSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetPresentingSourcesActionType = {
|
||||||
|
type: 'calling/SET_PRESENTING_SOURCES';
|
||||||
|
payload: Array<PresentableSource>;
|
||||||
|
};
|
||||||
|
|
||||||
type ShowCallLobbyActionType = {
|
type ShowCallLobbyActionType = {
|
||||||
type: 'calling/SHOW_CALL_LOBBY';
|
type: 'calling/SHOW_CALL_LOBBY';
|
||||||
payload: ShowCallLobbyType;
|
payload: ShowCallLobbyType;
|
||||||
|
@ -355,6 +394,10 @@ type StartDirectCallActionType = {
|
||||||
payload: StartDirectCallType;
|
payload: StartDirectCallType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ToggleNeedsScreenRecordingPermissionsActionType = {
|
||||||
|
type: 'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
|
||||||
|
};
|
||||||
|
|
||||||
type ToggleParticipantsActionType = {
|
type ToggleParticipantsActionType = {
|
||||||
type: 'calling/TOGGLE_PARTICIPANTS';
|
type: 'calling/TOGGLE_PARTICIPANTS';
|
||||||
};
|
};
|
||||||
|
@ -387,14 +430,18 @@ export type CallingActionType =
|
||||||
| OutgoingCallActionType
|
| OutgoingCallActionType
|
||||||
| PeekNotConnectedGroupCallFulfilledActionType
|
| PeekNotConnectedGroupCallFulfilledActionType
|
||||||
| RefreshIODevicesActionType
|
| RefreshIODevicesActionType
|
||||||
|
| RemoteSharingScreenChangeActionType
|
||||||
| RemoteVideoChangeActionType
|
| RemoteVideoChangeActionType
|
||||||
| ReturnToActiveCallActionType
|
| ReturnToActiveCallActionType
|
||||||
| SetLocalAudioActionType
|
| SetLocalAudioActionType
|
||||||
| SetLocalVideoFulfilledActionType
|
| SetLocalVideoFulfilledActionType
|
||||||
|
| SetPresentingSourcesActionType
|
||||||
| ShowCallLobbyActionType
|
| ShowCallLobbyActionType
|
||||||
| StartDirectCallActionType
|
| StartDirectCallActionType
|
||||||
|
| ToggleNeedsScreenRecordingPermissionsActionType
|
||||||
| ToggleParticipantsActionType
|
| ToggleParticipantsActionType
|
||||||
| TogglePipActionType
|
| TogglePipActionType
|
||||||
|
| SetPresentingFulfilledActionType
|
||||||
| ToggleSettingsActionType
|
| ToggleSettingsActionType
|
||||||
| ToggleSpeakerViewActionType;
|
| ToggleSpeakerViewActionType;
|
||||||
|
|
||||||
|
@ -438,6 +485,7 @@ function callStateChange(
|
||||||
}
|
}
|
||||||
if (callState === CallState.Ended) {
|
if (callState === CallState.Ended) {
|
||||||
await callingTones.playEndCall();
|
await callingTones.playEndCall();
|
||||||
|
ipcRenderer.send('close-screen-share-controller');
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -519,10 +567,59 @@ function declineCall(payload: DeclineCallType): DeclineCallActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPresentingSources(): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
| SetPresentingSourcesActionType
|
||||||
|
| ToggleNeedsScreenRecordingPermissionsActionType
|
||||||
|
> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
// We check if the user has permissions first before calling desktopCapturer
|
||||||
|
// Next we call getPresentingSources so that one gets the prompt for permissions,
|
||||||
|
// if necessary.
|
||||||
|
// Finally, we have the if statement which shows the modal, if needed.
|
||||||
|
// It is in this exact order so that during first-time-use one will be
|
||||||
|
// prompted for permissions and if they so happen to deny we can still
|
||||||
|
// capture that state correctly.
|
||||||
|
const platform = getPlatform(getState());
|
||||||
|
const needsPermission =
|
||||||
|
platform === 'darwin' && !hasScreenCapturePermission();
|
||||||
|
|
||||||
|
const sources = await calling.getPresentingSources();
|
||||||
|
|
||||||
|
if (needsPermission) {
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_PRESENTING_SOURCES,
|
||||||
|
payload: sources,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function groupCallStateChange(
|
function groupCallStateChange(
|
||||||
payload: GroupCallStateChangeArgumentType
|
payload: GroupCallStateChangeArgumentType
|
||||||
): ThunkAction<void, RootStateType, unknown, GroupCallStateChangeActionType> {
|
): ThunkAction<void, RootStateType, unknown, GroupCallStateChangeActionType> {
|
||||||
return (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
|
let didSomeoneStartPresenting: boolean;
|
||||||
|
const activeCall = getActiveCall(getState().calling);
|
||||||
|
if (activeCall?.callMode === CallMode.Group) {
|
||||||
|
const wasSomeonePresenting = activeCall.remoteParticipants.some(
|
||||||
|
participant => participant.presenting
|
||||||
|
);
|
||||||
|
const isSomeonePresenting = payload.remoteParticipants.some(
|
||||||
|
participant => participant.presenting
|
||||||
|
);
|
||||||
|
didSomeoneStartPresenting = !wasSomeonePresenting && isSomeonePresenting;
|
||||||
|
} else {
|
||||||
|
didSomeoneStartPresenting = false;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: GROUP_CALL_STATE_CHANGE,
|
type: GROUP_CALL_STATE_CHANGE,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -530,6 +627,10 @@ function groupCallStateChange(
|
||||||
ourUuid: getState().user.ourUuid,
|
ourUuid: getState().user.ourUuid,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (didSomeoneStartPresenting) {
|
||||||
|
callingTones.someonePresenting();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -601,6 +702,17 @@ function receiveIncomingCall(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSystemPreferencesAction(): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
never
|
||||||
|
> {
|
||||||
|
return () => {
|
||||||
|
openSystemPreferences();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function outgoingCall(payload: StartDirectCallType): OutgoingCallActionType {
|
function outgoingCall(payload: StartDirectCallType): OutgoingCallActionType {
|
||||||
callingTones.playRingtone();
|
callingTones.playRingtone();
|
||||||
|
|
||||||
|
@ -694,6 +806,15 @@ function refreshIODevices(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function remoteSharingScreenChange(
|
||||||
|
payload: RemoteSharingScreenChangeType
|
||||||
|
): RemoteSharingScreenChangeActionType {
|
||||||
|
return {
|
||||||
|
type: REMOTE_SHARING_SCREEN_CHANGE,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function remoteVideoChange(
|
function remoteVideoChange(
|
||||||
payload: RemoteVideoChangeType
|
payload: RemoteVideoChangeType
|
||||||
): RemoteVideoChangeActionType {
|
): RemoteVideoChangeActionType {
|
||||||
|
@ -764,7 +885,7 @@ function setLocalVideo(
|
||||||
} else if (payload.enabled) {
|
} else if (payload.enabled) {
|
||||||
calling.enableLocalCamera();
|
calling.enableLocalCamera();
|
||||||
} else {
|
} else {
|
||||||
calling.disableLocalCamera();
|
calling.disableLocalVideo();
|
||||||
}
|
}
|
||||||
({ enabled } = payload);
|
({ enabled } = payload);
|
||||||
} else {
|
} else {
|
||||||
|
@ -797,6 +918,35 @@ function setGroupCallVideoRequest(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setPresenting(
|
||||||
|
sourceToPresent?: PresentedSource
|
||||||
|
): ThunkAction<void, RootStateType, unknown, SetPresentingFulfilledActionType> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const callingState = getState().calling;
|
||||||
|
const { activeCallState } = callingState;
|
||||||
|
const activeCall = getActiveCall(callingState);
|
||||||
|
if (!activeCall || !activeCallState) {
|
||||||
|
window.log.warn('Trying to present when no call is active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
calling.setPresenting(
|
||||||
|
activeCall.conversationId,
|
||||||
|
activeCallState.hasLocalVideo,
|
||||||
|
sourceToPresent
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_PRESENTING,
|
||||||
|
payload: sourceToPresent,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sourceToPresent) {
|
||||||
|
await callingTones.someonePresenting();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function startCallingLobby(
|
function startCallingLobby(
|
||||||
payload: StartCallingLobbyType
|
payload: StartCallingLobbyType
|
||||||
): ThunkAction<void, RootStateType, unknown, never> {
|
): ThunkAction<void, RootStateType, unknown, never> {
|
||||||
|
@ -857,6 +1007,12 @@ function togglePip(): TogglePipActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleScreenRecordingPermissionsDialog(): ToggleNeedsScreenRecordingPermissionsActionType {
|
||||||
|
return {
|
||||||
|
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSettings(): ToggleSettingsActionType {
|
function toggleSettings(): ToggleSettingsActionType {
|
||||||
return {
|
return {
|
||||||
type: TOGGLE_SETTINGS,
|
type: TOGGLE_SETTINGS,
|
||||||
|
@ -871,31 +1027,36 @@ function toggleSpeakerView(): ToggleSpeakerViewActionType {
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
acceptCall,
|
acceptCall,
|
||||||
cancelCall,
|
|
||||||
callStateChange,
|
callStateChange,
|
||||||
|
cancelCall,
|
||||||
changeIODevice,
|
changeIODevice,
|
||||||
closeNeedPermissionScreen,
|
closeNeedPermissionScreen,
|
||||||
declineCall,
|
declineCall,
|
||||||
|
getPresentingSources,
|
||||||
groupCallStateChange,
|
groupCallStateChange,
|
||||||
hangUp,
|
hangUp,
|
||||||
keyChanged,
|
|
||||||
keyChangeOk,
|
keyChangeOk,
|
||||||
receiveIncomingCall,
|
keyChanged,
|
||||||
|
openSystemPreferencesAction,
|
||||||
outgoingCall,
|
outgoingCall,
|
||||||
peekNotConnectedGroupCall,
|
peekNotConnectedGroupCall,
|
||||||
|
receiveIncomingCall,
|
||||||
refreshIODevices,
|
refreshIODevices,
|
||||||
|
remoteSharingScreenChange,
|
||||||
remoteVideoChange,
|
remoteVideoChange,
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
setLocalPreview,
|
|
||||||
setRendererCanvas,
|
|
||||||
setLocalAudio,
|
|
||||||
setLocalVideo,
|
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
startCallingLobby,
|
setLocalAudio,
|
||||||
|
setLocalPreview,
|
||||||
|
setLocalVideo,
|
||||||
|
setPresenting,
|
||||||
|
setRendererCanvas,
|
||||||
showCallLobby,
|
showCallLobby,
|
||||||
startCall,
|
startCall,
|
||||||
|
startCallingLobby,
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
togglePip,
|
togglePip,
|
||||||
|
toggleScreenRecordingPermissionsDialog,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
toggleSpeakerView,
|
toggleSpeakerView,
|
||||||
};
|
};
|
||||||
|
@ -1270,6 +1431,26 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === REMOTE_SHARING_SCREEN_CHANGE) {
|
||||||
|
const { conversationId, isSharingScreen } = action.payload;
|
||||||
|
const call = getOwn(state.callsByConversation, conversationId);
|
||||||
|
if (call?.callMode !== CallMode.Direct) {
|
||||||
|
window.log.warn('Cannot update remote video for a non-direct call');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
callsByConversation: {
|
||||||
|
...callsByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...call,
|
||||||
|
isSharingScreen,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === REMOTE_VIDEO_CHANGE) {
|
if (action.type === REMOTE_VIDEO_CHANGE) {
|
||||||
const { conversationId, hasVideo } = action.payload;
|
const { conversationId, hasVideo } = action.payload;
|
||||||
const call = getOwn(state.callsByConversation, conversationId);
|
const call = getOwn(state.callsByConversation, conversationId);
|
||||||
|
@ -1427,6 +1608,59 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === SET_PRESENTING) {
|
||||||
|
const { activeCallState } = state;
|
||||||
|
if (!activeCallState) {
|
||||||
|
window.log.warn('Cannot toggle presenting when there is no active call');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeCallState: {
|
||||||
|
...activeCallState,
|
||||||
|
presentingSource: action.payload,
|
||||||
|
presentingSourcesAvailable: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === SET_PRESENTING_SOURCES) {
|
||||||
|
const { activeCallState } = state;
|
||||||
|
if (!activeCallState) {
|
||||||
|
window.log.warn(
|
||||||
|
'Cannot set presenting sources when there is no active call'
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeCallState: {
|
||||||
|
...activeCallState,
|
||||||
|
presentingSourcesAvailable: action.payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS) {
|
||||||
|
const { activeCallState } = state;
|
||||||
|
if (!activeCallState) {
|
||||||
|
window.log.warn(
|
||||||
|
'Cannot set presenting sources when there is no active call'
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeCallState: {
|
||||||
|
...activeCallState,
|
||||||
|
showNeedsScreenRecordingPermissionsWarning: !activeCallState.showNeedsScreenRecordingPermissionsWarning,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === TOGGLE_SPEAKER_VIEW) {
|
if (action.type === TOGGLE_SPEAKER_VIEW) {
|
||||||
const { activeCallState } = state;
|
const { activeCallState } = state;
|
||||||
if (!activeCallState) {
|
if (!activeCallState) {
|
||||||
|
|
|
@ -78,7 +78,12 @@ const mapStateToActiveCallProp = (
|
||||||
isInSpeakerView: activeCallState.isInSpeakerView,
|
isInSpeakerView: activeCallState.isInSpeakerView,
|
||||||
joinedAt: activeCallState.joinedAt,
|
joinedAt: activeCallState.joinedAt,
|
||||||
pip: activeCallState.pip,
|
pip: activeCallState.pip,
|
||||||
|
presentingSource: activeCallState.presentingSource,
|
||||||
|
presentingSourcesAvailable: activeCallState.presentingSourcesAvailable,
|
||||||
settingsDialogOpen: activeCallState.settingsDialogOpen,
|
settingsDialogOpen: activeCallState.settingsDialogOpen,
|
||||||
|
showNeedsScreenRecordingPermissionsWarning: Boolean(
|
||||||
|
activeCallState.showNeedsScreenRecordingPermissionsWarning
|
||||||
|
),
|
||||||
showParticipantsList: activeCallState.showParticipantsList,
|
showParticipantsList: activeCallState.showParticipantsList,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,6 +98,9 @@ const mapStateToActiveCallProp = (
|
||||||
remoteParticipants: [
|
remoteParticipants: [
|
||||||
{
|
{
|
||||||
hasRemoteVideo: Boolean(call.hasRemoteVideo),
|
hasRemoteVideo: Boolean(call.hasRemoteVideo),
|
||||||
|
presenting: Boolean(call.isSharingScreen),
|
||||||
|
title: conversation.title,
|
||||||
|
uuid: conversation.uuid,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -119,6 +127,8 @@ const mapStateToActiveCallProp = (
|
||||||
demuxId: remoteParticipant.demuxId,
|
demuxId: remoteParticipant.demuxId,
|
||||||
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
|
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
|
||||||
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
|
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
|
||||||
|
presenting: remoteParticipant.presenting,
|
||||||
|
sharingScreen: remoteParticipant.sharingScreen,
|
||||||
speakerTime: remoteParticipant.speakerTime,
|
speakerTime: remoteParticipant.speakerTime,
|
||||||
videoAspectRatio: remoteParticipant.videoAspectRatio,
|
videoAspectRatio: remoteParticipant.videoAspectRatio,
|
||||||
});
|
});
|
||||||
|
|
|
@ -86,6 +86,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 4 / 3,
|
videoAspectRatio: 4 / 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -129,6 +131,188 @@ describe('calling duck', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('actions', () => {
|
describe('actions', () => {
|
||||||
|
describe('getPresentingSources', () => {
|
||||||
|
beforeEach(function beforeEach() {
|
||||||
|
this.callingServiceGetPresentingSources = this.sandbox
|
||||||
|
.stub(callingService, 'getPresentingSources')
|
||||||
|
.resolves([
|
||||||
|
{
|
||||||
|
id: 'foo.bar',
|
||||||
|
name: 'Foo Bar',
|
||||||
|
thumbnail: 'xyz',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves sources from the calling service', async function test() {
|
||||||
|
const { getPresentingSources } = actions;
|
||||||
|
const dispatch = sinon.spy();
|
||||||
|
await getPresentingSources()(dispatch, getEmptyRootState, null);
|
||||||
|
|
||||||
|
sinon.assert.calledOnce(this.callingServiceGetPresentingSources);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches SET_PRESENTING_SOURCES', async function test() {
|
||||||
|
const { getPresentingSources } = actions;
|
||||||
|
const dispatch = sinon.spy();
|
||||||
|
await getPresentingSources()(dispatch, getEmptyRootState, null);
|
||||||
|
|
||||||
|
sinon.assert.calledOnce(dispatch);
|
||||||
|
sinon.assert.calledWith(dispatch, {
|
||||||
|
type: 'calling/SET_PRESENTING_SOURCES',
|
||||||
|
payload: [
|
||||||
|
{
|
||||||
|
id: 'foo.bar',
|
||||||
|
name: 'Foo Bar',
|
||||||
|
thumbnail: 'xyz',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remoteSharingScreenChange', () => {
|
||||||
|
it("updates whether someone's screen is being shared", () => {
|
||||||
|
const { remoteSharingScreenChange } = actions;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
conversationId: 'fake-direct-call-conversation-id',
|
||||||
|
isSharingScreen: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
...stateWithActiveDirectCall,
|
||||||
|
};
|
||||||
|
const nextState = reducer(state, remoteSharingScreenChange(payload));
|
||||||
|
|
||||||
|
const expectedState = {
|
||||||
|
...stateWithActiveDirectCall,
|
||||||
|
callsByConversation: {
|
||||||
|
'fake-direct-call-conversation-id': {
|
||||||
|
...stateWithActiveDirectCall.callsByConversation[
|
||||||
|
'fake-direct-call-conversation-id'
|
||||||
|
],
|
||||||
|
isSharingScreen: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(nextState, expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPresenting', () => {
|
||||||
|
beforeEach(function beforeEach() {
|
||||||
|
this.callingServiceSetPresenting = this.sandbox.stub(
|
||||||
|
callingService,
|
||||||
|
'setPresenting'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls setPresenting on the calling service', function test() {
|
||||||
|
const { setPresenting } = actions;
|
||||||
|
const dispatch = sinon.spy();
|
||||||
|
const presentedSource = {
|
||||||
|
id: 'window:786',
|
||||||
|
name: 'Application',
|
||||||
|
};
|
||||||
|
const getState = () => ({
|
||||||
|
...getEmptyRootState(),
|
||||||
|
calling: {
|
||||||
|
...stateWithActiveGroupCall,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setPresenting(presentedSource)(dispatch, getState, null);
|
||||||
|
|
||||||
|
sinon.assert.calledOnce(this.callingServiceSetPresenting);
|
||||||
|
sinon.assert.calledWith(
|
||||||
|
this.callingServiceSetPresenting,
|
||||||
|
'fake-group-call-conversation-id',
|
||||||
|
false,
|
||||||
|
presentedSource
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches SET_PRESENTING', () => {
|
||||||
|
const { setPresenting } = actions;
|
||||||
|
const dispatch = sinon.spy();
|
||||||
|
const presentedSource = {
|
||||||
|
id: 'window:786',
|
||||||
|
name: 'Application',
|
||||||
|
};
|
||||||
|
const getState = () => ({
|
||||||
|
...getEmptyRootState(),
|
||||||
|
calling: {
|
||||||
|
...stateWithActiveGroupCall,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setPresenting(presentedSource)(dispatch, getState, null);
|
||||||
|
|
||||||
|
sinon.assert.calledOnce(dispatch);
|
||||||
|
sinon.assert.calledWith(dispatch, {
|
||||||
|
type: 'calling/SET_PRESENTING',
|
||||||
|
payload: presentedSource,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('turns off presenting when no value is passed in', () => {
|
||||||
|
const dispatch = sinon.spy();
|
||||||
|
const { setPresenting } = actions;
|
||||||
|
const presentedSource = {
|
||||||
|
id: 'window:786',
|
||||||
|
name: 'Application',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getState = () => ({
|
||||||
|
...getEmptyRootState(),
|
||||||
|
calling: {
|
||||||
|
...stateWithActiveGroupCall,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setPresenting(presentedSource)(dispatch, getState, null);
|
||||||
|
|
||||||
|
const action = dispatch.getCall(0).args[0];
|
||||||
|
|
||||||
|
const nextState = reducer(getState().calling, action);
|
||||||
|
|
||||||
|
assert.isDefined(nextState.activeCallState);
|
||||||
|
assert.equal(
|
||||||
|
nextState.activeCallState?.presentingSource,
|
||||||
|
presentedSource
|
||||||
|
);
|
||||||
|
assert.isUndefined(
|
||||||
|
nextState.activeCallState?.presentingSourcesAvailable
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the presenting value when one is passed in', () => {
|
||||||
|
const dispatch = sinon.spy();
|
||||||
|
const { setPresenting } = actions;
|
||||||
|
|
||||||
|
const getState = () => ({
|
||||||
|
...getEmptyRootState(),
|
||||||
|
calling: {
|
||||||
|
...stateWithActiveGroupCall,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setPresenting()(dispatch, getState, null);
|
||||||
|
|
||||||
|
const action = dispatch.getCall(0).args[0];
|
||||||
|
|
||||||
|
const nextState = reducer(getState().calling, action);
|
||||||
|
|
||||||
|
assert.isDefined(nextState.activeCallState);
|
||||||
|
assert.isUndefined(nextState.activeCallState?.presentingSource);
|
||||||
|
assert.isUndefined(
|
||||||
|
nextState.activeCallState?.presentingSourcesAvailable
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('acceptCall', () => {
|
describe('acceptCall', () => {
|
||||||
const { acceptCall } = actions;
|
const { acceptCall } = actions;
|
||||||
|
|
||||||
|
@ -403,6 +587,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 4 / 3,
|
videoAspectRatio: 4 / 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -429,6 +615,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 4 / 3,
|
videoAspectRatio: 4 / 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -491,6 +679,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 456,
|
demuxId: 456,
|
||||||
hasRemoteAudio: false,
|
hasRemoteAudio: false,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 16 / 9,
|
videoAspectRatio: 16 / 9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -515,6 +705,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 456,
|
demuxId: 456,
|
||||||
hasRemoteAudio: false,
|
hasRemoteAudio: false,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 16 / 9,
|
videoAspectRatio: 16 / 9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -542,6 +734,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 456,
|
demuxId: 456,
|
||||||
hasRemoteAudio: false,
|
hasRemoteAudio: false,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 16 / 9,
|
videoAspectRatio: 16 / 9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -571,6 +765,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 456,
|
demuxId: 456,
|
||||||
hasRemoteAudio: false,
|
hasRemoteAudio: false,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 16 / 9,
|
videoAspectRatio: 16 / 9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -609,6 +805,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 456,
|
demuxId: 456,
|
||||||
hasRemoteAudio: false,
|
hasRemoteAudio: false,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 16 / 9,
|
videoAspectRatio: 16 / 9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -850,6 +1048,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 4 / 3,
|
videoAspectRatio: 4 / 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -874,6 +1074,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 4 / 3,
|
videoAspectRatio: 4 / 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -925,6 +1127,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 4 / 3,
|
videoAspectRatio: 4 / 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -965,6 +1169,8 @@ describe('calling duck', () => {
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
videoAspectRatio: 4 / 3,
|
videoAspectRatio: 4 / 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -10,16 +10,31 @@ export enum CallMode {
|
||||||
Group = 'Group',
|
Group = 'Group',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PresentableSource = {
|
||||||
|
appIcon?: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
thumbnail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PresentedSource = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
type ActiveCallBaseType = {
|
type ActiveCallBaseType = {
|
||||||
conversation: ConversationType;
|
conversation: ConversationType;
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
isInSpeakerView: boolean;
|
isInSpeakerView: boolean;
|
||||||
|
isSharingScreen?: boolean;
|
||||||
joinedAt?: number;
|
joinedAt?: number;
|
||||||
pip: boolean;
|
pip: boolean;
|
||||||
|
presentingSource?: PresentedSource;
|
||||||
|
presentingSourcesAvailable?: Array<PresentableSource>;
|
||||||
settingsDialogOpen: boolean;
|
settingsDialogOpen: boolean;
|
||||||
|
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
||||||
showParticipantsList: boolean;
|
showParticipantsList: boolean;
|
||||||
showSafetyNumberDialog?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActiveDirectCallType = ActiveCallBaseType & {
|
type ActiveDirectCallType = ActiveCallBaseType & {
|
||||||
|
@ -30,6 +45,9 @@ type ActiveDirectCallType = ActiveCallBaseType & {
|
||||||
remoteParticipants: [
|
remoteParticipants: [
|
||||||
{
|
{
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
|
presenting: boolean;
|
||||||
|
title: string;
|
||||||
|
uuid?: string;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -100,6 +118,8 @@ export type GroupCallRemoteParticipantType = ConversationType & {
|
||||||
demuxId: number;
|
demuxId: number;
|
||||||
hasRemoteAudio: boolean;
|
hasRemoteAudio: boolean;
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
|
presenting: boolean;
|
||||||
|
sharingScreen: boolean;
|
||||||
speakerTime?: number;
|
speakerTime?: number;
|
||||||
videoAspectRatio: number;
|
videoAspectRatio: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -54,6 +54,20 @@ class CallingTones {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
async someonePresenting() {
|
||||||
|
const canPlayTone = await window.getCallRingtoneNotification();
|
||||||
|
if (!canPlayTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tone = new Sound({
|
||||||
|
src: 'sounds/navigation_selection-complete-celebration.ogg',
|
||||||
|
});
|
||||||
|
|
||||||
|
await tone.play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const callingTones = new CallingTones();
|
export const callingTones = new CallingTones();
|
||||||
|
|
12
ts/util/isScreenSharingEnabled.ts
Normal file
12
ts/util/isScreenSharingEnabled.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as RemoteConfig from '../RemoteConfig';
|
||||||
|
|
||||||
|
// We can remove this function once screen sharing has been turned on for everyone
|
||||||
|
export function isScreenSharingEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
RemoteConfig.isEnabled('desktop.worksAtSignal') ||
|
||||||
|
RemoteConfig.isEnabled('desktop.screensharing')
|
||||||
|
);
|
||||||
|
}
|
|
@ -2726,6 +2726,13 @@
|
||||||
"updated": "2020-08-26T00:10:28.628Z",
|
"updated": "2020-08-26T00:10:28.628Z",
|
||||||
"reasonDetail": "isn't react"
|
"reasonDetail": "isn't react"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/execa/node_modules/signal-exit/index.js",
|
||||||
|
"line": " load()",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2021-05-20T20:01:50.505Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/expand-range/node_modules/fill-range/index.js",
|
"path": "node_modules/expand-range/node_modules/fill-range/index.js",
|
||||||
|
@ -2859,6 +2866,13 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-15T00:38:04.183Z"
|
"updated": "2018-09-15T00:38:04.183Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/foreground-child/node_modules/signal-exit/index.js",
|
||||||
|
"line": " load()",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2021-05-20T20:01:50.505Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "node_modules/form-data/lib/form_data.js",
|
"path": "node_modules/form-data/lib/form_data.js",
|
||||||
|
@ -2880,6 +2894,13 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-09-11T17:24:56.124Z"
|
"updated": "2020-09-11T17:24:56.124Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/gauge/node_modules/signal-exit/index.js",
|
||||||
|
"line": " load()",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2021-05-20T20:01:50.505Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "node_modules/global-agent/node_modules/core-js/internals/collection.js",
|
"path": "node_modules/global-agent/node_modules/core-js/internals/collection.js",
|
||||||
|
@ -8702,6 +8723,13 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2019-03-09T00:08:44.242Z"
|
"updated": "2019-03-09T00:08:44.242Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/loud-rejection/node_modules/signal-exit/index.js",
|
||||||
|
"line": " load()",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2021-05-20T20:01:50.505Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "thenify-multiArgs",
|
"rule": "thenify-multiArgs",
|
||||||
"path": "node_modules/make-dir/node_modules/pify/index.js",
|
"path": "node_modules/make-dir/node_modules/pify/index.js",
|
||||||
|
@ -9407,6 +9435,13 @@
|
||||||
"updated": "2021-05-07T20:07:48.358Z",
|
"updated": "2021-05-07T20:07:48.358Z",
|
||||||
"reasonDetail": "isn't jquery"
|
"reasonDetail": "isn't jquery"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/os-locale/node_modules/signal-exit/index.js",
|
||||||
|
"line": " load()",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2021-05-20T20:01:50.505Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "node_modules/pac-proxy-agent/node_modules/socks/build/client/socksclient.js",
|
"path": "node_modules/pac-proxy-agent/node_modules/socks/build/client/socksclient.js",
|
||||||
|
@ -11100,13 +11135,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-09-11T17:24:56.124Z"
|
"updated": "2020-09-11T17:24:56.124Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-load(",
|
|
||||||
"path": "node_modules/proper-lockfile/node_modules/signal-exit/index.js",
|
|
||||||
"line": " load()",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2021-04-06T04:01:59.934Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "eval",
|
"rule": "eval",
|
||||||
"path": "node_modules/protobufjs/dist/light/protobuf.js",
|
"path": "node_modules/protobufjs/dist/light/protobuf.js",
|
||||||
|
@ -12619,13 +12647,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-04-30T22:45:07.878Z"
|
"updated": "2020-04-30T22:45:07.878Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-load(",
|
|
||||||
"path": "node_modules/restore-cursor/node_modules/signal-exit/index.js",
|
|
||||||
"line": " load()",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-04-25T01:47:02.583Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "node_modules/rx-lite-aggregates/rx.lite.aggregates.min.js",
|
"path": "node_modules/rx-lite-aggregates/rx.lite.aggregates.min.js",
|
||||||
|
@ -12866,13 +12887,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-load(",
|
|
||||||
"path": "node_modules/spawn-wrap/node_modules/signal-exit/index.js",
|
|
||||||
"line": " load()",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-04-25T01:47:02.583Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-before(",
|
"rule": "jQuery-before(",
|
||||||
"path": "node_modules/sshpk/lib/dhe.js",
|
"path": "node_modules/sshpk/lib/dhe.js",
|
||||||
|
@ -12930,6 +12944,13 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2021-01-20T22:42:00.662Z"
|
"updated": "2021-01-20T22:42:00.662Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/term-size/node_modules/signal-exit/index.js",
|
||||||
|
"line": " load()",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2021-05-20T20:01:50.505Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-after(",
|
"rule": "jQuery-after(",
|
||||||
"path": "node_modules/test-exclude/node_modules/braces/index.js",
|
"path": "node_modules/test-exclude/node_modules/braces/index.js",
|
||||||
|
@ -13263,13 +13284,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-load(",
|
|
||||||
"path": "node_modules/write-file-atomic/node_modules/signal-exit/index.js",
|
|
||||||
"line": " load()",
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-04-30T22:35:27.860Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "node_modules/xregexp/xregexp-all.js",
|
"path": "node_modules/xregexp/xregexp-all.js",
|
||||||
|
@ -13517,6 +13531,13 @@
|
||||||
"updated": "2020-10-26T19:12:24.410Z",
|
"updated": "2020-10-26T19:12:24.410Z",
|
||||||
"reasonDetail": "Used to get the local video element for rendering."
|
"reasonDetail": "Used to get the local video element for rendering."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/CallingToastManager.js",
|
||||||
|
"line": " const timeoutRef = react_1.useRef(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-05-13T19:40:31.751Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/CaptchaDialog.js",
|
"path": "ts/components/CaptchaDialog.js",
|
||||||
|
|
11
ts/window.d.ts
vendored
11
ts/window.d.ts
vendored
|
@ -84,6 +84,10 @@ import { ConversationModel } from './models/conversations';
|
||||||
import { combineNames } from './util';
|
import { combineNames } from './util';
|
||||||
import { BatcherType } from './util/batcher';
|
import { BatcherType } from './util/batcher';
|
||||||
import { AttachmentList } from './components/conversation/AttachmentList';
|
import { AttachmentList } from './components/conversation/AttachmentList';
|
||||||
|
import {
|
||||||
|
CallingScreenSharingController,
|
||||||
|
PropsType as CallingScreenSharingControllerProps,
|
||||||
|
} from './components/CallingScreenSharingController';
|
||||||
import { CaptionEditor } from './components/CaptionEditor';
|
import { CaptionEditor } from './components/CaptionEditor';
|
||||||
import { ConfirmationDialog } from './components/ConfirmationDialog';
|
import { ConfirmationDialog } from './components/ConfirmationDialog';
|
||||||
import { ContactDetail } from './components/conversation/ContactDetail';
|
import { ContactDetail } from './components/conversation/ContactDetail';
|
||||||
|
@ -147,6 +151,13 @@ declare global {
|
||||||
|
|
||||||
WhatIsThis: WhatIsThis;
|
WhatIsThis: WhatIsThis;
|
||||||
|
|
||||||
|
registerScreenShareControllerRenderer: (
|
||||||
|
f: (
|
||||||
|
component: typeof CallingScreenSharingController,
|
||||||
|
props: CallingScreenSharingControllerProps
|
||||||
|
) => void
|
||||||
|
) => void;
|
||||||
|
|
||||||
attachmentDownloadQueue: Array<MessageModel> | undefined;
|
attachmentDownloadQueue: Array<MessageModel> | undefined;
|
||||||
startupProcessingQueue: StartupQueue | undefined;
|
startupProcessingQueue: StartupQueue | undefined;
|
||||||
baseAttachmentsPath: string;
|
baseAttachmentsPath: string;
|
||||||
|
|
11
ts/windows/screenShare.ts
Normal file
11
ts/windows/screenShare.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// This needs to use window.React & window.ReactDOM since it's
|
||||||
|
// not commonJS compatible.
|
||||||
|
window.registerScreenShareControllerRenderer((Component, props) => {
|
||||||
|
window.ReactDOM.render(
|
||||||
|
window.React.createElement(Component, props),
|
||||||
|
document.getElementById('app')
|
||||||
|
);
|
||||||
|
});
|
|
@ -10,6 +10,7 @@ const context = __dirname;
|
||||||
const { NODE_ENV: mode = 'development' } = process.env;
|
const { NODE_ENV: mode = 'development' } = process.env;
|
||||||
|
|
||||||
const EXTERNAL_MODULE = new Set([
|
const EXTERNAL_MODULE = new Set([
|
||||||
|
'@signalapp/signal-client',
|
||||||
'backbone',
|
'backbone',
|
||||||
'better-sqlite3',
|
'better-sqlite3',
|
||||||
'ffi-napi',
|
'ffi-napi',
|
||||||
|
@ -17,7 +18,7 @@ const EXTERNAL_MODULE = new Set([
|
||||||
'fsevents',
|
'fsevents',
|
||||||
'got',
|
'got',
|
||||||
'jquery',
|
'jquery',
|
||||||
'@signalapp/signal-client',
|
'mac-screen-capture-permissions',
|
||||||
'node-fetch',
|
'node-fetch',
|
||||||
'node-sass',
|
'node-sass',
|
||||||
'pino',
|
'pino',
|
||||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -6006,7 +6006,7 @@ cross-spawn@^5.0.1:
|
||||||
shebang-command "^1.2.0"
|
shebang-command "^1.2.0"
|
||||||
which "^1.2.9"
|
which "^1.2.9"
|
||||||
|
|
||||||
cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||||
|
@ -6910,6 +6910,11 @@ electron-download@^4.1.0:
|
||||||
semver "^5.3.0"
|
semver "^5.3.0"
|
||||||
sumchecker "^2.0.1"
|
sumchecker "^2.0.1"
|
||||||
|
|
||||||
|
electron-is-dev@^1.1.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.2.0.tgz#2e5cea0a1b3ccf1c86f577cee77363ef55deb05e"
|
||||||
|
integrity sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==
|
||||||
|
|
||||||
electron-mocha@8.1.1:
|
electron-mocha@8.1.1:
|
||||||
version "8.1.1"
|
version "8.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/electron-mocha/-/electron-mocha-8.1.1.tgz#e540e7d9ba80a024007a18533ae491c18f9a0ce2"
|
resolved "https://registry.yarnpkg.com/electron-mocha/-/electron-mocha-8.1.1.tgz#e540e7d9ba80a024007a18533ae491c18f9a0ce2"
|
||||||
|
@ -6955,6 +6960,14 @@ electron-to-chromium@^1.3.649:
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.707.tgz#71386d0ceca6727835c33ba31f507f6824d18c35"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.707.tgz#71386d0ceca6727835c33ba31f507f6824d18c35"
|
||||||
integrity sha512-BqddgxNPrcWnbDdJw7SzXVzPmp+oiyjVrc7tkQVaznPGSS9SKZatw6qxoP857M+HbOyyqJQwYQtsuFIMSTNSZA==
|
integrity sha512-BqddgxNPrcWnbDdJw7SzXVzPmp+oiyjVrc7tkQVaznPGSS9SKZatw6qxoP857M+HbOyyqJQwYQtsuFIMSTNSZA==
|
||||||
|
|
||||||
|
electron-util@^0.13.0:
|
||||||
|
version "0.13.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/electron-util/-/electron-util-0.13.1.tgz#ba3b9cb7e5fdb6a51970a01e9070877cf7855ef8"
|
||||||
|
integrity sha512-CvOuAyQPaPtnDp7SspwnT1yTb1yynw6yp4LrZCfEJ7TG/kJFiZW9RqMHlCEFWMn3QNoMkNhGVeCvWJV5NsYyuQ==
|
||||||
|
dependencies:
|
||||||
|
electron-is-dev "^1.1.0"
|
||||||
|
new-github-issue-url "^0.2.1"
|
||||||
|
|
||||||
electron-window@^0.8.0:
|
electron-window@^0.8.0:
|
||||||
version "0.8.1"
|
version "0.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/electron-window/-/electron-window-0.8.1.tgz#16ca187eb4870b0679274fc8299c5960e6ab2c5e"
|
resolved "https://registry.yarnpkg.com/electron-window/-/electron-window-0.8.1.tgz#16ca187eb4870b0679274fc8299c5960e6ab2c5e"
|
||||||
|
@ -7748,6 +7761,21 @@ execa@^1.0.0:
|
||||||
signal-exit "^3.0.0"
|
signal-exit "^3.0.0"
|
||||||
strip-eof "^1.0.0"
|
strip-eof "^1.0.0"
|
||||||
|
|
||||||
|
execa@^2.0.4:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99"
|
||||||
|
integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==
|
||||||
|
dependencies:
|
||||||
|
cross-spawn "^7.0.0"
|
||||||
|
get-stream "^5.0.0"
|
||||||
|
is-stream "^2.0.0"
|
||||||
|
merge-stream "^2.0.0"
|
||||||
|
npm-run-path "^3.0.0"
|
||||||
|
onetime "^5.1.0"
|
||||||
|
p-finally "^2.0.0"
|
||||||
|
signal-exit "^3.0.2"
|
||||||
|
strip-final-newline "^2.0.0"
|
||||||
|
|
||||||
execa@^5.0.0:
|
execa@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376"
|
resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376"
|
||||||
|
@ -8696,6 +8724,13 @@ get-stream@^4.0.0, get-stream@^4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pump "^3.0.0"
|
pump "^3.0.0"
|
||||||
|
|
||||||
|
get-stream@^5.0.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
|
||||||
|
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
|
||||||
|
dependencies:
|
||||||
|
pump "^3.0.0"
|
||||||
|
|
||||||
get-stream@^5.1.0:
|
get-stream@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
|
||||||
|
@ -11450,11 +11485,28 @@ lru-queue@0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
es5-ext "~0.10.2"
|
es5-ext "~0.10.2"
|
||||||
|
|
||||||
|
mac-screen-capture-permissions@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mac-screen-capture-permissions/-/mac-screen-capture-permissions-2.0.0.tgz#fdef314118db4d593a88dd2d7d3e66b175c92f80"
|
||||||
|
integrity sha512-f70KKpx5WhD8mmrAwLeeee31EfSM4p1K7kBBNBVXyfWE7ZQTIbbAF2PxJ0bMsDxyyeX5roBcH+qJYlSTANtCOA==
|
||||||
|
dependencies:
|
||||||
|
electron-util "^0.13.0"
|
||||||
|
execa "^2.0.4"
|
||||||
|
macos-version "^5.2.1"
|
||||||
|
prebuild-install "^6.0.0"
|
||||||
|
|
||||||
macos-release@^2.2.0:
|
macos-release@^2.2.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
|
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
|
||||||
integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==
|
integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==
|
||||||
|
|
||||||
|
macos-version@^5.2.1:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/macos-version/-/macos-version-5.2.1.tgz#056c943aac8edb81d7cafef6445b7ca1d7a2e56e"
|
||||||
|
integrity sha512-OHJU8nTNxHYL1FQhD+nZawWgXKXAqDGr4kluLtaqKO4au3cR41y1mKuVShOU5U4rOYiuPanljq6oFGmV2B9DFA==
|
||||||
|
dependencies:
|
||||||
|
semver "^5.6.0"
|
||||||
|
|
||||||
make-dir@^1.0.0:
|
make-dir@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
|
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
|
||||||
|
@ -12268,6 +12320,11 @@ netmask@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
|
resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
|
||||||
integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
|
integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
|
||||||
|
|
||||||
|
new-github-issue-url@^0.2.1:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/new-github-issue-url/-/new-github-issue-url-0.2.1.tgz#e17be1f665a92de465926603e44b9f8685630c1d"
|
||||||
|
integrity sha512-md4cGoxuT4T4d/HDOXbrUHkTKrp/vp+m3aOA7XXVYwNsUNMK49g3SQicTSeV5GIz/5QVGAeYRAOlyp9OvlgsYA==
|
||||||
|
|
||||||
next-tick@1, next-tick@^1.0.0, next-tick@~1.0.0:
|
next-tick@1, next-tick@^1.0.0, next-tick@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||||
|
@ -12624,6 +12681,13 @@ npm-run-path@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key "^2.0.0"
|
path-key "^2.0.0"
|
||||||
|
|
||||||
|
npm-run-path@^3.0.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5"
|
||||||
|
integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==
|
||||||
|
dependencies:
|
||||||
|
path-key "^3.0.0"
|
||||||
|
|
||||||
npm-run-path@^4.0.1:
|
npm-run-path@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||||
|
@ -13065,6 +13129,11 @@ p-finally@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||||
|
|
||||||
|
p-finally@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561"
|
||||||
|
integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==
|
||||||
|
|
||||||
p-is-promise@^1.1.0:
|
p-is-promise@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
|
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
|
||||||
|
@ -13853,6 +13922,26 @@ postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
supports-color "^6.1.0"
|
supports-color "^6.1.0"
|
||||||
|
|
||||||
|
prebuild-install@^6.0.0:
|
||||||
|
version "6.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.2.tgz#6ce5fc5978feba5d3cbffedca0682b136a0b5bff"
|
||||||
|
integrity sha512-PzYWIKZeP+967WuKYXlTOhYBgGOvTRSfaKI89XnfJ0ansRAH7hDU45X+K+FZeI1Wb/7p/NnuctPH3g0IqKUuSQ==
|
||||||
|
dependencies:
|
||||||
|
detect-libc "^1.0.3"
|
||||||
|
expand-template "^2.0.3"
|
||||||
|
github-from-package "0.0.0"
|
||||||
|
minimist "^1.2.3"
|
||||||
|
mkdirp-classic "^0.5.3"
|
||||||
|
napi-build-utils "^1.0.1"
|
||||||
|
node-abi "^2.21.0"
|
||||||
|
noop-logger "^0.1.1"
|
||||||
|
npmlog "^4.0.1"
|
||||||
|
pump "^3.0.0"
|
||||||
|
rc "^1.2.7"
|
||||||
|
simple-get "^3.0.3"
|
||||||
|
tar-fs "^2.0.0"
|
||||||
|
tunnel-agent "^0.6.0"
|
||||||
|
|
||||||
prebuild-install@^6.1.1:
|
prebuild-install@^6.1.1:
|
||||||
version "6.1.1"
|
version "6.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.1.tgz#6754fa6c0d55eced7f9e14408ff9e4cba6f097b4"
|
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.1.tgz#6754fa6c0d55eced7f9e14408ff9e4cba6f097b4"
|
||||||
|
@ -15467,9 +15556,9 @@ rimraf@^3.0.2, rimraf@~3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#b43c6b728d62b6d386d95705e128f32f44edb650":
|
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#17b22fc9d47605867608193202c54be06bce6f56":
|
||||||
version "2.9.4"
|
version "2.10.1"
|
||||||
resolved "https://github.com/signalapp/signal-ringrtc-node.git#b43c6b728d62b6d386d95705e128f32f44edb650"
|
resolved "https://github.com/signalapp/signal-ringrtc-node.git#17b22fc9d47605867608193202c54be06bce6f56"
|
||||||
|
|
||||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
|
|
Loading…
Add table
Reference in a new issue