Add screensharing behind a feature flag
This commit is contained in:
		
					parent
					
						
							
								7c7f7ee5a0
							
						
					
				
			
			
				commit
				
					
						ceffc2380c
					
				
			
		
					 49 changed files with 2044 additions and 164 deletions
				
			
		| 
						 | 
				
			
			@ -1729,6 +1729,10 @@ Signal Desktop makes use of the following open source projects.
 | 
			
		|||
    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 | 
			
		||||
    IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
 | 
			
		||||
## mac-screen-capture-permissions
 | 
			
		||||
 | 
			
		||||
    License: MIT
 | 
			
		||||
 | 
			
		||||
## memoizee
 | 
			
		||||
 | 
			
		||||
    ISC License
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								images/icons/v2/share-screen-solid-28.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								images/icons/v2/share-screen-solid-28.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><path d="m23 4.5h-18a3 3 0 0 0 -3 3v13a3 3 0 0 0 3 3h18a3 3 0 0 0 3-3v-13a3 3 0 0 0 -3-3zm-5 10-2.6-2.6-.65-.9v9h-1.5v-9l-.62.93-2.63 2.6-1-1.06 5-5 5 5z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 223 B  | 
							
								
								
									
										71
									
								
								main.js
									
										
									
									
									
								
							
							
						
						
									
										71
									
								
								main.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -753,6 +753,61 @@ function setupAsStandalone() {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let screenShareWindow;
 | 
			
		||||
function showScreenShareWindow(sourceName) {
 | 
			
		||||
  if (screenShareWindow) {
 | 
			
		||||
    screenShareWindow.show();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const width = 480;
 | 
			
		||||
 | 
			
		||||
  const { screen } = electron;
 | 
			
		||||
  const display = screen.getPrimaryDisplay();
 | 
			
		||||
  const options = {
 | 
			
		||||
    alwaysOnTop: true,
 | 
			
		||||
    autoHideMenuBar: true,
 | 
			
		||||
    backgroundColor: '#2e2e2e',
 | 
			
		||||
    darkTheme: true,
 | 
			
		||||
    frame: false,
 | 
			
		||||
    fullscreenable: false,
 | 
			
		||||
    height: 44,
 | 
			
		||||
    maximizable: false,
 | 
			
		||||
    minimizable: false,
 | 
			
		||||
    resizable: false,
 | 
			
		||||
    show: false,
 | 
			
		||||
    title: locale.messages.screenShareWindow.message,
 | 
			
		||||
    width,
 | 
			
		||||
    webPreferences: {
 | 
			
		||||
      ...defaultWebPrefs,
 | 
			
		||||
      nodeIntegration: false,
 | 
			
		||||
      nodeIntegrationInWorker: false,
 | 
			
		||||
      contextIsolation: false,
 | 
			
		||||
      preload: path.join(__dirname, 'screenShare_preload.js'),
 | 
			
		||||
    },
 | 
			
		||||
    x: Math.floor(display.size.width / 2) - width / 2,
 | 
			
		||||
    y: 24,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  screenShareWindow = new BrowserWindow(options);
 | 
			
		||||
 | 
			
		||||
  handleCommonWindowEvents(screenShareWindow);
 | 
			
		||||
 | 
			
		||||
  screenShareWindow.loadURL(prepareFileUrl([__dirname, 'screenShare.html']));
 | 
			
		||||
 | 
			
		||||
  screenShareWindow.on('closed', () => {
 | 
			
		||||
    screenShareWindow = null;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  screenShareWindow.once('ready-to-show', () => {
 | 
			
		||||
    screenShareWindow.show();
 | 
			
		||||
    screenShareWindow.webContents.send(
 | 
			
		||||
      'render-screen-sharing-controller',
 | 
			
		||||
      sourceName
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let aboutWindow;
 | 
			
		||||
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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								patches/electron-util+0.13.1.patch
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								patches/electron-util+0.13.1.patch
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
diff --git a/node_modules/electron-util/index.d.ts b/node_modules/electron-util/index.d.ts
 | 
			
		||||
index 8d493d5..3408e21 100644
 | 
			
		||||
--- a/node_modules/electron-util/index.d.ts
 | 
			
		||||
+++ b/node_modules/electron-util/index.d.ts
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
 /// <reference lib="dom"/>
 | 
			
		||||
 /// <reference types="electron"/>
 | 
			
		||||
 /// <reference types="node"/>
 | 
			
		||||
-import {AllElectron, Remote, BrowserWindow, Size, Rectangle, Session, MenuItemConstructorOptions, MenuItem} from 'electron';
 | 
			
		||||
+import {RemoteMainInterface, BrowserWindow, Size, Rectangle, Session, MenuItemConstructorOptions, MenuItem} from 'electron';
 | 
			
		||||
 import {Options as NewGithubIssueUrlOptions} from 'new-github-issue-url';
 | 
			
		||||
 import {RequireAtLeastOne} from 'type-fest';
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ Access the Electron APIs in both the main and renderer process without having to
 | 
			
		||||
 api.app.quit(); // The `app` API is usually only available in the main process.
 | 
			
		||||
 ```
 | 
			
		||||
 */
 | 
			
		||||
-export const api: AllElectron | Remote;
 | 
			
		||||
+export const api: RemoteMainInterface;
 | 
			
		||||
 
 | 
			
		||||
 /**
 | 
			
		||||
 Check for various things.
 | 
			
		||||
							
								
								
									
										13
									
								
								patches/mac-screen-capture-permissions+2.0.0.patch
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								patches/mac-screen-capture-permissions+2.0.0.patch
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
diff --git a/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m b/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
 | 
			
		||||
index d9d6a00..78fa83f 100644
 | 
			
		||||
--- a/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
 | 
			
		||||
+++ b/node_modules/mac-screen-capture-permissions/screen-capture-permissions.m
 | 
			
		||||
@@ -2,6 +2,8 @@
 | 
			
		||||
 #import <Foundation/Foundation.h>
 | 
			
		||||
 #include <node_api.h>
 | 
			
		||||
 | 
			
		||||
+CG_EXTERN bool CGPreflightScreenCaptureAccess(void) CG_AVAILABLE_STARTING(10.15);
 | 
			
		||||
+
 | 
			
		||||
 static napi_value hasPermissions(napi_env env, napi_callback_info info) {
 | 
			
		||||
   napi_status status;
 | 
			
		||||
   bool hasPermissions;
 | 
			
		||||
							
								
								
									
										22
									
								
								screenShare.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								screenShare.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
<!-- Copyright 2021 Signal Messenger, LLC -->
 | 
			
		||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
 | 
			
		||||
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <meta http-equiv="Content-Security-Policy"
 | 
			
		||||
    content="default-src 'none';
 | 
			
		||||
            font-src 'self';
 | 
			
		||||
            img-src 'self' blob: data:;
 | 
			
		||||
            media-src 'self' blob:;
 | 
			
		||||
            object-src 'none';
 | 
			
		||||
            script-src 'self';
 | 
			
		||||
            style-src 'self' 'unsafe-inline';"
 | 
			
		||||
  >
 | 
			
		||||
  <link href="node_modules/sanitize.css/sanitize.css" rel="stylesheet" type="text/css" />
 | 
			
		||||
  <link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div id="app"></div>
 | 
			
		||||
  <script type='application/javascript' src='ts/windows/screenShare.js'></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										59
									
								
								screenShare_preload.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								screenShare_preload.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
/* global window */
 | 
			
		||||
 | 
			
		||||
const React = require('react');
 | 
			
		||||
const ReactDOM = require('react-dom');
 | 
			
		||||
const url = require('url');
 | 
			
		||||
const { ipcRenderer } = require('electron');
 | 
			
		||||
 | 
			
		||||
const i18n = require('./js/modules/i18n');
 | 
			
		||||
const {
 | 
			
		||||
  getEnvironment,
 | 
			
		||||
  setEnvironment,
 | 
			
		||||
  parseEnvironment,
 | 
			
		||||
} = require('./ts/environment');
 | 
			
		||||
const {
 | 
			
		||||
  CallingScreenSharingController,
 | 
			
		||||
} = require('./ts/components/CallingScreenSharingController');
 | 
			
		||||
 | 
			
		||||
const config = url.parse(window.location.toString(), true).query;
 | 
			
		||||
const { locale } = config;
 | 
			
		||||
const localeMessages = ipcRenderer.sendSync('locale-data');
 | 
			
		||||
setEnvironment(parseEnvironment(config.environment));
 | 
			
		||||
 | 
			
		||||
window.React = React;
 | 
			
		||||
window.ReactDOM = ReactDOM;
 | 
			
		||||
window.getAppInstance = () => config.appInstance;
 | 
			
		||||
window.getEnvironment = getEnvironment;
 | 
			
		||||
window.getVersion = () => config.version;
 | 
			
		||||
window.i18n = i18n.setup(locale, localeMessages);
 | 
			
		||||
 | 
			
		||||
let renderComponent;
 | 
			
		||||
window.registerScreenShareControllerRenderer = f => {
 | 
			
		||||
  renderComponent = f;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function renderScreenSharingController(event, presentedSourceName) {
 | 
			
		||||
  if (!renderComponent) {
 | 
			
		||||
    setTimeout(renderScreenSharingController, 100);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const props = {
 | 
			
		||||
    i18n: window.i18n,
 | 
			
		||||
    onCloseController: () => ipcRenderer.send('close-screen-share-controller'),
 | 
			
		||||
    onStopSharing: () => ipcRenderer.send('stop-screen-share'),
 | 
			
		||||
    presentedSourceName,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  renderComponent(CallingScreenSharingController, props);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ipcRenderer.once(
 | 
			
		||||
  'render-screen-sharing-controller',
 | 
			
		||||
  renderScreenSharingController
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
require('./ts/logging/set_up_renderer_logging').initialize();
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								sounds/navigation_selection-complete-celebration.ogg
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								sounds/navigation_selection-complete-celebration.ogg
									
										
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -5989,6 +5989,19 @@ button.module-image__border-overlay:focus {
 | 
			
		|||
      $color-white
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										35
									
								
								stylesheets/components/CallingScreenSharingController.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								stylesheets/components/CallingScreenSharingController.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
.module-CallingScreenSharingController {
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  padding: 4px 16px;
 | 
			
		||||
  -webkit-app-region: drag;
 | 
			
		||||
 | 
			
		||||
  &__text {
 | 
			
		||||
    @include font-body-2;
 | 
			
		||||
    color: $color-gray-05;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    width: 212px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__buttons {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    -webkit-app-region: no-drag;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__close {
 | 
			
		||||
    @include button-reset;
 | 
			
		||||
    @include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    margin-left: 12px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    width: 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,84 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
.module-CallingSelectPresentingSourcesModal {
 | 
			
		||||
  // specificity
 | 
			
		||||
  &.module-Modal {
 | 
			
		||||
    max-width: 665px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    padding-bottom: 48px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__footer {
 | 
			
		||||
    background-color: $color-gray-95;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    margin-left: -16px;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
    padding: 16px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__sources {
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
    margin-left: -6px;
 | 
			
		||||
    margin-right: -6px;
 | 
			
		||||
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__title {
 | 
			
		||||
    margin-bottom: 12px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__source {
 | 
			
		||||
    @include button-reset();
 | 
			
		||||
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    border: 1px solid $color-gray-60;
 | 
			
		||||
    margin-bottom: 14px;
 | 
			
		||||
    margin-left: 6px;
 | 
			
		||||
    margin-right: 6px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    padding: 8px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    width: 200px;
 | 
			
		||||
 | 
			
		||||
    &--selected {
 | 
			
		||||
      background-color: $ultramarine-ui-dark;
 | 
			
		||||
      border: 1px solid $ultramarine-ui-dark;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    img {
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__screenshot {
 | 
			
		||||
    max-height: 102px;
 | 
			
		||||
    max-width: 184px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__name {
 | 
			
		||||
    &--container {
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      margin-top: 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--text {
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--icon {
 | 
			
		||||
      margin-right: 8px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +31,8 @@
 | 
			
		|||
@import './components/Avatar.scss';
 | 
			
		||||
@import './components/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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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' },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    })}
 | 
			
		||||
  />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
            />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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} />;
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
      }),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
              )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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} />;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								ts/components/CallingScreenSharingController.stories.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ts/components/CallingScreenSharingController.stories.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { storiesOf } from '@storybook/react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  CallingScreenSharingController,
 | 
			
		||||
  PropsType,
 | 
			
		||||
} from './CallingScreenSharingController';
 | 
			
		||||
 | 
			
		||||
import { setup as setupI18n } from '../../js/modules/i18n';
 | 
			
		||||
import enMessages from '../../_locales/en/messages.json';
 | 
			
		||||
 | 
			
		||||
const i18n = setupI18n('en', enMessages);
 | 
			
		||||
 | 
			
		||||
const createProps = (): PropsType => ({
 | 
			
		||||
  i18n,
 | 
			
		||||
  onCloseController: action('on-close-controller'),
 | 
			
		||||
  onStopSharing: action('on-stop-sharing'),
 | 
			
		||||
  presentedSourceName: 'Application',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const story = storiesOf('Components/CallingScreenSharingController', module);
 | 
			
		||||
 | 
			
		||||
story.add('Controller', () => {
 | 
			
		||||
  return <CallingScreenSharingController {...createProps()} />;
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										39
									
								
								ts/components/CallingScreenSharingController.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								ts/components/CallingScreenSharingController.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Button, ButtonVariant } from './Button';
 | 
			
		||||
import { LocalizerType } from '../types/Util';
 | 
			
		||||
 | 
			
		||||
export type PropsType = {
 | 
			
		||||
  i18n: LocalizerType;
 | 
			
		||||
  onCloseController: () => unknown;
 | 
			
		||||
  onStopSharing: () => unknown;
 | 
			
		||||
  presentedSourceName: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const CallingScreenSharingController = ({
 | 
			
		||||
  i18n,
 | 
			
		||||
  onCloseController,
 | 
			
		||||
  onStopSharing,
 | 
			
		||||
  presentedSourceName,
 | 
			
		||||
}: PropsType): JSX.Element => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="module-CallingScreenSharingController">
 | 
			
		||||
      <div className="module-CallingScreenSharingController__text">
 | 
			
		||||
        {i18n('calling__presenting--info', [presentedSourceName])}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="module-CallingScreenSharingController__buttons">
 | 
			
		||||
        <Button onClick={onStopSharing} variant={ButtonVariant.Destructive}>
 | 
			
		||||
          {i18n('calling__presenting--stop')}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <button
 | 
			
		||||
          aria-label={i18n('close')}
 | 
			
		||||
          className="module-CallingScreenSharingController__close"
 | 
			
		||||
          onClick={onCloseController}
 | 
			
		||||
          type="button"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { storiesOf } from '@storybook/react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  CallingSelectPresentingSourcesModal,
 | 
			
		||||
  PropsType,
 | 
			
		||||
} from './CallingSelectPresentingSourcesModal';
 | 
			
		||||
 | 
			
		||||
import { setup as setupI18n } from '../../js/modules/i18n';
 | 
			
		||||
import enMessages from '../../_locales/en/messages.json';
 | 
			
		||||
 | 
			
		||||
const i18n = setupI18n('en', enMessages);
 | 
			
		||||
 | 
			
		||||
const createProps = (): PropsType => ({
 | 
			
		||||
  i18n,
 | 
			
		||||
  presentingSourcesAvailable: [
 | 
			
		||||
    {
 | 
			
		||||
      id: 'screen',
 | 
			
		||||
      name: 'Entire Screen',
 | 
			
		||||
      thumbnail:
 | 
			
		||||
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P/1PwAF8AL1sEVIPAAAAABJRU5ErkJggg==',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'window:123',
 | 
			
		||||
      name: 'Bozirro Airhorse',
 | 
			
		||||
      thumbnail:
 | 
			
		||||
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z1D4HwAF5wJxzsNOIAAAAABJRU5ErkJggg==',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'window:456',
 | 
			
		||||
      name: 'Discoverer',
 | 
			
		||||
      thumbnail:
 | 
			
		||||
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8HwHwAFHQIIj4yLtgAAAABJRU5ErkJggg==',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'window:789',
 | 
			
		||||
      name: 'Signal Beta',
 | 
			
		||||
      thumbnail: '',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'window:xyz',
 | 
			
		||||
      name: 'Window that has a really long name and overflows',
 | 
			
		||||
      thumbnail:
 | 
			
		||||
        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+O/wHwAEhgJAyqFnAgAAAABJRU5ErkJggg==',
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  setPresenting: action('set-presenting'),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const story = storiesOf(
 | 
			
		||||
  'Components/CallingSelectPresentingSourcesModal',
 | 
			
		||||
  module
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
story.add('Modal', () => {
 | 
			
		||||
  return <CallingSelectPresentingSourcesModal {...createProps()} />;
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										137
									
								
								ts/components/CallingSelectPresentingSourcesModal.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								ts/components/CallingSelectPresentingSourcesModal.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,137 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { groupBy } from 'lodash';
 | 
			
		||||
import { Button, ButtonVariant } from './Button';
 | 
			
		||||
import { LocalizerType } from '../types/Util';
 | 
			
		||||
import { Modal } from './Modal';
 | 
			
		||||
import { PresentedSource, PresentableSource } from '../types/Calling';
 | 
			
		||||
import { Theme } from '../util/theme';
 | 
			
		||||
 | 
			
		||||
export type PropsType = {
 | 
			
		||||
  i18n: LocalizerType;
 | 
			
		||||
  presentingSourcesAvailable: Array<PresentableSource>;
 | 
			
		||||
  setPresenting: (_?: PresentedSource) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Source = ({
 | 
			
		||||
  onSourceClick,
 | 
			
		||||
  source,
 | 
			
		||||
  sourceToPresent,
 | 
			
		||||
}: {
 | 
			
		||||
  onSourceClick: (source: PresentedSource) => void;
 | 
			
		||||
  source: PresentableSource;
 | 
			
		||||
  sourceToPresent?: PresentedSource;
 | 
			
		||||
}): JSX.Element => {
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      className={classNames({
 | 
			
		||||
        'module-CallingSelectPresentingSourcesModal__source': true,
 | 
			
		||||
        'module-CallingSelectPresentingSourcesModal__source--selected':
 | 
			
		||||
          sourceToPresent?.id === source.id,
 | 
			
		||||
      })}
 | 
			
		||||
      key={source.id}
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        onSourceClick({
 | 
			
		||||
          id: source.id,
 | 
			
		||||
          name: source.name,
 | 
			
		||||
        });
 | 
			
		||||
      }}
 | 
			
		||||
      type="button"
 | 
			
		||||
    >
 | 
			
		||||
      <img
 | 
			
		||||
        alt={source.name}
 | 
			
		||||
        className="module-CallingSelectPresentingSourcesModal__name--screenshot"
 | 
			
		||||
        src={source.thumbnail}
 | 
			
		||||
      />
 | 
			
		||||
      <div className="module-CallingSelectPresentingSourcesModal__name--container">
 | 
			
		||||
        {source.appIcon ? (
 | 
			
		||||
          <img
 | 
			
		||||
            alt={source.name}
 | 
			
		||||
            className="module-CallingSelectPresentingSourcesModal__name--icon"
 | 
			
		||||
            height={16}
 | 
			
		||||
            src={source.appIcon}
 | 
			
		||||
            width={16}
 | 
			
		||||
          />
 | 
			
		||||
        ) : null}
 | 
			
		||||
        <span className="module-CallingSelectPresentingSourcesModal__name--text">
 | 
			
		||||
          {source.name}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const CallingSelectPresentingSourcesModal = ({
 | 
			
		||||
  i18n,
 | 
			
		||||
  presentingSourcesAvailable,
 | 
			
		||||
  setPresenting,
 | 
			
		||||
}: PropsType): JSX.Element | null => {
 | 
			
		||||
  const [sourceToPresent, setSourceToPresent] = useState<
 | 
			
		||||
    PresentedSource | undefined
 | 
			
		||||
  >(undefined);
 | 
			
		||||
 | 
			
		||||
  if (!presentingSourcesAvailable.length) {
 | 
			
		||||
    throw new Error('No sources available for presenting');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sources = groupBy(presentingSourcesAvailable, source =>
 | 
			
		||||
    source.id.startsWith('screen')
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      hasXButton
 | 
			
		||||
      i18n={i18n}
 | 
			
		||||
      moduleClassName="module-CallingSelectPresentingSourcesModal"
 | 
			
		||||
      onClose={() => {
 | 
			
		||||
        setPresenting(sourceToPresent);
 | 
			
		||||
      }}
 | 
			
		||||
      theme={Theme.Dark}
 | 
			
		||||
      title={i18n('calling__SelectPresentingSourcesModal--title')}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="module-CallingSelectPresentingSourcesModal__title">
 | 
			
		||||
        {i18n('calling__SelectPresentingSourcesModal--entireScreen')}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="module-CallingSelectPresentingSourcesModal__sources">
 | 
			
		||||
        {sources.true.map(source => (
 | 
			
		||||
          <Source
 | 
			
		||||
            key={source.id}
 | 
			
		||||
            onSourceClick={selectedSource => setSourceToPresent(selectedSource)}
 | 
			
		||||
            source={source}
 | 
			
		||||
            sourceToPresent={sourceToPresent}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="module-CallingSelectPresentingSourcesModal__title">
 | 
			
		||||
        {i18n('calling__SelectPresentingSourcesModal--window')}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="module-CallingSelectPresentingSourcesModal__sources">
 | 
			
		||||
        {sources.false.map(source => (
 | 
			
		||||
          <Source
 | 
			
		||||
            key={source.id}
 | 
			
		||||
            onSourceClick={selectedSource => setSourceToPresent(selectedSource)}
 | 
			
		||||
            source={source}
 | 
			
		||||
            sourceToPresent={sourceToPresent}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
      <Modal.Footer moduleClassName="module-CallingSelectPresentingSourcesModal">
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => setPresenting()}
 | 
			
		||||
          variant={ButtonVariant.Secondary}
 | 
			
		||||
        >
 | 
			
		||||
          {i18n('cancel')}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button
 | 
			
		||||
          disabled={!sourceToPresent}
 | 
			
		||||
          onClick={() => setPresenting(sourceToPresent)}
 | 
			
		||||
        >
 | 
			
		||||
          {i18n('calling__SelectPresentingSourcesModal--confirm')}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Modal.Footer>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										163
									
								
								ts/components/CallingToastManager.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								ts/components/CallingToastManager.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,163 @@
 | 
			
		|||
// Copyright 2020-2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import {
 | 
			
		||||
  ActiveCallType,
 | 
			
		||||
  CallMode,
 | 
			
		||||
  GroupCallConnectionState,
 | 
			
		||||
} from '../types/Calling';
 | 
			
		||||
import { ConversationType } from '../state/ducks/conversations';
 | 
			
		||||
import { LocalizerType } from '../types/Util';
 | 
			
		||||
 | 
			
		||||
type PropsType = {
 | 
			
		||||
  activeCall: ActiveCallType;
 | 
			
		||||
  i18n: LocalizerType;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ToastType =
 | 
			
		||||
  | {
 | 
			
		||||
      message: string;
 | 
			
		||||
      type: 'dismissable' | 'static';
 | 
			
		||||
    }
 | 
			
		||||
  | undefined;
 | 
			
		||||
 | 
			
		||||
function getReconnectingToast({ activeCall, i18n }: PropsType): ToastType {
 | 
			
		||||
  if (
 | 
			
		||||
    activeCall.callMode === CallMode.Group &&
 | 
			
		||||
    activeCall.connectionState === GroupCallConnectionState.Reconnecting
 | 
			
		||||
  ) {
 | 
			
		||||
    return {
 | 
			
		||||
      message: i18n('callReconnecting'),
 | 
			
		||||
      type: 'static',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ME = Symbol('me');
 | 
			
		||||
 | 
			
		||||
function getCurrentPresenter(
 | 
			
		||||
  activeCall: Readonly<ActiveCallType>
 | 
			
		||||
): ConversationType | typeof ME | undefined {
 | 
			
		||||
  if (activeCall.presentingSource) {
 | 
			
		||||
    return ME;
 | 
			
		||||
  }
 | 
			
		||||
  if (activeCall.callMode === CallMode.Direct) {
 | 
			
		||||
    const isOtherPersonPresenting = activeCall.remoteParticipants.some(
 | 
			
		||||
      participant => participant.presenting
 | 
			
		||||
    );
 | 
			
		||||
    return isOtherPersonPresenting ? activeCall.conversation : undefined;
 | 
			
		||||
  }
 | 
			
		||||
  if (activeCall.callMode === CallMode.Group) {
 | 
			
		||||
    return activeCall.remoteParticipants.find(
 | 
			
		||||
      participant => participant.presenting
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useScreenSharingToast({ activeCall, i18n }: PropsType): ToastType {
 | 
			
		||||
  const [result, setResult] = useState<undefined | ToastType>(undefined);
 | 
			
		||||
 | 
			
		||||
  const [previousPresenter, setPreviousPresenter] = useState<
 | 
			
		||||
    undefined | { id: string | typeof ME; title?: string }
 | 
			
		||||
  >(undefined);
 | 
			
		||||
 | 
			
		||||
  const previousPresenterId = previousPresenter?.id;
 | 
			
		||||
  const previousPresenterTitle = previousPresenter?.title;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const currentPresenter = getCurrentPresenter(activeCall);
 | 
			
		||||
    if (!currentPresenter && previousPresenterId) {
 | 
			
		||||
      if (previousPresenterId === ME) {
 | 
			
		||||
        setResult({
 | 
			
		||||
          type: 'dismissable',
 | 
			
		||||
          message: i18n('calling__presenting--you-stopped'),
 | 
			
		||||
        });
 | 
			
		||||
      } else if (previousPresenterTitle) {
 | 
			
		||||
        setResult({
 | 
			
		||||
          type: 'dismissable',
 | 
			
		||||
          message: i18n('calling__presenting--person-stopped', [
 | 
			
		||||
            previousPresenterTitle,
 | 
			
		||||
          ]),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [activeCall, i18n, previousPresenterId, previousPresenterTitle]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const currentPresenter = getCurrentPresenter(activeCall);
 | 
			
		||||
    if (currentPresenter === ME) {
 | 
			
		||||
      setPreviousPresenter({
 | 
			
		||||
        id: ME,
 | 
			
		||||
      });
 | 
			
		||||
    } else if (!currentPresenter) {
 | 
			
		||||
      setPreviousPresenter(undefined);
 | 
			
		||||
    } else {
 | 
			
		||||
      const { id, title } = currentPresenter;
 | 
			
		||||
      setPreviousPresenter({ id, title });
 | 
			
		||||
    }
 | 
			
		||||
  }, [activeCall]);
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DEFAULT_DELAY = 5000;
 | 
			
		||||
 | 
			
		||||
// In the future, this component should show toasts when users join or leave. See
 | 
			
		||||
//   DESKTOP-902.
 | 
			
		||||
export const CallingToastManager: React.FC<PropsType> = props => {
 | 
			
		||||
  const reconnectingToast = getReconnectingToast(props);
 | 
			
		||||
  const screenSharingToast = useScreenSharingToast(props);
 | 
			
		||||
 | 
			
		||||
  let toast: ToastType;
 | 
			
		||||
  if (reconnectingToast) {
 | 
			
		||||
    toast = reconnectingToast;
 | 
			
		||||
  } else if (screenSharingToast) {
 | 
			
		||||
    toast = screenSharingToast;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [toastMessage, setToastMessage] = useState('');
 | 
			
		||||
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
 | 
			
		||||
 | 
			
		||||
  const dismissToast = useCallback(() => {
 | 
			
		||||
    if (timeoutRef) {
 | 
			
		||||
      setToastMessage('');
 | 
			
		||||
    }
 | 
			
		||||
  }, [setToastMessage, timeoutRef]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (toast) {
 | 
			
		||||
      if (toast.type === 'dismissable') {
 | 
			
		||||
        if (timeoutRef && timeoutRef.current) {
 | 
			
		||||
          clearTimeout(timeoutRef.current);
 | 
			
		||||
        }
 | 
			
		||||
        timeoutRef.current = setTimeout(dismissToast, DEFAULT_DELAY);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setToastMessage(toast.message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (timeoutRef && timeoutRef.current) {
 | 
			
		||||
        clearTimeout(timeoutRef.current);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }, [dismissToast, setToastMessage, timeoutRef, toast]);
 | 
			
		||||
 | 
			
		||||
  const isVisible = Boolean(toastMessage);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      className={classNames('module-ongoing-call__toast', {
 | 
			
		||||
        'module-ongoing-call__toast--hidden': !isVisible,
 | 
			
		||||
      })}
 | 
			
		||||
      type="button"
 | 
			
		||||
      onClick={dismissToast}
 | 
			
		||||
    >
 | 
			
		||||
      {toastMessage}
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +22,8 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
 | 
			
		|||
  demuxId: index,
 | 
			
		||||
  hasRemoteAudio: index % 3 !== 0,
 | 
			
		||||
  hasRemoteVideo: index % 4 !== 0,
 | 
			
		||||
  presenting: false,
 | 
			
		||||
  sharingScreen: false,
 | 
			
		||||
  videoAspectRatio: 1.3,
 | 
			
		||||
  ...getDefaultConversation({
 | 
			
		||||
    isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,6 +42,8 @@ const createProps = (
 | 
			
		|||
    demuxId: 123,
 | 
			
		||||
    hasRemoteAudio: false,
 | 
			
		||||
    hasRemoteVideo: true,
 | 
			
		||||
    presenting: false,
 | 
			
		||||
    sharingScreen: false,
 | 
			
		||||
    videoAspectRatio: 1.3,
 | 
			
		||||
    ...getDefaultConversation({
 | 
			
		||||
      isBlocked: Boolean(isBlocked),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -105,7 +105,8 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
 | 
			
		|||
  // 2. Split participants into two groups: ones in the main grid and ones in the overflow
 | 
			
		||||
  //   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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,37 +0,0 @@
 | 
			
		|||
// Copyright 2020-2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { GroupCallConnectionState } from '../types/Calling';
 | 
			
		||||
import { LocalizerType } from '../types/Util';
 | 
			
		||||
 | 
			
		||||
type PropsType = {
 | 
			
		||||
  connectionState: GroupCallConnectionState;
 | 
			
		||||
  i18n: LocalizerType;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// In the future, this component should show toasts when users join or leave. See
 | 
			
		||||
//   DESKTOP-902.
 | 
			
		||||
export const GroupCallToastManager: React.FC<PropsType> = ({
 | 
			
		||||
  connectionState,
 | 
			
		||||
  i18n,
 | 
			
		||||
}) => {
 | 
			
		||||
  const [isVisible, setIsVisible] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setIsVisible(connectionState === GroupCallConnectionState.Reconnecting);
 | 
			
		||||
  }, [connectionState, setIsVisible]);
 | 
			
		||||
 | 
			
		||||
  const message = i18n('callReconnecting');
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames('module-ongoing-call__toast', {
 | 
			
		||||
        'module-ongoing-call__toast--hidden': !isVisible,
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
      {message}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										60
									
								
								ts/components/NeedsScreenRecordingPermissionsModal.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								ts/components/NeedsScreenRecordingPermissionsModal.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,60 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { LocalizerType } from '../types/Util';
 | 
			
		||||
import { Theme } from '../util/theme';
 | 
			
		||||
import { Modal } from './Modal';
 | 
			
		||||
import { Button, ButtonVariant } from './Button';
 | 
			
		||||
 | 
			
		||||
type PropsType = {
 | 
			
		||||
  i18n: LocalizerType;
 | 
			
		||||
  openSystemPreferencesAction: () => unknown;
 | 
			
		||||
  toggleScreenRecordingPermissionsDialog: () => unknown;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function focusRef(el: HTMLElement | null) {
 | 
			
		||||
  if (el) {
 | 
			
		||||
    el.focus();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const NeedsScreenRecordingPermissionsModal = ({
 | 
			
		||||
  i18n,
 | 
			
		||||
  openSystemPreferencesAction,
 | 
			
		||||
  toggleScreenRecordingPermissionsDialog,
 | 
			
		||||
}: PropsType): JSX.Element => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      i18n={i18n}
 | 
			
		||||
      title={i18n('calling__presenting--permission-title')}
 | 
			
		||||
      theme={Theme.Dark}
 | 
			
		||||
    >
 | 
			
		||||
      <p>{i18n('calling__presenting--macos-permission-description')}</p>
 | 
			
		||||
      <ol style={{ paddingLeft: 16 }}>
 | 
			
		||||
        <li>{i18n('calling__presenting--permission-instruction-step1')}</li>
 | 
			
		||||
        <li>{i18n('calling__presenting--permission-instruction-step2')}</li>
 | 
			
		||||
        <li>{i18n('calling__presenting--permission-instruction-step3')}</li>
 | 
			
		||||
        <li>{i18n('calling__presenting--permission-instruction-step4')}</li>
 | 
			
		||||
      </ol>
 | 
			
		||||
      <Modal.Footer>
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={toggleScreenRecordingPermissionsDialog}
 | 
			
		||||
          ref={focusRef}
 | 
			
		||||
          variant={ButtonVariant.Secondary}
 | 
			
		||||
        >
 | 
			
		||||
          {i18n('calling__presenting--permission-cancel')}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            openSystemPreferencesAction();
 | 
			
		||||
            toggleScreenRecordingPermissionsDialog();
 | 
			
		||||
          }}
 | 
			
		||||
          variant={ButtonVariant.Primary}
 | 
			
		||||
        >
 | 
			
		||||
          {i18n('calling__presenting--permission-open')}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Modal.Footer>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										29
									
								
								ts/hooks/useActivateSpeakerViewOnPresenting.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ts/hooks/useActivateSpeakerViewOnPresenting.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { usePrevious } from '../util/hooks';
 | 
			
		||||
 | 
			
		||||
type RemoteParticipant = {
 | 
			
		||||
  hasRemoteVideo: boolean;
 | 
			
		||||
  presenting: boolean;
 | 
			
		||||
  title: string;
 | 
			
		||||
  uuid?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function useActivateSpeakerViewOnPresenting(
 | 
			
		||||
  remoteParticipants: ReadonlyArray<RemoteParticipant>,
 | 
			
		||||
  isInSpeakerView: boolean,
 | 
			
		||||
  toggleSpeakerView: () => void
 | 
			
		||||
): void {
 | 
			
		||||
  const presenterUuid = remoteParticipants.find(
 | 
			
		||||
    participant => participant.presenting
 | 
			
		||||
  )?.uuid;
 | 
			
		||||
  const prevPresenterUuid = usePrevious(presenterUuid, presenterUuid);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (prevPresenterUuid !== presenterUuid && !isInSpeakerView) {
 | 
			
		||||
      toggleSpeakerView();
 | 
			
		||||
    }
 | 
			
		||||
  }, [isInSpeakerView, presenterUuid, prevPresenterUuid, toggleSpeakerView]);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,9 @@
 | 
			
		|||
// Copyright 2020 Signal Messenger, LLC
 | 
			
		||||
// Copyright 2020-2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
/* 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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										12
									
								
								ts/util/isScreenSharingEnabled.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								ts/util/isScreenSharingEnabled.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
import * as RemoteConfig from '../RemoteConfig';
 | 
			
		||||
 | 
			
		||||
// We can remove this function once screen sharing has been turned on for everyone
 | 
			
		||||
export function isScreenSharingEnabled(): boolean {
 | 
			
		||||
  return (
 | 
			
		||||
    RemoteConfig.isEnabled('desktop.worksAtSignal') ||
 | 
			
		||||
    RemoteConfig.isEnabled('desktop.screensharing')
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2726,6 +2726,13 @@
 | 
			
		|||
    "updated": "2020-08-26T00:10:28.628Z",
 | 
			
		||||
    "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
									
									
								
							
							
						
						
									
										11
									
								
								ts/window.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -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
									
								
							
							
						
						
									
										11
									
								
								ts/windows/screenShare.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
// This needs to use window.React & window.ReactDOM since it's
 | 
			
		||||
// not commonJS compatible.
 | 
			
		||||
window.registerScreenShareControllerRenderer((Component, props) => {
 | 
			
		||||
  window.ReactDOM.render(
 | 
			
		||||
    window.React.createElement(Component, props),
 | 
			
		||||
    document.getElementById('app')
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ const context = __dirname;
 | 
			
		|||
const { NODE_ENV: mode = 'development' } = process.env;
 | 
			
		||||
 | 
			
		||||
const 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',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										97
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										97
									
								
								yarn.lock
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue