Add screensharing behind a feature flag

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,29 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import {
CallingScreenSharingController,
PropsType,
} from './CallingScreenSharingController';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const createProps = (): PropsType => ({
i18n,
onCloseController: action('on-close-controller'),
onStopSharing: action('on-stop-sharing'),
presentedSourceName: 'Application',
});
const story = storiesOf('Components/CallingScreenSharingController', module);
story.add('Controller', () => {
return <CallingScreenSharingController {...createProps()} />;
});

View file

@ -0,0 +1,39 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Button, ButtonVariant } from './Button';
import { LocalizerType } from '../types/Util';
export type PropsType = {
i18n: LocalizerType;
onCloseController: () => unknown;
onStopSharing: () => unknown;
presentedSourceName: string;
};
export const CallingScreenSharingController = ({
i18n,
onCloseController,
onStopSharing,
presentedSourceName,
}: PropsType): JSX.Element => {
return (
<div className="module-CallingScreenSharingController">
<div className="module-CallingScreenSharingController__text">
{i18n('calling__presenting--info', [presentedSourceName])}
</div>
<div className="module-CallingScreenSharingController__buttons">
<Button onClick={onStopSharing} variant={ButtonVariant.Destructive}>
{i18n('calling__presenting--stop')}
</Button>
<button
aria-label={i18n('close')}
className="module-CallingScreenSharingController__close"
onClick={onCloseController}
type="button"
/>
</div>
</div>
);
};

View file

@ -0,0 +1,61 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import {
CallingSelectPresentingSourcesModal,
PropsType,
} from './CallingSelectPresentingSourcesModal';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const createProps = (): PropsType => ({
i18n,
presentingSourcesAvailable: [
{
id: 'screen',
name: 'Entire Screen',
thumbnail:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P/1PwAF8AL1sEVIPAAAAABJRU5ErkJggg==',
},
{
id: 'window:123',
name: 'Bozirro Airhorse',
thumbnail:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z1D4HwAF5wJxzsNOIAAAAABJRU5ErkJggg==',
},
{
id: 'window:456',
name: 'Discoverer',
thumbnail:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8HwHwAFHQIIj4yLtgAAAABJRU5ErkJggg==',
},
{
id: 'window:789',
name: 'Signal Beta',
thumbnail: '',
},
{
id: 'window:xyz',
name: 'Window that has a really long name and overflows',
thumbnail:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+O/wHwAEhgJAyqFnAgAAAABJRU5ErkJggg==',
},
],
setPresenting: action('set-presenting'),
});
const story = storiesOf(
'Components/CallingSelectPresentingSourcesModal',
module
);
story.add('Modal', () => {
return <CallingSelectPresentingSourcesModal {...createProps()} />;
});

View file

@ -0,0 +1,137 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
import classNames from 'classnames';
import { groupBy } from 'lodash';
import { Button, ButtonVariant } from './Button';
import { LocalizerType } from '../types/Util';
import { Modal } from './Modal';
import { PresentedSource, PresentableSource } from '../types/Calling';
import { Theme } from '../util/theme';
export type PropsType = {
i18n: LocalizerType;
presentingSourcesAvailable: Array<PresentableSource>;
setPresenting: (_?: PresentedSource) => void;
};
const Source = ({
onSourceClick,
source,
sourceToPresent,
}: {
onSourceClick: (source: PresentedSource) => void;
source: PresentableSource;
sourceToPresent?: PresentedSource;
}): JSX.Element => {
return (
<button
className={classNames({
'module-CallingSelectPresentingSourcesModal__source': true,
'module-CallingSelectPresentingSourcesModal__source--selected':
sourceToPresent?.id === source.id,
})}
key={source.id}
onClick={() => {
onSourceClick({
id: source.id,
name: source.name,
});
}}
type="button"
>
<img
alt={source.name}
className="module-CallingSelectPresentingSourcesModal__name--screenshot"
src={source.thumbnail}
/>
<div className="module-CallingSelectPresentingSourcesModal__name--container">
{source.appIcon ? (
<img
alt={source.name}
className="module-CallingSelectPresentingSourcesModal__name--icon"
height={16}
src={source.appIcon}
width={16}
/>
) : null}
<span className="module-CallingSelectPresentingSourcesModal__name--text">
{source.name}
</span>
</div>
</button>
);
};
export const CallingSelectPresentingSourcesModal = ({
i18n,
presentingSourcesAvailable,
setPresenting,
}: PropsType): JSX.Element | null => {
const [sourceToPresent, setSourceToPresent] = useState<
PresentedSource | undefined
>(undefined);
if (!presentingSourcesAvailable.length) {
throw new Error('No sources available for presenting');
}
const sources = groupBy(presentingSourcesAvailable, source =>
source.id.startsWith('screen')
);
return (
<Modal
hasXButton
i18n={i18n}
moduleClassName="module-CallingSelectPresentingSourcesModal"
onClose={() => {
setPresenting(sourceToPresent);
}}
theme={Theme.Dark}
title={i18n('calling__SelectPresentingSourcesModal--title')}
>
<div className="module-CallingSelectPresentingSourcesModal__title">
{i18n('calling__SelectPresentingSourcesModal--entireScreen')}
</div>
<div className="module-CallingSelectPresentingSourcesModal__sources">
{sources.true.map(source => (
<Source
key={source.id}
onSourceClick={selectedSource => setSourceToPresent(selectedSource)}
source={source}
sourceToPresent={sourceToPresent}
/>
))}
</div>
<div className="module-CallingSelectPresentingSourcesModal__title">
{i18n('calling__SelectPresentingSourcesModal--window')}
</div>
<div className="module-CallingSelectPresentingSourcesModal__sources">
{sources.false.map(source => (
<Source
key={source.id}
onSourceClick={selectedSource => setSourceToPresent(selectedSource)}
source={source}
sourceToPresent={sourceToPresent}
/>
))}
</div>
<Modal.Footer moduleClassName="module-CallingSelectPresentingSourcesModal">
<Button
onClick={() => setPresenting()}
variant={ButtonVariant.Secondary}
>
{i18n('cancel')}
</Button>
<Button
disabled={!sourceToPresent}
onClick={() => setPresenting(sourceToPresent)}
>
{i18n('calling__SelectPresentingSourcesModal--confirm')}
</Button>
</Modal.Footer>
</Modal>
);
};

View file

@ -0,0 +1,163 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import {
ActiveCallType,
CallMode,
GroupCallConnectionState,
} from '../types/Calling';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
type PropsType = {
activeCall: ActiveCallType;
i18n: LocalizerType;
};
type ToastType =
| {
message: string;
type: 'dismissable' | 'static';
}
| undefined;
function getReconnectingToast({ activeCall, i18n }: PropsType): ToastType {
if (
activeCall.callMode === CallMode.Group &&
activeCall.connectionState === GroupCallConnectionState.Reconnecting
) {
return {
message: i18n('callReconnecting'),
type: 'static',
};
}
return undefined;
}
const ME = Symbol('me');
function getCurrentPresenter(
activeCall: Readonly<ActiveCallType>
): ConversationType | typeof ME | undefined {
if (activeCall.presentingSource) {
return ME;
}
if (activeCall.callMode === CallMode.Direct) {
const isOtherPersonPresenting = activeCall.remoteParticipants.some(
participant => participant.presenting
);
return isOtherPersonPresenting ? activeCall.conversation : undefined;
}
if (activeCall.callMode === CallMode.Group) {
return activeCall.remoteParticipants.find(
participant => participant.presenting
);
}
return undefined;
}
function useScreenSharingToast({ activeCall, i18n }: PropsType): ToastType {
const [result, setResult] = useState<undefined | ToastType>(undefined);
const [previousPresenter, setPreviousPresenter] = useState<
undefined | { id: string | typeof ME; title?: string }
>(undefined);
const previousPresenterId = previousPresenter?.id;
const previousPresenterTitle = previousPresenter?.title;
useEffect(() => {
const currentPresenter = getCurrentPresenter(activeCall);
if (!currentPresenter && previousPresenterId) {
if (previousPresenterId === ME) {
setResult({
type: 'dismissable',
message: i18n('calling__presenting--you-stopped'),
});
} else if (previousPresenterTitle) {
setResult({
type: 'dismissable',
message: i18n('calling__presenting--person-stopped', [
previousPresenterTitle,
]),
});
}
}
}, [activeCall, i18n, previousPresenterId, previousPresenterTitle]);
useEffect(() => {
const currentPresenter = getCurrentPresenter(activeCall);
if (currentPresenter === ME) {
setPreviousPresenter({
id: ME,
});
} else if (!currentPresenter) {
setPreviousPresenter(undefined);
} else {
const { id, title } = currentPresenter;
setPreviousPresenter({ id, title });
}
}, [activeCall]);
return result;
}
const DEFAULT_DELAY = 5000;
// In the future, this component should show toasts when users join or leave. See
// DESKTOP-902.
export const CallingToastManager: React.FC<PropsType> = props => {
const reconnectingToast = getReconnectingToast(props);
const screenSharingToast = useScreenSharingToast(props);
let toast: ToastType;
if (reconnectingToast) {
toast = reconnectingToast;
} else if (screenSharingToast) {
toast = screenSharingToast;
}
const [toastMessage, setToastMessage] = useState('');
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const dismissToast = useCallback(() => {
if (timeoutRef) {
setToastMessage('');
}
}, [setToastMessage, timeoutRef]);
useEffect(() => {
if (toast) {
if (toast.type === 'dismissable') {
if (timeoutRef && timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(dismissToast, DEFAULT_DELAY);
}
setToastMessage(toast.message);
}
return () => {
if (timeoutRef && timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [dismissToast, setToastMessage, timeoutRef, toast]);
const isVisible = Boolean(toastMessage);
return (
<button
className={classNames('module-ongoing-call__toast', {
'module-ongoing-call__toast--hidden': !isVisible,
})}
type="button"
onClick={dismissToast}
>
{toastMessage}
</button>
);
};

View file

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

View file

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

View file

@ -105,7 +105,8 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
// 2. Split participants into two groups: ones in the main grid and ones in the overflow
// sidebar.
//
// We start by sorting by `speakerTime` so that the most recent speakers are first in
// We start by sorting by `presenting` first since presenters should be on the main grid
// then we sort by `speakerTime` so that the most recent speakers are next in
// line for the main grid. Then we split the list in two: one for the grid and one for
// the overflow area.
//
@ -119,7 +120,9 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
remoteParticipants
.concat()
.sort(
(a, b) => (b.speakerTime || -Infinity) - (a.speakerTime || -Infinity)
(a, b) =>
Number(b.presenting || 0) - Number(a.presenting || 0) ||
(b.speakerTime || -Infinity) - (a.speakerTime || -Infinity)
),
[remoteParticipants]
);
@ -275,18 +278,23 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
if (isPageVisible) {
setGroupCallVideoRequest([
...gridParticipants.map(participant => {
if (participant.hasRemoteVideo) {
return {
demuxId: participant.demuxId,
width: Math.floor(
gridParticipantHeight *
participant.videoAspectRatio *
VIDEO_REQUEST_SCALAR
),
height: Math.floor(gridParticipantHeight * VIDEO_REQUEST_SCALAR),
};
let scalar: number;
if (participant.sharingScreen) {
// We want best-resolution video if someone is sharing their screen. This code
// is extra-defensive against strange devicePixelRatios.
scalar = Math.max(window.devicePixelRatio || 1, 1);
} else if (participant.hasRemoteVideo) {
scalar = VIDEO_REQUEST_SCALAR;
} else {
scalar = 0;
}
return nonRenderedRemoteParticipant(participant);
return {
demuxId: participant.demuxId,
width: Math.floor(
gridParticipantHeight * participant.videoAspectRatio * scalar
),
height: Math.floor(gridParticipantHeight * scalar),
};
}),
...overflowedParticipants.map(participant => {
if (participant.hasRemoteVideo) {

View file

@ -1,37 +0,0 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import { GroupCallConnectionState } from '../types/Calling';
import { LocalizerType } from '../types/Util';
type PropsType = {
connectionState: GroupCallConnectionState;
i18n: LocalizerType;
};
// In the future, this component should show toasts when users join or leave. See
// DESKTOP-902.
export const GroupCallToastManager: React.FC<PropsType> = ({
connectionState,
i18n,
}) => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setIsVisible(connectionState === GroupCallConnectionState.Reconnecting);
}, [connectionState, setIsVisible]);
const message = i18n('callReconnecting');
return (
<div
className={classNames('module-ongoing-call__toast', {
'module-ongoing-call__toast--hidden': !isVisible,
})}
>
{message}
</div>
);
};

View file

@ -0,0 +1,60 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { LocalizerType } from '../types/Util';
import { Theme } from '../util/theme';
import { Modal } from './Modal';
import { Button, ButtonVariant } from './Button';
type PropsType = {
i18n: LocalizerType;
openSystemPreferencesAction: () => unknown;
toggleScreenRecordingPermissionsDialog: () => unknown;
};
function focusRef(el: HTMLElement | null) {
if (el) {
el.focus();
}
}
export const NeedsScreenRecordingPermissionsModal = ({
i18n,
openSystemPreferencesAction,
toggleScreenRecordingPermissionsDialog,
}: PropsType): JSX.Element => {
return (
<Modal
i18n={i18n}
title={i18n('calling__presenting--permission-title')}
theme={Theme.Dark}
>
<p>{i18n('calling__presenting--macos-permission-description')}</p>
<ol style={{ paddingLeft: 16 }}>
<li>{i18n('calling__presenting--permission-instruction-step1')}</li>
<li>{i18n('calling__presenting--permission-instruction-step2')}</li>
<li>{i18n('calling__presenting--permission-instruction-step3')}</li>
<li>{i18n('calling__presenting--permission-instruction-step4')}</li>
</ol>
<Modal.Footer>
<Button
onClick={toggleScreenRecordingPermissionsDialog}
ref={focusRef}
variant={ButtonVariant.Secondary}
>
{i18n('calling__presenting--permission-cancel')}
</Button>
<Button
onClick={() => {
openSystemPreferencesAction();
toggleScreenRecordingPermissionsDialog();
}}
variant={ButtonVariant.Primary}
>
{i18n('calling__presenting--permission-open')}
</Button>
</Modal.Footer>
</Modal>
);
};

View file

@ -0,0 +1,29 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useEffect } from 'react';
import { usePrevious } from '../util/hooks';
type RemoteParticipant = {
hasRemoteVideo: boolean;
presenting: boolean;
title: string;
uuid?: string;
};
export function useActivateSpeakerViewOnPresenting(
remoteParticipants: ReadonlyArray<RemoteParticipant>,
isInSpeakerView: boolean,
toggleSpeakerView: () => void
): void {
const presenterUuid = remoteParticipants.find(
participant => participant.presenting
)?.uuid;
const prevPresenterUuid = usePrevious(presenterUuid, presenterUuid);
useEffect(() => {
if (prevPresenterUuid !== presenterUuid && !isInSpeakerView) {
toggleSpeakerView();
}
}, [isInSpeakerView, presenterUuid, prevPresenterUuid, toggleSpeakerView]);
}

View file

@ -1,8 +1,9 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable class-methods-use-this */
import { desktopCapturer, ipcRenderer } from 'electron';
import {
Call,
CallEndedReason,
@ -44,6 +45,8 @@ import {
MediaDeviceSettings,
GroupCallConnectionState,
GroupCallJoinState,
PresentableSource,
PresentedSource,
} from '../types/Calling';
import { ConversationModel } from '../models/conversations';
import {
@ -64,6 +67,7 @@ import {
REQUESTED_VIDEO_HEIGHT,
REQUESTED_VIDEO_FRAMERATE,
} from '../calling/constants';
import { notify } from './notify';
const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map<
HttpMethod,
@ -100,12 +104,14 @@ export class CallingClass {
private callsByConversation: { [conversationId: string]: Call | GroupCall };
private hadLocalVideoBeforePresenting?: boolean;
constructor() {
this.videoCapturer = new GumVideoCapturer(
REQUESTED_VIDEO_WIDTH,
REQUESTED_VIDEO_HEIGHT,
REQUESTED_VIDEO_FRAMERATE
);
this.videoCapturer = new GumVideoCapturer({
maxWidth: REQUESTED_VIDEO_WIDTH,
maxHeight: REQUESTED_VIDEO_HEIGHT,
maxFramerate: REQUESTED_VIDEO_FRAMERATE,
});
this.videoRenderer = new CanvasVideoRenderer();
this.callsByConversation = {};
@ -127,6 +133,10 @@ export class CallingClass {
RingRTC.handleLogMessage = this.handleLogMessage.bind(this);
RingRTC.handleSendHttpRequest = this.handleSendHttpRequest.bind(this);
RingRTC.handleSendCallMessage = this.handleSendCallMessage.bind(this);
ipcRenderer.on('stop-screen-share', () => {
uxActions.setPresenting();
});
}
async startCallingLobby(
@ -247,7 +257,7 @@ export class CallingClass {
}
stopCallingLobby(conversationId?: string): void {
this.disableLocalCamera();
this.disableLocalVideo();
this.stopDeviceReselectionTimer();
this.lastMediaDeviceSettings = undefined;
@ -441,7 +451,7 @@ export class CallingClass {
// NOTE: This assumes that only one call is active at a time. For example, if
// there are two calls using the camera, this will disable both of them.
// That's fine for now, but this will break if that assumption changes.
this.disableLocalCamera();
this.disableLocalVideo();
delete this.callsByConversation[conversationId];
@ -457,7 +467,7 @@ export class CallingClass {
// NOTE: This assumes only one active call at a time. See comment above.
if (localDeviceState.videoMuted) {
this.disableLocalCamera();
this.disableLocalVideo();
} else {
this.videoCapturer.enableCaptureAndSend(groupCall);
}
@ -689,6 +699,8 @@ export class CallingClass {
demuxId: remoteDeviceState.demuxId,
hasRemoteAudio: !remoteDeviceState.audioMuted,
hasRemoteVideo: !remoteDeviceState.videoMuted,
presenting: Boolean(remoteDeviceState.presenting),
sharingScreen: Boolean(remoteDeviceState.sharingScreen),
speakerTime: normalizeGroupCallTimestamp(
remoteDeviceState.speakerTime
),
@ -807,6 +819,8 @@ export class CallingClass {
return;
}
ipcRenderer.send('close-screen-share-controller');
if (call instanceof Call) {
RingRTC.hangup(call.callId);
} else if (call instanceof GroupCall) {
@ -851,6 +865,101 @@ export class CallingClass {
}
}
private setOutgoingVideoIsScreenShare(
call: Call | GroupCall,
enabled: boolean
): void {
if (call instanceof Call) {
RingRTC.setOutgoingVideoIsScreenShare(call.callId, enabled);
// Note: there is no "presenting" API for direct calls.
} else if (call instanceof GroupCall) {
call.setOutgoingVideoIsScreenShare(enabled);
call.setPresenting(enabled);
} else {
throw missingCaseError(call);
}
}
async getPresentingSources(): Promise<Array<PresentableSource>> {
const sources = await desktopCapturer.getSources({
fetchWindowIcons: true,
thumbnailSize: { height: 102, width: 184 },
types: ['window', 'screen'],
});
const presentableSources: Array<PresentableSource> = [];
sources.forEach(source => {
// If electron can't retrieve a thumbnail then it won't be able to
// present this source so we filter these out.
if (source.thumbnail.isEmpty()) {
return;
}
presentableSources.push({
appIcon:
source.appIcon && !source.appIcon.isEmpty()
? source.appIcon.toDataURL()
: undefined,
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL(),
});
});
return presentableSources;
}
setPresenting(
conversationId: string,
hasLocalVideo: boolean,
source?: PresentedSource
): void {
const call = getOwn(this.callsByConversation, conversationId);
if (!call) {
window.log.warn('Trying to set presenting for a non-existent call');
return;
}
this.videoCapturer.disable();
if (source) {
this.hadLocalVideoBeforePresenting = hasLocalVideo;
this.videoCapturer.enableCaptureAndSend(call, {
// 15fps is much nicer but takes up a lot more CPU.
maxFramerate: 5,
maxHeight: 1080,
maxWidth: 1920,
screenShareSourceId: source.id,
});
this.setOutgoingVideo(conversationId, true);
} else {
this.setOutgoingVideo(
conversationId,
Boolean(this.hadLocalVideoBeforePresenting) || hasLocalVideo
);
this.hadLocalVideoBeforePresenting = undefined;
}
const isPresenting = Boolean(source);
this.setOutgoingVideoIsScreenShare(call, isPresenting);
if (source) {
ipcRenderer.send('show-screen-share', source.name);
notify({
icon: 'images/icons/v2/video-solid-24.svg',
message: window.i18n('calling__presenting--notification-body'),
onNotificationClick: () => {
if (this.uxActions) {
this.uxActions.setPresenting();
}
},
silent: true,
title: window.i18n('calling__presenting--notification-title'),
});
} else {
ipcRenderer.send('close-screen-share-controller');
}
}
private async startDeviceReselectionTimer(): Promise<void> {
// Poll once
await this.pollForMediaDevices();
@ -1066,7 +1175,7 @@ export class CallingClass {
this.videoCapturer.enableCapture();
}
disableLocalCamera(): void {
disableLocalVideo(): void {
this.videoCapturer.disable();
}
@ -1387,6 +1496,14 @@ export class CallingClass {
hasVideo: call.remoteVideoEnabled,
});
};
// eslint-disable-next-line no-param-reassign
call.handleRemoteSharingScreen = () => {
uxActions.remoteSharingScreenChange({
conversationId: conversation.id,
isSharingScreen: Boolean(call.remoteSharingScreen),
});
};
}
private async handleLogMessage(

View file

@ -1,10 +1,16 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer } from 'electron';
import { ThunkAction } from 'redux-thunk';
import { CallEndedReason } from 'ringrtc';
import {
hasScreenCapturePermission,
openSystemPreferences,
} from 'mac-screen-capture-permissions';
import { has, omit } from 'lodash';
import { getOwn } from '../../util/getOwn';
import { getPlatform } from '../selectors/user';
import { missingCaseError } from '../../util/missingCaseError';
import { notify } from '../../services/notify';
import { calling } from '../../services/calling';
@ -18,6 +24,8 @@ import {
GroupCallJoinState,
GroupCallVideoRequest,
MediaDeviceSettings,
PresentedSource,
PresentableSource,
} from '../../types/Calling';
import { callingTones } from '../../util/callingTones';
import { requestCameraPermissions } from '../../util/callingPermissions';
@ -43,6 +51,8 @@ export type GroupCallParticipantInfoType = {
demuxId: number;
hasRemoteAudio: boolean;
hasRemoteVideo: boolean;
presenting: boolean;
sharingScreen: boolean;
speakerTime?: number;
videoAspectRatio: number;
};
@ -53,6 +63,7 @@ export type DirectCallStateType = {
callState?: CallState;
callEndedReason?: CallEndedReason;
isIncoming: boolean;
isSharingScreen?: boolean;
isVideoCall: boolean;
hasRemoteVideo?: boolean;
};
@ -73,8 +84,11 @@ export type ActiveCallStateType = {
isInSpeakerView: boolean;
joinedAt?: number;
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>;
safetyNumberChangedUuids: Array<string>;
settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;
};
@ -160,6 +174,11 @@ export type RemoteVideoChangeType = {
hasVideo: boolean;
};
type RemoteSharingScreenChangeType = {
conversationId: string;
isSharingScreen: boolean;
};
export type SetLocalAudioType = {
enabled: boolean;
};
@ -236,10 +255,15 @@ const OUTGOING_CALL = 'calling/OUTGOING_CALL';
const PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED =
'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
const REMOTE_SHARING_SCREEN_CHANGE = 'calling/REMOTE_SHARING_SCREEN_CHANGE';
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
const RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
const SET_PRESENTING = 'calling/SET_PRESENTING';
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
const TOGGLE_PARTICIPANTS = 'calling/TOGGLE_PARTICIPANTS';
const TOGGLE_PIP = 'calling/TOGGLE_PIP';
@ -326,6 +350,11 @@ type RefreshIODevicesActionType = {
payload: MediaDeviceSettings;
};
type RemoteSharingScreenChangeActionType = {
type: 'calling/REMOTE_SHARING_SCREEN_CHANGE';
payload: RemoteSharingScreenChangeType;
};
type RemoteVideoChangeActionType = {
type: 'calling/REMOTE_VIDEO_CHANGE';
payload: RemoteVideoChangeType;
@ -345,6 +374,16 @@ type SetLocalVideoFulfilledActionType = {
payload: SetLocalVideoType;
};
type SetPresentingFulfilledActionType = {
type: 'calling/SET_PRESENTING';
payload?: PresentedSource;
};
type SetPresentingSourcesActionType = {
type: 'calling/SET_PRESENTING_SOURCES';
payload: Array<PresentableSource>;
};
type ShowCallLobbyActionType = {
type: 'calling/SHOW_CALL_LOBBY';
payload: ShowCallLobbyType;
@ -355,6 +394,10 @@ type StartDirectCallActionType = {
payload: StartDirectCallType;
};
type ToggleNeedsScreenRecordingPermissionsActionType = {
type: 'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
};
type ToggleParticipantsActionType = {
type: 'calling/TOGGLE_PARTICIPANTS';
};
@ -387,14 +430,18 @@ export type CallingActionType =
| OutgoingCallActionType
| PeekNotConnectedGroupCallFulfilledActionType
| RefreshIODevicesActionType
| RemoteSharingScreenChangeActionType
| RemoteVideoChangeActionType
| ReturnToActiveCallActionType
| SetLocalAudioActionType
| SetLocalVideoFulfilledActionType
| SetPresentingSourcesActionType
| ShowCallLobbyActionType
| StartDirectCallActionType
| ToggleNeedsScreenRecordingPermissionsActionType
| ToggleParticipantsActionType
| TogglePipActionType
| SetPresentingFulfilledActionType
| ToggleSettingsActionType
| ToggleSpeakerViewActionType;
@ -438,6 +485,7 @@ function callStateChange(
}
if (callState === CallState.Ended) {
await callingTones.playEndCall();
ipcRenderer.send('close-screen-share-controller');
}
dispatch({
@ -519,10 +567,59 @@ function declineCall(payload: DeclineCallType): DeclineCallActionType {
};
}
function getPresentingSources(): ThunkAction<
void,
RootStateType,
unknown,
| SetPresentingSourcesActionType
| ToggleNeedsScreenRecordingPermissionsActionType
> {
return async (dispatch, getState) => {
// We check if the user has permissions first before calling desktopCapturer
// Next we call getPresentingSources so that one gets the prompt for permissions,
// if necessary.
// Finally, we have the if statement which shows the modal, if needed.
// It is in this exact order so that during first-time-use one will be
// prompted for permissions and if they so happen to deny we can still
// capture that state correctly.
const platform = getPlatform(getState());
const needsPermission =
platform === 'darwin' && !hasScreenCapturePermission();
const sources = await calling.getPresentingSources();
if (needsPermission) {
dispatch({
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
});
return;
}
dispatch({
type: SET_PRESENTING_SOURCES,
payload: sources,
});
};
}
function groupCallStateChange(
payload: GroupCallStateChangeArgumentType
): ThunkAction<void, RootStateType, unknown, GroupCallStateChangeActionType> {
return (dispatch, getState) => {
return async (dispatch, getState) => {
let didSomeoneStartPresenting: boolean;
const activeCall = getActiveCall(getState().calling);
if (activeCall?.callMode === CallMode.Group) {
const wasSomeonePresenting = activeCall.remoteParticipants.some(
participant => participant.presenting
);
const isSomeonePresenting = payload.remoteParticipants.some(
participant => participant.presenting
);
didSomeoneStartPresenting = !wasSomeonePresenting && isSomeonePresenting;
} else {
didSomeoneStartPresenting = false;
}
dispatch({
type: GROUP_CALL_STATE_CHANGE,
payload: {
@ -530,6 +627,10 @@ function groupCallStateChange(
ourUuid: getState().user.ourUuid,
},
});
if (didSomeoneStartPresenting) {
callingTones.someonePresenting();
}
};
}
@ -601,6 +702,17 @@ function receiveIncomingCall(
};
}
function openSystemPreferencesAction(): ThunkAction<
void,
RootStateType,
unknown,
never
> {
return () => {
openSystemPreferences();
};
}
function outgoingCall(payload: StartDirectCallType): OutgoingCallActionType {
callingTones.playRingtone();
@ -694,6 +806,15 @@ function refreshIODevices(
};
}
function remoteSharingScreenChange(
payload: RemoteSharingScreenChangeType
): RemoteSharingScreenChangeActionType {
return {
type: REMOTE_SHARING_SCREEN_CHANGE,
payload,
};
}
function remoteVideoChange(
payload: RemoteVideoChangeType
): RemoteVideoChangeActionType {
@ -764,7 +885,7 @@ function setLocalVideo(
} else if (payload.enabled) {
calling.enableLocalCamera();
} else {
calling.disableLocalCamera();
calling.disableLocalVideo();
}
({ enabled } = payload);
} else {
@ -797,6 +918,35 @@ function setGroupCallVideoRequest(
};
}
function setPresenting(
sourceToPresent?: PresentedSource
): ThunkAction<void, RootStateType, unknown, SetPresentingFulfilledActionType> {
return async (dispatch, getState) => {
const callingState = getState().calling;
const { activeCallState } = callingState;
const activeCall = getActiveCall(callingState);
if (!activeCall || !activeCallState) {
window.log.warn('Trying to present when no call is active');
return;
}
calling.setPresenting(
activeCall.conversationId,
activeCallState.hasLocalVideo,
sourceToPresent
);
dispatch({
type: SET_PRESENTING,
payload: sourceToPresent,
});
if (sourceToPresent) {
await callingTones.someonePresenting();
}
};
}
function startCallingLobby(
payload: StartCallingLobbyType
): ThunkAction<void, RootStateType, unknown, never> {
@ -857,6 +1007,12 @@ function togglePip(): TogglePipActionType {
};
}
function toggleScreenRecordingPermissionsDialog(): ToggleNeedsScreenRecordingPermissionsActionType {
return {
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
};
}
function toggleSettings(): ToggleSettingsActionType {
return {
type: TOGGLE_SETTINGS,
@ -871,31 +1027,36 @@ function toggleSpeakerView(): ToggleSpeakerViewActionType {
export const actions = {
acceptCall,
cancelCall,
callStateChange,
cancelCall,
changeIODevice,
closeNeedPermissionScreen,
declineCall,
getPresentingSources,
groupCallStateChange,
hangUp,
keyChanged,
keyChangeOk,
receiveIncomingCall,
keyChanged,
openSystemPreferencesAction,
outgoingCall,
peekNotConnectedGroupCall,
receiveIncomingCall,
refreshIODevices,
remoteSharingScreenChange,
remoteVideoChange,
returnToActiveCall,
setLocalPreview,
setRendererCanvas,
setLocalAudio,
setLocalVideo,
setGroupCallVideoRequest,
startCallingLobby,
setLocalAudio,
setLocalPreview,
setLocalVideo,
setPresenting,
setRendererCanvas,
showCallLobby,
startCall,
startCallingLobby,
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSettings,
toggleSpeakerView,
};
@ -1270,6 +1431,26 @@ export function reducer(
};
}
if (action.type === REMOTE_SHARING_SCREEN_CHANGE) {
const { conversationId, isSharingScreen } = action.payload;
const call = getOwn(state.callsByConversation, conversationId);
if (call?.callMode !== CallMode.Direct) {
window.log.warn('Cannot update remote video for a non-direct call');
return state;
}
return {
...state,
callsByConversation: {
...callsByConversation,
[conversationId]: {
...call,
isSharingScreen,
},
},
};
}
if (action.type === REMOTE_VIDEO_CHANGE) {
const { conversationId, hasVideo } = action.payload;
const call = getOwn(state.callsByConversation, conversationId);
@ -1427,6 +1608,59 @@ export function reducer(
};
}
if (action.type === SET_PRESENTING) {
const { activeCallState } = state;
if (!activeCallState) {
window.log.warn('Cannot toggle presenting when there is no active call');
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
presentingSource: action.payload,
presentingSourcesAvailable: undefined,
},
};
}
if (action.type === SET_PRESENTING_SOURCES) {
const { activeCallState } = state;
if (!activeCallState) {
window.log.warn(
'Cannot set presenting sources when there is no active call'
);
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
presentingSourcesAvailable: action.payload,
},
};
}
if (action.type === TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS) {
const { activeCallState } = state;
if (!activeCallState) {
window.log.warn(
'Cannot set presenting sources when there is no active call'
);
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
showNeedsScreenRecordingPermissionsWarning: !activeCallState.showNeedsScreenRecordingPermissionsWarning,
},
};
}
if (action.type === TOGGLE_SPEAKER_VIEW) {
const { activeCallState } = state;
if (!activeCallState) {

View file

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

View file

@ -86,6 +86,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -129,6 +131,188 @@ describe('calling duck', () => {
});
describe('actions', () => {
describe('getPresentingSources', () => {
beforeEach(function beforeEach() {
this.callingServiceGetPresentingSources = this.sandbox
.stub(callingService, 'getPresentingSources')
.resolves([
{
id: 'foo.bar',
name: 'Foo Bar',
thumbnail: 'xyz',
},
]);
});
it('retrieves sources from the calling service', async function test() {
const { getPresentingSources } = actions;
const dispatch = sinon.spy();
await getPresentingSources()(dispatch, getEmptyRootState, null);
sinon.assert.calledOnce(this.callingServiceGetPresentingSources);
});
it('dispatches SET_PRESENTING_SOURCES', async function test() {
const { getPresentingSources } = actions;
const dispatch = sinon.spy();
await getPresentingSources()(dispatch, getEmptyRootState, null);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/SET_PRESENTING_SOURCES',
payload: [
{
id: 'foo.bar',
name: 'Foo Bar',
thumbnail: 'xyz',
},
],
});
});
});
describe('remoteSharingScreenChange', () => {
it("updates whether someone's screen is being shared", () => {
const { remoteSharingScreenChange } = actions;
const payload = {
conversationId: 'fake-direct-call-conversation-id',
isSharingScreen: true,
};
const state = {
...stateWithActiveDirectCall,
};
const nextState = reducer(state, remoteSharingScreenChange(payload));
const expectedState = {
...stateWithActiveDirectCall,
callsByConversation: {
'fake-direct-call-conversation-id': {
...stateWithActiveDirectCall.callsByConversation[
'fake-direct-call-conversation-id'
],
isSharingScreen: true,
},
},
};
assert.deepEqual(nextState, expectedState);
});
});
describe('setPresenting', () => {
beforeEach(function beforeEach() {
this.callingServiceSetPresenting = this.sandbox.stub(
callingService,
'setPresenting'
);
});
it('calls setPresenting on the calling service', function test() {
const { setPresenting } = actions;
const dispatch = sinon.spy();
const presentedSource = {
id: 'window:786',
name: 'Application',
};
const getState = () => ({
...getEmptyRootState(),
calling: {
...stateWithActiveGroupCall,
},
});
setPresenting(presentedSource)(dispatch, getState, null);
sinon.assert.calledOnce(this.callingServiceSetPresenting);
sinon.assert.calledWith(
this.callingServiceSetPresenting,
'fake-group-call-conversation-id',
false,
presentedSource
);
});
it('dispatches SET_PRESENTING', () => {
const { setPresenting } = actions;
const dispatch = sinon.spy();
const presentedSource = {
id: 'window:786',
name: 'Application',
};
const getState = () => ({
...getEmptyRootState(),
calling: {
...stateWithActiveGroupCall,
},
});
setPresenting(presentedSource)(dispatch, getState, null);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/SET_PRESENTING',
payload: presentedSource,
});
});
it('turns off presenting when no value is passed in', () => {
const dispatch = sinon.spy();
const { setPresenting } = actions;
const presentedSource = {
id: 'window:786',
name: 'Application',
};
const getState = () => ({
...getEmptyRootState(),
calling: {
...stateWithActiveGroupCall,
},
});
setPresenting(presentedSource)(dispatch, getState, null);
const action = dispatch.getCall(0).args[0];
const nextState = reducer(getState().calling, action);
assert.isDefined(nextState.activeCallState);
assert.equal(
nextState.activeCallState?.presentingSource,
presentedSource
);
assert.isUndefined(
nextState.activeCallState?.presentingSourcesAvailable
);
});
it('sets the presenting value when one is passed in', () => {
const dispatch = sinon.spy();
const { setPresenting } = actions;
const getState = () => ({
...getEmptyRootState(),
calling: {
...stateWithActiveGroupCall,
},
});
setPresenting()(dispatch, getState, null);
const action = dispatch.getCall(0).args[0];
const nextState = reducer(getState().calling, action);
assert.isDefined(nextState.activeCallState);
assert.isUndefined(nextState.activeCallState?.presentingSource);
assert.isUndefined(
nextState.activeCallState?.presentingSourcesAvailable
);
});
});
describe('acceptCall', () => {
const { acceptCall } = actions;
@ -403,6 +587,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -429,6 +615,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -491,6 +679,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -515,6 +705,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -542,6 +734,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -571,6 +765,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -609,6 +805,8 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
},
],
@ -850,6 +1048,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -874,6 +1074,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -925,6 +1127,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],
@ -965,6 +1169,8 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
},
],

View file

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

View file

@ -54,6 +54,20 @@ class CallingTones {
}
});
}
// eslint-disable-next-line class-methods-use-this
async someonePresenting() {
const canPlayTone = await window.getCallRingtoneNotification();
if (!canPlayTone) {
return;
}
const tone = new Sound({
src: 'sounds/navigation_selection-complete-celebration.ogg',
});
await tone.play();
}
}
export const callingTones = new CallingTones();

View file

@ -0,0 +1,12 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as RemoteConfig from '../RemoteConfig';
// We can remove this function once screen sharing has been turned on for everyone
export function isScreenSharingEnabled(): boolean {
return (
RemoteConfig.isEnabled('desktop.worksAtSignal') ||
RemoteConfig.isEnabled('desktop.screensharing')
);
}

View file

@ -2726,6 +2726,13 @@
"updated": "2020-08-26T00:10:28.628Z",
"reasonDetail": "isn't react"
},
{
"rule": "jQuery-load(",
"path": "node_modules/execa/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2021-05-20T20:01:50.505Z"
},
{
"rule": "jQuery-wrap(",
"path": "node_modules/expand-range/node_modules/fill-range/index.js",
@ -2859,6 +2866,13 @@
"reasonCategory": "falseMatch",
"updated": "2018-09-15T00:38:04.183Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/foreground-child/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2021-05-20T20:01:50.505Z"
},
{
"rule": "jQuery-append(",
"path": "node_modules/form-data/lib/form_data.js",
@ -2880,6 +2894,13 @@
"reasonCategory": "falseMatch",
"updated": "2020-09-11T17:24:56.124Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/gauge/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2021-05-20T20:01:50.505Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/global-agent/node_modules/core-js/internals/collection.js",
@ -8702,6 +8723,13 @@
"reasonCategory": "falseMatch",
"updated": "2019-03-09T00:08:44.242Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/loud-rejection/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2021-05-20T20:01:50.505Z"
},
{
"rule": "thenify-multiArgs",
"path": "node_modules/make-dir/node_modules/pify/index.js",
@ -9407,6 +9435,13 @@
"updated": "2021-05-07T20:07:48.358Z",
"reasonDetail": "isn't jquery"
},
{
"rule": "jQuery-load(",
"path": "node_modules/os-locale/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2021-05-20T20:01:50.505Z"
},
{
"rule": "jQuery-append(",
"path": "node_modules/pac-proxy-agent/node_modules/socks/build/client/socksclient.js",
@ -11100,13 +11135,6 @@
"reasonCategory": "falseMatch",
"updated": "2020-09-11T17:24:56.124Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/proper-lockfile/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2021-04-06T04:01:59.934Z"
},
{
"rule": "eval",
"path": "node_modules/protobufjs/dist/light/protobuf.js",
@ -12619,13 +12647,6 @@
"reasonCategory": "falseMatch",
"updated": "2020-04-30T22:45:07.878Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/restore-cursor/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2020-04-25T01:47:02.583Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/rx-lite-aggregates/rx.lite.aggregates.min.js",
@ -12866,13 +12887,6 @@
"reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/spawn-wrap/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2020-04-25T01:47:02.583Z"
},
{
"rule": "jQuery-before(",
"path": "node_modules/sshpk/lib/dhe.js",
@ -12930,6 +12944,13 @@
"reasonCategory": "falseMatch",
"updated": "2021-01-20T22:42:00.662Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/term-size/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2021-05-20T20:01:50.505Z"
},
{
"rule": "jQuery-after(",
"path": "node_modules/test-exclude/node_modules/braces/index.js",
@ -13263,13 +13284,6 @@
"reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/write-file-atomic/node_modules/signal-exit/index.js",
"line": " load()",
"reasonCategory": "falseMatch",
"updated": "2020-04-30T22:35:27.860Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/xregexp/xregexp-all.js",
@ -13517,6 +13531,13 @@
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Used to get the local video element for rendering."
},
{
"rule": "React-useRef",
"path": "ts/components/CallingToastManager.js",
"line": " const timeoutRef = react_1.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-05-13T19:40:31.751Z"
},
{
"rule": "React-useRef",
"path": "ts/components/CaptchaDialog.js",

11
ts/window.d.ts vendored
View file

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

11
ts/windows/screenShare.ts Normal file
View file

@ -0,0 +1,11 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// This needs to use window.React & window.ReactDOM since it's
// not commonJS compatible.
window.registerScreenShareControllerRenderer((Component, props) => {
window.ReactDOM.render(
window.React.createElement(Component, props),
document.getElementById('app')
);
});