Add screensharing behind a feature flag
This commit is contained in:
parent
7c7f7ee5a0
commit
ceffc2380c
49 changed files with 2044 additions and 164 deletions
|
@ -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>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue