Add screensharing behind a feature flag

This commit is contained in:
Josh Perez 2021-05-20 17:54:03 -04:00 committed by Scott Nonnenberg
parent 7c7f7ee5a0
commit ceffc2380c
49 changed files with 2044 additions and 164 deletions

View File

@ -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
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
## mac-screen-capture-permissions
License: MIT
## memoizee
ISC License

View File

@ -691,6 +691,10 @@
"message": "About Signal Desktop",
"description": "Item under the Help menu, which opens a small about window"
},
"screenShareWindow": {
"message": "Sharing screen",
"description": "Title for screen sharing window"
},
"speech": {
"message": "Speech",
"description": "Item under the Edit menu, with 'start/stop speaking' items below it"
@ -1249,6 +1253,18 @@
"message": "Unmute mic",
"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": {
"message": "Your camera is off",
"description": "Label in the calling lobby indicating that your camera is off"
@ -1361,6 +1377,84 @@
"message": "Scroll down",
"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": {
"message": "Always relay calls",
"description": "Description of the always relay calls setting"
@ -3240,6 +3334,22 @@
"message": "Leave call",
"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": {
"message": "Video",
"description": "Label for video input selector"

View File

@ -23,18 +23,29 @@ function _createPermissionHandler(userConfig) {
return (webContents, permission, callback, details) => {
// We default 'media' permission to false, but the user can override that for
// the microphone and camera.
if (
permission === 'media' &&
details.mediaTypes.includes('audio') &&
userConfig.get('mediaPermissions')
) {
return callback(true);
}
if (
permission === 'media' &&
details.mediaTypes.includes('video') &&
userConfig.get('mediaCameraPermissions')
) {
if (permission === 'media') {
if (
details.mediaTypes.includes('audio') ||
details.mediaTypes.includes('video')
) {
if (
details.mediaTypes.includes('audio') &&
userConfig.get('mediaPermissions')
) {
return callback(true);
}
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);
}

View 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
View File

@ -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;
function showAbout() {
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) => {
if (tray) {
tray.updateIcon(unreadCount);

View File

@ -106,6 +106,7 @@
"linkify-it": "2.2.0",
"lodash": "4.17.21",
"lru-cache": "6.0.0",
"mac-screen-capture-permissions": "2.0.0",
"memoizee": "0.4.14",
"mkdirp": "0.5.2",
"moment": "2.29.1",
@ -147,7 +148,7 @@
"redux-ts-utils": "3.2.2",
"reselect": "4.0.0",
"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",
"sanitize-filename": "1.6.3",
"sanitize.css": "11.0.0",
@ -302,7 +303,8 @@
"asarUnpack": [
"**/*.node",
"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}",
"category": "public.app-category.social-networking",
@ -392,6 +394,7 @@
"config/local-${env.SIGNAL_ENV}.json",
"background.html",
"about.html",
"screenShare.html",
"settings.html",
"permissions_popup.html",
"debug_log.html",
@ -408,6 +411,7 @@
"preload.bundle.js",
"preload_utils.js",
"about_preload.js",
"screenShare_preload.js",
"settings_preload.js",
"permissions_popup_preload.js",
"debug_log_preload.js",
@ -448,6 +452,7 @@
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
"node_modules/@signalapp/signal-client/build/*${platform}*.node",
"node_modules/ringrtc/build/${platform}/**",
"node_modules/mac-screen-capture-permissions/build/Release/*.node",
"!**/node_modules/ffi-napi/deps",
"!**/node_modules/react-dom/*/*.development.js",
"!node_modules/.cache"

View 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.

View 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
View 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
View 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();

Binary file not shown.

View File

@ -5989,6 +5989,19 @@ button.module-image__border-overlay:focus {
$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 {
@ -6286,6 +6299,10 @@ button.module-image__border-overlay:focus {
height: 100%;
transform: rotateY(180deg);
width: 100%;
&--presenting {
transform: inherit;
}
}
&--audio-muted::before {
@ -6323,6 +6340,7 @@ button.module-image__border-overlay:focus {
}
&__toast {
@include button-reset();
@include font-body-1-bold;
background-color: $color-gray-75;
border-radius: 8px;
@ -6649,6 +6667,17 @@ button.module-image__border-overlay:focus {
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 {

View 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;
}
}

View File

@ -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;
}
}
}

View File

@ -31,6 +31,8 @@
@import './components/Avatar.scss';
@import './components/AvatarInput.scss';
@import './components/Button.scss';
@import './components/CallingScreenSharingController.scss';
@import './components/CallingSelectPresentingSourcesModal.scss';
@import './components/ContactPill.scss';
@import './components/ContactPills.scss';
@import './components/ContactSpoofingReviewDialog.scss';

View File

@ -11,8 +11,10 @@ export type ConfigKeyType =
| 'desktop.gv2'
| 'desktop.mandatoryProfileSharing'
| 'desktop.messageRequests'
| 'desktop.screensharing'
| 'desktop.storage'
| 'desktop.storageWrite3'
| 'desktop.worksAtSignal'
| 'global.groupsv2.maxGroupSize'
| 'global.groupsv2.groupSizeHardLimit';
type ConfigValueType = {

View File

@ -68,6 +68,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
declineCall: action('decline-call'),
getGroupCallVideoFrameSource: (_: string, demuxId: number) =>
fakeGetGroupCallVideoFrameSource(demuxId),
getPresentingSources: action('get-presenting-sources'),
hangUp: action('hang-up'),
i18n,
keyChangeOk: action('key-change-ok'),
@ -78,16 +79,21 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
}),
uuid: 'cb0dd0c8-7393-41e9-a0aa-d631c4109541',
},
openSystemPreferencesAction: action('open-system-preferences-action'),
renderDeviceSelection: () => <div />,
renderSafetyNumberViewer: (_: SafetyNumberViewerProps) => <div />,
setGroupCallVideoRequest: action('set-group-call-video-request'),
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),
setPresenting: action('toggle-presenting'),
setRendererCanvas: action('set-renderer-canvas'),
startCall: action('start-call'),
toggleParticipants: action('toggle-participants'),
togglePip: action('toggle-pip'),
toggleScreenRecordingPermissionsDialog: action(
'toggle-screen-recording-permissions-dialog'
),
toggleSettings: action('toggle-settings'),
toggleSpeakerView: action('toggle-speaker-view'),
});
@ -104,7 +110,9 @@ story.add('Ongoing Direct Call', () => (
callMode: CallMode.Direct,
callState: CallState.Accepted,
peekedParticipants: [],
remoteParticipants: [{ hasRemoteVideo: true }],
remoteParticipants: [
{ hasRemoteVideo: true, presenting: false, title: 'Remy' },
],
},
})}
/>
@ -148,7 +156,9 @@ story.add('Call Request Needed', () => (
callMode: CallMode.Direct,
callState: CallState.Accepted,
peekedParticipants: [],
remoteParticipants: [{ hasRemoteVideo: true }],
remoteParticipants: [
{ hasRemoteVideo: true, presenting: false, title: 'Mike' },
],
},
})}
/>

View File

@ -6,6 +6,7 @@ import { CallNeedPermissionScreen } from './CallNeedPermissionScreen';
import { CallScreen } from './CallScreen';
import { CallingLobby } from './CallingLobby';
import { CallingParticipantsList } from './CallingParticipantsList';
import { CallingSelectPresentingSourcesModal } from './CallingSelectPresentingSourcesModal';
import { CallingPip } from './CallingPip';
import { IncomingCallBar } from './IncomingCallBar';
import {
@ -19,6 +20,7 @@ import {
CallState,
GroupCallJoinState,
GroupCallVideoRequest,
PresentedSource,
VideoFrameSource,
} from '../types/Calling';
import { ConversationType } from '../state/ducks/conversations';
@ -52,6 +54,7 @@ export type PropsType = {
conversationId: string,
demuxId: number
) => VideoFrameSource;
getPresentingSources: () => void;
incomingCall?: {
call: DirectCallStateType;
conversation: ConversationType;
@ -65,13 +68,16 @@ export type PropsType = {
declineCall: (_: DeclineCallType) => void;
i18n: LocalizerType;
me: MeType;
openSystemPreferencesAction: () => unknown;
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setPresenting: (_?: PresentedSource) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
hangUp: (_: HangUpType) => void;
togglePip: () => void;
toggleScreenRecordingPermissionsDialog: () => unknown;
toggleSettings: () => void;
toggleSpeakerView: () => void;
};
@ -89,17 +95,21 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
i18n,
keyChangeOk,
getGroupCallVideoFrameSource,
getPresentingSources,
me,
openSystemPreferencesAction,
renderDeviceSelection,
renderSafetyNumberViewer,
setGroupCallVideoRequest,
setLocalAudio,
setLocalPreview,
setLocalVideo,
setPresenting,
setRendererCanvas,
startCall,
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSettings,
toggleSpeakerView,
}) => {
@ -110,6 +120,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
joinedAt,
peekedParticipants,
pip,
presentingSourcesAvailable,
settingsDialogOpen,
showParticipantsList,
} = activeCall;
@ -238,13 +249,15 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
? [
...activeCall.remoteParticipants.map(participant => ({
...participant,
hasAudio: participant.hasRemoteAudio,
hasVideo: participant.hasRemoteVideo,
hasRemoteAudio: participant.hasRemoteAudio,
hasRemoteVideo: participant.hasRemoteVideo,
presenting: participant.presenting,
})),
{
...me,
hasAudio: hasLocalAudio,
hasVideo: hasLocalVideo,
hasRemoteAudio: hasLocalAudio,
hasRemoteVideo: hasLocalVideo,
presenting: Boolean(activeCall.presentingSource),
},
]
: [];
@ -253,22 +266,35 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
<>
<CallScreen
activeCall={activeCall}
getPresentingSources={getPresentingSources}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
hangUp={hangUp}
i18n={i18n}
joinedAt={joinedAt}
me={me}
openSystemPreferencesAction={openSystemPreferencesAction}
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
setLocalPreview={setLocalPreview}
setRendererCanvas={setRendererCanvas}
setLocalAudio={setLocalAudio}
setLocalVideo={setLocalVideo}
setPresenting={setPresenting}
stickyControls={showParticipantsList}
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
toggleParticipants={toggleParticipants}
togglePip={togglePip}
toggleSettings={toggleSettings}
toggleSpeakerView={toggleSpeakerView}
/>
{presentingSourcesAvailable && presentingSourcesAvailable.length ? (
<CallingSelectPresentingSourcesModal
i18n={i18n}
presentingSourcesAvailable={presentingSourcesAvailable}
setPresenting={setPresenting}
/>
) : null}
{settingsDialogOpen && renderDeviceSelection()}
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
<CallingParticipantsList

View File

@ -74,10 +74,14 @@ const createActiveDirectCallProp = (
'hasRemoteVideo',
Boolean(overrideProps.hasRemoteVideo)
),
presenting: false,
title: 'test',
},
] as [
{
hasRemoteVideo: boolean;
presenting: boolean;
title: string;
}
],
});
@ -137,6 +141,7 @@ const createProps = (
): PropsType => ({
activeCall: createActiveCallProp(overrideProps),
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
getPresentingSources: action('get-presenting-sources'),
hangUp: action('hang-up'),
i18n,
me: {
@ -145,14 +150,19 @@ const createProps = (
profileName: 'Morty Smith',
title: 'Morty Smith',
},
openSystemPreferencesAction: action('open-system-preferences-action'),
setGroupCallVideoRequest: action('set-group-call-video-request'),
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),
setPresenting: action('toggle-presenting'),
setRendererCanvas: action('set-renderer-canvas'),
stickyControls: boolean('stickyControls', false),
toggleParticipants: action('toggle-participants'),
togglePip: action('toggle-pip'),
toggleScreenRecordingPermissionsDialog: action(
'toggle-screen-recording-permissions-dialog'
),
toggleSettings: action('toggle-settings'),
toggleSpeakerView: action('toggle-speaker-view'),
});
@ -249,6 +259,8 @@ story.add('Group call - 1', () => (
demuxId: 0,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: false,
@ -266,6 +278,8 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
demuxId: index,
hasRemoteAudio: index % 3 !== 0,
hasRemoteVideo: index % 4 !== 0,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
@ -303,6 +317,8 @@ story.add('Group call - reconnecting', () => (
demuxId: 0,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: false,

View File

@ -21,18 +21,23 @@ import {
CallState,
GroupCallConnectionState,
GroupCallVideoRequest,
PresentedSource,
VideoFrameSource,
} from '../types/Calling';
import { CallingToastManager } from './CallingToastManager';
import { ColorType } from '../types/Colors';
import { LocalizerType } from '../types/Util';
import { missingCaseError } from '../util/missingCaseError';
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
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 = {
activeCall: ActiveCallType;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
getPresentingSources: () => void;
hangUp: (_: HangUpType) => void;
i18n: LocalizerType;
joinedAt?: number;
@ -44,14 +49,17 @@ export type PropsType = {
profileName?: string;
title: string;
};
openSystemPreferencesAction: () => unknown;
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setPresenting: (_?: PresentedSource) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
stickyControls: boolean;
toggleParticipants: () => void;
togglePip: () => void;
toggleScreenRecordingPermissionsDialog: () => unknown;
toggleSettings: () => void;
toggleSpeakerView: () => void;
};
@ -59,18 +67,22 @@ export type PropsType = {
export const CallScreen: React.FC<PropsType> = ({
activeCall,
getGroupCallVideoFrameSource,
getPresentingSources,
hangUp,
i18n,
joinedAt,
me,
openSystemPreferencesAction,
setGroupCallVideoRequest,
setLocalAudio,
setLocalVideo,
setLocalPreview,
setPresenting,
setRendererCanvas,
stickyControls,
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSettings,
toggleSpeakerView,
}) => {
@ -78,9 +90,19 @@ export const CallScreen: React.FC<PropsType> = ({
conversation,
hasLocalAudio,
hasLocalVideo,
isInSpeakerView,
presentingSource,
remoteParticipants,
showNeedsScreenRecordingPermissionsWarning,
showParticipantsList,
} = activeCall;
useActivateSpeakerViewOnPresenting(
remoteParticipants,
isInSpeakerView,
toggleSpeakerView
);
const toggleAudio = useCallback(() => {
setLocalAudio({
enabled: !hasLocalAudio,
@ -93,6 +115,14 @@ export const CallScreen: React.FC<PropsType> = ({
});
}, [setLocalVideo, hasLocalVideo]);
const togglePresenting = useCallback(() => {
if (presentingSource) {
setPresenting();
} else {
getPresentingSources();
}
}, [getPresentingSources, presentingSource, setPresenting]);
const [acceptedDuration, setAcceptedDuration] = useState<number | null>(null);
const [showControls, setShowControls] = useState(true);
@ -151,7 +181,11 @@ export const CallScreen: React.FC<PropsType> = ({
};
}, [toggleAudio, toggleVideo]);
const hasRemoteVideo = activeCall.remoteParticipants.some(
const currentPresenter = remoteParticipants.find(
participant => participant.presenting
);
const hasRemoteVideo = remoteParticipants.some(
remoteParticipant => remoteParticipant.hasRemoteVideo
);
@ -183,16 +217,22 @@ export const CallScreen: React.FC<PropsType> = ({
case CallMode.Group:
participantCount = activeCall.remoteParticipants.length + 1;
headerMessage = undefined;
headerTitle = activeCall.remoteParticipants.length
? undefined
: i18n('calling__in-this-call--zero');
if (currentPresenter) {
headerTitle = i18n('calling__presenting--person-ongoing', [
currentPresenter.title,
]);
} else if (!activeCall.remoteParticipants.length) {
headerTitle = i18n('calling__in-this-call--zero');
}
isConnected =
activeCall.connectionState === GroupCallConnectionState.Connected;
remoteParticipantsElement = (
<GroupCallRemoteParticipants
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n}
isInSpeakerView={activeCall.isInSpeakerView}
isInSpeakerView={isInSpeakerView}
remoteParticipants={activeCall.remoteParticipants}
setGroupCallVideoRequest={setGroupCallVideoRequest}
/>
@ -206,9 +246,15 @@ export const CallScreen: React.FC<PropsType> = ({
activeCall.callMode === CallMode.Group &&
!activeCall.remoteParticipants.length;
const videoButtonType = hasLocalVideo
? CallingButtonType.VIDEO_ON
: CallingButtonType.VIDEO_OFF;
let videoButtonType: CallingButtonType;
if (presentingSource) {
videoButtonType = CallingButtonType.VIDEO_DISABLED;
} else if (hasLocalVideo) {
videoButtonType = CallingButtonType.VIDEO_ON;
} else {
videoButtonType = CallingButtonType.VIDEO_OFF;
}
const audioButtonType = hasLocalAudio
? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF;
@ -222,6 +268,23 @@ export const CallScreen: React.FC<PropsType> = ({
!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 (
<div
className={classNames(
@ -235,20 +298,24 @@ export const CallScreen: React.FC<PropsType> = ({
}}
role="group"
>
{activeCall.callMode === CallMode.Group ? (
<GroupCallToastManager
connectionState={activeCall.connectionState}
{showNeedsScreenRecordingPermissionsWarning ? (
<NeedsScreenRecordingPermissionsModal
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
i18n={i18n}
openSystemPreferencesAction={openSystemPreferencesAction}
/>
) : null}
<CallingToastManager activeCall={activeCall} i18n={i18n} />
<div
className={classNames('module-ongoing-call__header', controlsFadeClass)}
>
<CallingHeader
canPip
i18n={i18n}
isInSpeakerView={activeCall.isInSpeakerView}
isGroupCall={activeCall.callMode === CallMode.Group}
isInSpeakerView={isInSpeakerView}
isGroupCall={isGroupCall}
message={headerMessage}
participantCount={participantCount}
showParticipantsList={showParticipantsList}
@ -263,7 +330,7 @@ export const CallScreen: React.FC<PropsType> = ({
{hasLocalVideo && isLonelyInGroup ? (
<div className="module-ongoing-call__local-preview-fullsize">
<video
className="module-ongoing-call__footer__local-preview__video"
className={localPreviewVideoClass}
ref={localVideoRef}
autoPlay
/>
@ -308,6 +375,13 @@ export const CallScreen: React.FC<PropsType> = ({
controlsFadeClass
)}
>
{isScreenSharingEnabled() ? (
<CallingButton
buttonType={presentingButtonType}
i18n={i18n}
onClick={togglePresenting}
/>
) : null}
<CallingButton
buttonType={videoButtonType}
i18n={i18n}
@ -333,7 +407,7 @@ export const CallScreen: React.FC<PropsType> = ({
>
{hasLocalVideo && !isLonelyInGroup ? (
<video
className="module-ongoing-call__footer__local-preview__video"
className={localPreviewVideoClass}
ref={localVideoRef}
autoPlay
/>

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -14,11 +14,9 @@ import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
buttonType: select(
'buttonType',
CallingButtonType,
overrideProps.buttonType || CallingButtonType.HANG_UP
),
buttonType:
overrideProps.buttonType ||
select('buttonType', CallingButtonType, CallingButtonType.HANG_UP),
i18n,
onClick: action('on-click'),
tooltipDirection: select(
@ -30,9 +28,16 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
const story = storiesOf('Components/CallingButton', module);
story.add('Default', () => {
const props = createProps();
return <CallingButton {...props} />;
story.add('Kitchen Sink', () => {
return (
<>
{Object.keys(CallingButtonType).map(buttonType => (
<CallingButton
{...createProps({ buttonType: buttonType as CallingButtonType })}
/>
))}
</>
);
});
story.add('Audio On', () => {
@ -83,3 +88,17 @@ story.add('Tooltip right', () => {
});
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} />;
});

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
@ -12,6 +12,9 @@ export enum CallingButtonType {
AUDIO_OFF = 'AUDIO_OFF',
AUDIO_ON = 'AUDIO_ON',
HANG_UP = 'HANG_UP',
PRESENTING_DISABLED = 'PRESENTING_DISABLED',
PRESENTING_OFF = 'PRESENTING_OFF',
PRESENTING_ON = 'PRESENTING_ON',
VIDEO_DISABLED = 'VIDEO_DISABLED',
VIDEO_OFF = 'VIDEO_OFF',
VIDEO_ON = 'VIDEO_ON',
@ -32,9 +35,11 @@ export const CallingButton = ({
}: PropsType): JSX.Element => {
let classNameSuffix = '';
let tooltipContent = '';
let disabled = false;
if (buttonType === CallingButtonType.AUDIO_DISABLED) {
classNameSuffix = 'audio--disabled';
tooltipContent = i18n('calling__button--audio-disabled');
disabled = true;
} else if (buttonType === CallingButtonType.AUDIO_OFF) {
classNameSuffix = 'audio--off';
tooltipContent = i18n('calling__button--audio-on');
@ -44,6 +49,7 @@ export const CallingButton = ({
} else if (buttonType === CallingButtonType.VIDEO_DISABLED) {
classNameSuffix = 'video--disabled';
tooltipContent = i18n('calling__button--video-disabled');
disabled = true;
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
classNameSuffix = 'video--off';
tooltipContent = i18n('calling__button--video-on');
@ -53,6 +59,16 @@ export const CallingButton = ({
} else if (buttonType === CallingButtonType.HANG_UP) {
classNameSuffix = '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(
@ -68,9 +84,10 @@ export const CallingButton = ({
>
<button
aria-label={tooltipContent}
type="button"
className={className}
disabled={disabled}
onClick={onClick}
type="button"
>
<div />
</button>

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -23,6 +23,8 @@ function createParticipant(
demuxId: 2,
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
presenting: Boolean(participantProps.presenting),
sharingScreen: Boolean(participantProps.sharingScreen),
videoAspectRatio: 1.3,
...getDefaultConversation({
avatarPath: participantProps.avatarPath,
@ -69,7 +71,7 @@ story.add('Many Participants', () => {
}),
createParticipant({
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: true,
name: 'Rage Trunks',
title: 'Rage Trunks',
}),

View File

@ -13,8 +13,9 @@ import { sortByTitle } from '../util/sortByTitle';
import { ConversationType } from '../state/ducks/conversations';
type ParticipantType = ConversationType & {
hasAudio?: boolean;
hasVideo?: boolean;
hasRemoteAudio?: boolean;
hasRemoteVideo?: boolean;
presenting?: boolean;
};
export type PropsType = {
@ -130,12 +131,15 @@ export const CallingParticipantsList = React.memo(
)}
</div>
<div>
{participant.hasAudio === false ? (
{participant.hasRemoteAudio === false ? (
<span className="module-calling-participants-list__muted--audio" />
) : null}
{participant.hasVideo === false ? (
{participant.hasRemoteVideo === false ? (
<span className="module-calling-participants-list__muted--video" />
) : null}
{participant.presenting ? (
<span className="module-calling-participants-list__presenting" />
) : null}
</div>
</li>
)

View File

@ -49,7 +49,9 @@ const defaultCall: ActiveCallType = {
callMode: CallMode.Direct as CallMode.Direct,
callState: CallState.Accepted,
peekedParticipants: [],
remoteParticipants: [{ hasRemoteVideo: true }],
remoteParticipants: [
{ hasRemoteVideo: true, presenting: false, title: 'Arsene' },
],
};
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
@ -79,7 +81,9 @@ story.add('Contact (with avatar and no video)', () => {
...conversation,
avatarPath: 'https://www.fillmurray.com/64/64',
},
remoteParticipants: [{ hasRemoteVideo: false }],
remoteParticipants: [
{ hasRemoteVideo: false, presenting: false, title: 'Julian' },
],
},
});
return <CallingPip {...props} />;

View File

@ -96,9 +96,8 @@ export const CallingPipRemoteVideo = ({
return undefined;
}
return maxBy(
activeCall.remoteParticipants,
participant => participant.speakerTime || -Infinity
return maxBy(activeCall.remoteParticipants, participant =>
participant.presenting ? Infinity : participant.speakerTime || -Infinity
);
}, [activeCall.callMode, activeCall.remoteParticipants]);

View 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()} />;
});

View 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>
);
};

View File

@ -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()} />;
});

View 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>
);
};

View 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>
);
};

View File

@ -22,6 +22,8 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
demuxId: index,
hasRemoteAudio: index % 3 !== 0,
hasRemoteVideo: index % 4 !== 0,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,

View File

@ -42,6 +42,8 @@ const createProps = (
demuxId: 123,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: Boolean(isBlocked),

View File

@ -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
// 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
// the overflow area.
//
@ -119,7 +120,9 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
remoteParticipants
.concat()
.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]
);
@ -275,18 +278,23 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
if (isPageVisible) {
setGroupCallVideoRequest([
...gridParticipants.map(participant => {
if (participant.hasRemoteVideo) {
return {
demuxId: participant.demuxId,
width: Math.floor(
gridParticipantHeight *
participant.videoAspectRatio *
VIDEO_REQUEST_SCALAR
),
height: Math.floor(gridParticipantHeight * VIDEO_REQUEST_SCALAR),
};
let scalar: number;
if (participant.sharingScreen) {
// We want best-resolution video if someone is sharing their screen. This code
// is extra-defensive against strange devicePixelRatios.
scalar = Math.max(window.devicePixelRatio || 1, 1);
} else if (participant.hasRemoteVideo) {
scalar = VIDEO_REQUEST_SCALAR;
} else {
scalar = 0;
}
return nonRenderedRemoteParticipant(participant);
return {
demuxId: participant.demuxId,
width: Math.floor(
gridParticipantHeight * participant.videoAspectRatio * scalar
),
height: Math.floor(gridParticipantHeight * scalar),
};
}),
...overflowedParticipants.map(participant => {
if (participant.hasRemoteVideo) {

View File

@ -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>
);
};

View 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>
);
};

View 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]);
}

View File

@ -1,8 +1,9 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable class-methods-use-this */
import { desktopCapturer, ipcRenderer } from 'electron';
import {
Call,
CallEndedReason,
@ -44,6 +45,8 @@ import {
MediaDeviceSettings,
GroupCallConnectionState,
GroupCallJoinState,
PresentableSource,
PresentedSource,
} from '../types/Calling';
import { ConversationModel } from '../models/conversations';
import {
@ -64,6 +67,7 @@ import {
REQUESTED_VIDEO_HEIGHT,
REQUESTED_VIDEO_FRAMERATE,
} from '../calling/constants';
import { notify } from './notify';
const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map<
HttpMethod,
@ -100,12 +104,14 @@ export class CallingClass {
private callsByConversation: { [conversationId: string]: Call | GroupCall };
private hadLocalVideoBeforePresenting?: boolean;
constructor() {
this.videoCapturer = new GumVideoCapturer(
REQUESTED_VIDEO_WIDTH,
REQUESTED_VIDEO_HEIGHT,
REQUESTED_VIDEO_FRAMERATE
);
this.videoCapturer = new GumVideoCapturer({
maxWidth: REQUESTED_VIDEO_WIDTH,
maxHeight: REQUESTED_VIDEO_HEIGHT,
maxFramerate: REQUESTED_VIDEO_FRAMERATE,
});
this.videoRenderer = new CanvasVideoRenderer();
this.callsByConversation = {};
@ -127,6 +133,10 @@ export class CallingClass {
RingRTC.handleLogMessage = this.handleLogMessage.bind(this);
RingRTC.handleSendHttpRequest = this.handleSendHttpRequest.bind(this);
RingRTC.handleSendCallMessage = this.handleSendCallMessage.bind(this);
ipcRenderer.on('stop-screen-share', () => {
uxActions.setPresenting();
});
}
async startCallingLobby(
@ -247,7 +257,7 @@ export class CallingClass {
}
stopCallingLobby(conversationId?: string): void {
this.disableLocalCamera();
this.disableLocalVideo();
this.stopDeviceReselectionTimer();
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
// 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.
this.disableLocalCamera();
this.disableLocalVideo();
delete this.callsByConversation[conversationId];
@ -457,7 +467,7 @@ export class CallingClass {
// NOTE: This assumes only one active call at a time. See comment above.
if (localDeviceState.videoMuted) {
this.disableLocalCamera();
this.disableLocalVideo();
} else {
this.videoCapturer.enableCaptureAndSend(groupCall);
}
@ -689,6 +699,8 @@ export class CallingClass {
demuxId: remoteDeviceState.demuxId,
hasRemoteAudio: !remoteDeviceState.audioMuted,
hasRemoteVideo: !remoteDeviceState.videoMuted,
presenting: Boolean(remoteDeviceState.presenting),
sharingScreen: Boolean(remoteDeviceState.sharingScreen),
speakerTime: normalizeGroupCallTimestamp(
remoteDeviceState.speakerTime
),
@ -807,6 +819,8 @@ export class CallingClass {
return;
}
ipcRenderer.send('close-screen-share-controller');
if (call instanceof Call) {
RingRTC.hangup(call.callId);
} 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> {
// Poll once
await this.pollForMediaDevices();
@ -1066,7 +1175,7 @@ export class CallingClass {
this.videoCapturer.enableCapture();
}
disableLocalCamera(): void {
disableLocalVideo(): void {
this.videoCapturer.disable();
}
@ -1387,6 +1496,14 @@ export class CallingClass {
hasVideo: call.remoteVideoEnabled,
});
};
// eslint-disable-next-line no-param-reassign
call.handleRemoteSharingScreen = () => {
uxActions.remoteSharingScreenChange({
conversationId: conversation.id,
isSharingScreen: Boolean(call.remoteSharingScreen),
});
};
}
private async handleLogMessage(

View File

@ -1,10 +1,16 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer } from 'electron';
import { ThunkAction } from 'redux-thunk';
import { CallEndedReason } from 'ringrtc';
import {
hasScreenCapturePermission,
openSystemPreferences,
} from 'mac-screen-capture-permissions';
import { has, omit } from 'lodash';
import { getOwn } from '../../util/getOwn';
import { getPlatform } from '../selectors/user';
import { missingCaseError } from '../../util/missingCaseError';
import { notify } from '../../services/notify';
import { calling } from '../../services/calling';
@ -18,6 +24,8 @@ import {
GroupCallJoinState,
GroupCallVideoRequest,
MediaDeviceSettings,
PresentedSource,
PresentableSource,
} from '../../types/Calling';
import { callingTones } from '../../util/callingTones';
import { requestCameraPermissions } from '../../util/callingPermissions';
@ -43,6 +51,8 @@ export type GroupCallParticipantInfoType = {
demuxId: number;
hasRemoteAudio: boolean;
hasRemoteVideo: boolean;
presenting: boolean;
sharingScreen: boolean;
speakerTime?: number;
videoAspectRatio: number;
};
@ -53,6 +63,7 @@ export type DirectCallStateType = {
callState?: CallState;
callEndedReason?: CallEndedReason;
isIncoming: boolean;
isSharingScreen?: boolean;
isVideoCall: boolean;
hasRemoteVideo?: boolean;
};
@ -73,8 +84,11 @@ export type ActiveCallStateType = {
isInSpeakerView: boolean;
joinedAt?: number;
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>;
safetyNumberChangedUuids: Array<string>;
settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;
};
@ -160,6 +174,11 @@ export type RemoteVideoChangeType = {
hasVideo: boolean;
};
type RemoteSharingScreenChangeType = {
conversationId: string;
isSharingScreen: boolean;
};
export type SetLocalAudioType = {
enabled: boolean;
};
@ -236,10 +255,15 @@ const OUTGOING_CALL = 'calling/OUTGOING_CALL';
const PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED =
'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
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 RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_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 TOGGLE_PARTICIPANTS = 'calling/TOGGLE_PARTICIPANTS';
const TOGGLE_PIP = 'calling/TOGGLE_PIP';
@ -326,6 +350,11 @@ type RefreshIODevicesActionType = {
payload: MediaDeviceSettings;
};
type RemoteSharingScreenChangeActionType = {
type: 'calling/REMOTE_SHARING_SCREEN_CHANGE';
payload: RemoteSharingScreenChangeType;
};
type RemoteVideoChangeActionType = {
type: 'calling/REMOTE_VIDEO_CHANGE';
payload: RemoteVideoChangeType;
@ -345,6 +374,16 @@ type SetLocalVideoFulfilledActionType = {
payload: SetLocalVideoType;
};
type SetPresentingFulfilledActionType = {
type: 'calling/SET_PRESENTING';
payload?: PresentedSource;
};
type SetPresentingSourcesActionType = {
type: 'calling/SET_PRESENTING_SOURCES';
payload: Array<PresentableSource>;
};
type ShowCallLobbyActionType = {
type: 'calling/SHOW_CALL_LOBBY';
payload: ShowCallLobbyType;
@ -355,6 +394,10 @@ type StartDirectCallActionType = {
payload: StartDirectCallType;
};
type ToggleNeedsScreenRecordingPermissionsActionType = {
type: 'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
};
type ToggleParticipantsActionType = {
type: 'calling/TOGGLE_PARTICIPANTS';
};
@ -387,14 +430,18 @@ export type CallingActionType =
| OutgoingCallActionType
| PeekNotConnectedGroupCallFulfilledActionType
| RefreshIODevicesActionType
| RemoteSharingScreenChangeActionType
| RemoteVideoChangeActionType
| ReturnToActiveCallActionType
| SetLocalAudioActionType
| SetLocalVideoFulfilledActionType
| SetPresentingSourcesActionType
| ShowCallLobbyActionType
| StartDirectCallActionType
| ToggleNeedsScreenRecordingPermissionsActionType
| ToggleParticipantsActionType
| TogglePipActionType
| SetPresentingFulfilledActionType
| ToggleSettingsActionType
| ToggleSpeakerViewActionType;
@ -438,6 +485,7 @@ function callStateChange(
}
if (callState === CallState.Ended) {
await callingTones.playEndCall();
ipcRenderer.send('close-screen-share-controller');
}
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(
payload: GroupCallStateChangeArgumentType
): 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({
type: GROUP_CALL_STATE_CHANGE,
payload: {
@ -530,6 +627,10 @@ function groupCallStateChange(
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 {
callingTones.playRingtone();
@ -694,6 +806,15 @@ function refreshIODevices(
};
}
function remoteSharingScreenChange(
payload: RemoteSharingScreenChangeType
): RemoteSharingScreenChangeActionType {
return {
type: REMOTE_SHARING_SCREEN_CHANGE,
payload,
};
}
function remoteVideoChange(
payload: RemoteVideoChangeType
): RemoteVideoChangeActionType {
@ -764,7 +885,7 @@ function setLocalVideo(
} else if (payload.enabled) {
calling.enableLocalCamera();
} else {
calling.disableLocalCamera();
calling.disableLocalVideo();
}
({ enabled } = payload);
} 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(
payload: StartCallingLobbyType
): 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 {
return {
type: TOGGLE_SETTINGS,
@ -871,31 +1027,36 @@ function toggleSpeakerView(): ToggleSpeakerViewActionType {
export const actions = {
acceptCall,
cancelCall,
callStateChange,
cancelCall,
changeIODevice,
closeNeedPermissionScreen,
declineCall,
getPresentingSources,
groupCallStateChange,
hangUp,
keyChanged,
keyChangeOk,
receiveIncomingCall,
keyChanged,
openSystemPreferencesAction,
outgoingCall,
peekNotConnectedGroupCall,
receiveIncomingCall,
refreshIODevices,
remoteSharingScreenChange,
remoteVideoChange,
returnToActiveCall,
setLocalPreview,
setRendererCanvas,
setLocalAudio,
setLocalVideo,
setGroupCallVideoRequest,
startCallingLobby,
setLocalAudio,
setLocalPreview,
setLocalVideo,
setPresenting,
setRendererCanvas,
showCallLobby,
startCall,
startCallingLobby,
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSettings,
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) {
const { conversationId, hasVideo } = action.payload;
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) {
const { activeCallState } = state;
if (!activeCallState) {

View File

@ -78,7 +78,12 @@ const mapStateToActiveCallProp = (
isInSpeakerView: activeCallState.isInSpeakerView,
joinedAt: activeCallState.joinedAt,
pip: activeCallState.pip,
presentingSource: activeCallState.presentingSource,
presentingSourcesAvailable: activeCallState.presentingSourcesAvailable,
settingsDialogOpen: activeCallState.settingsDialogOpen,
showNeedsScreenRecordingPermissionsWarning: Boolean(
activeCallState.showNeedsScreenRecordingPermissionsWarning
),
showParticipantsList: activeCallState.showParticipantsList,
};
@ -93,6 +98,9 @@ const mapStateToActiveCallProp = (
remoteParticipants: [
{
hasRemoteVideo: Boolean(call.hasRemoteVideo),
presenting: Boolean(call.isSharingScreen),
title: conversation.title,
uuid: conversation.uuid,
},
],
};
@ -119,6 +127,8 @@ const mapStateToActiveCallProp = (
demuxId: remoteParticipant.demuxId,
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
presenting: remoteParticipant.presenting,
sharingScreen: remoteParticipant.sharingScreen,
speakerTime: remoteParticipant.speakerTime,
videoAspectRatio: remoteParticipant.videoAspectRatio,
});

View File

@ -86,6 +86,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -129,6 +131,188 @@ describe('calling duck', () => {
});
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', () => {
const { acceptCall } = actions;
@ -403,6 +587,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -429,6 +615,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -491,6 +679,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -515,6 +705,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -542,6 +734,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -571,6 +765,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -609,6 +805,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -850,6 +1048,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -874,6 +1074,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -925,6 +1127,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -965,6 +1169,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],

View File

@ -10,16 +10,31 @@ export enum CallMode {
Group = 'Group',
}
export type PresentableSource = {
appIcon?: string;
id: string;
name: string;
thumbnail: string;
};
export type PresentedSource = {
id: string;
name: string;
};
type ActiveCallBaseType = {
conversation: ConversationType;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
isInSpeakerView: boolean;
isSharingScreen?: boolean;
joinedAt?: number;
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>;
settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;
showSafetyNumberDialog?: boolean;
};
type ActiveDirectCallType = ActiveCallBaseType & {
@ -30,6 +45,9 @@ type ActiveDirectCallType = ActiveCallBaseType & {
remoteParticipants: [
{
hasRemoteVideo: boolean;
presenting: boolean;
title: string;
uuid?: string;
}
];
};
@ -100,6 +118,8 @@ export type GroupCallRemoteParticipantType = ConversationType & {
demuxId: number;
hasRemoteAudio: boolean;
hasRemoteVideo: boolean;
presenting: boolean;
sharingScreen: boolean;
speakerTime?: number;
videoAspectRatio: number;
};

View File

@ -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();

View 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')
);
}

View File

@ -2726,6 +2726,13 @@
"updated": "2020-08-26T00:10:28.628Z",
"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(",
"path": "node_modules/expand-range/node_modules/fill-range/index.js",
@ -2859,6 +2866,13 @@
"reasonCategory": "falseMatch",
"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(",
"path": "node_modules/form-data/lib/form_data.js",
@ -2880,6 +2894,13 @@
"reasonCategory": "falseMatch",
"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-$(",
"path": "node_modules/global-agent/node_modules/core-js/internals/collection.js",
@ -8702,6 +8723,13 @@
"reasonCategory": "falseMatch",
"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",
"path": "node_modules/make-dir/node_modules/pify/index.js",
@ -9407,6 +9435,13 @@
"updated": "2021-05-07T20:07:48.358Z",
"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(",
"path": "node_modules/pac-proxy-agent/node_modules/socks/build/client/socksclient.js",
@ -11100,13 +11135,6 @@
"reasonCategory": "falseMatch",
"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",
"path": "node_modules/protobufjs/dist/light/protobuf.js",
@ -12619,13 +12647,6 @@
"reasonCategory": "falseMatch",
"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-$(",
"path": "node_modules/rx-lite-aggregates/rx.lite.aggregates.min.js",
@ -12866,13 +12887,6 @@
"reasonCategory": "falseMatch",
"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(",
"path": "node_modules/sshpk/lib/dhe.js",
@ -12930,6 +12944,13 @@
"reasonCategory": "falseMatch",
"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(",
"path": "node_modules/test-exclude/node_modules/braces/index.js",
@ -13263,13 +13284,6 @@
"reasonCategory": "falseMatch",
"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-$(",
"path": "node_modules/xregexp/xregexp-all.js",
@ -13517,6 +13531,13 @@
"updated": "2020-10-26T19:12:24.410Z",
"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",
"path": "ts/components/CaptchaDialog.js",

11
ts/window.d.ts vendored
View File

@ -84,6 +84,10 @@ import { ConversationModel } from './models/conversations';
import { combineNames } from './util';
import { BatcherType } from './util/batcher';
import { AttachmentList } from './components/conversation/AttachmentList';
import {
CallingScreenSharingController,
PropsType as CallingScreenSharingControllerProps,
} from './components/CallingScreenSharingController';
import { CaptionEditor } from './components/CaptionEditor';
import { ConfirmationDialog } from './components/ConfirmationDialog';
import { ContactDetail } from './components/conversation/ContactDetail';
@ -147,6 +151,13 @@ declare global {
WhatIsThis: WhatIsThis;
registerScreenShareControllerRenderer: (
f: (
component: typeof CallingScreenSharingController,
props: CallingScreenSharingControllerProps
) => void
) => void;
attachmentDownloadQueue: Array<MessageModel> | undefined;
startupProcessingQueue: StartupQueue | undefined;
baseAttachmentsPath: string;

11
ts/windows/screenShare.ts Normal file
View 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')
);
});

View File

@ -10,6 +10,7 @@ const context = __dirname;
const { NODE_ENV: mode = 'development' } = process.env;
const EXTERNAL_MODULE = new Set([
'@signalapp/signal-client',
'backbone',
'better-sqlite3',
'ffi-napi',
@ -17,7 +18,7 @@ const EXTERNAL_MODULE = new Set([
'fsevents',
'got',
'jquery',
'@signalapp/signal-client',
'mac-screen-capture-permissions',
'node-fetch',
'node-sass',
'pino',

View File

@ -6006,7 +6006,7 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0"
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"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@ -6910,6 +6910,11 @@ electron-download@^4.1.0:
semver "^5.3.0"
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:
version "8.1.1"
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"
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:
version "0.8.1"
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"
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:
version "5.0.0"
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:
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:
version "5.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
@ -11450,11 +11485,28 @@ lru-queue@0.1:
dependencies:
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:
version "2.3.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
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:
version "1.0.0"
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"
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:
version "1.0.0"
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:
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:
version "4.0.1"
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"
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:
version "1.1.0"
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"
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:
version "6.1.1"
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:
glob "^7.1.3"
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#b43c6b728d62b6d386d95705e128f32f44edb650":
version "2.9.4"
resolved "https://github.com/signalapp/signal-ringrtc-node.git#b43c6b728d62b6d386d95705e128f32f44edb650"
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#17b22fc9d47605867608193202c54be06bce6f56":
version "2.10.1"
resolved "https://github.com/signalapp/signal-ringrtc-node.git#17b22fc9d47605867608193202c54be06bce6f56"
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.1"