Calling: Improve the Picture-in-Picture popout
This commit is contained in:
parent
efffc4f569
commit
a623ee44c4
18 changed files with 941 additions and 261 deletions
|
@ -4123,6 +4123,14 @@
|
|||
"messageformat": "Leave",
|
||||
"description": "Title for the hangup button for a group call."
|
||||
},
|
||||
"icu:CallControls__JoinLeaveButton--hangup-1-1-tooltip": {
|
||||
"messageformat": "End call",
|
||||
"description": "The tooltip for the hangup button in the PIP for 1:1 calls"
|
||||
},
|
||||
"icu:CallControls__JoinLeaveButton--hangup-group-tooltip": {
|
||||
"messageformat": "Leave call",
|
||||
"description": "The tooltip for the hangup button in the PIP for group calls"
|
||||
},
|
||||
"icu:CallControls__MutedToast--muted": {
|
||||
"messageformat": "Mic off",
|
||||
"description": "Shown in a call when the user mutes their audio input using the Mute toggle button."
|
||||
|
|
BIN
fixtures/cat-screenshot-3x4.png
Normal file
BIN
fixtures/cat-screenshot-3x4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
|
@ -1 +1 @@
|
|||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16.98 3.75a.729.729 0 0 0-.73-.73h-4.583a.73.73 0 1 0 0 1.46h3.032l-1.256 1.046-2.709 2.708a.73.73 0 0 0 1.031 1.032l2.709-2.709 1.047-1.256v3.032a.73.73 0 1 0 1.458 0V3.75ZM3.234 16.766a.73.73 0 0 1-.213-.516v-4.583a.73.73 0 0 1 1.458 0v3.032l1.047-1.256 2.708-2.709a.73.73 0 0 1 1.032 1.032l-2.709 2.708-1.256 1.047h3.032a.73.73 0 1 1 0 1.458H3.75a.729.729 0 0 1-.516-.213Z" fill="#000"/></svg>
|
||||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16.98 16.25a.729.729 0 0 1-.73.73h-4.583a.73.73 0 0 1 0-1.46h3.032l-1.256-1.046-2.709-2.708a.73.73 0 0 1 1.031-1.032l2.709 2.709 1.047 1.256v-3.032a.73.73 0 1 1 1.458 0v4.583ZM3.234 3.234a.73.73 0 0 0-.213.516v4.583a.73.73 0 1 0 1.458 0V5.301l1.047 1.256 2.708 2.709a.73.73 0 1 0 1.032-1.032L6.557 5.526 5.301 4.48h3.032a.73.73 0 1 0 0-1.458H3.75a.73.73 0 0 0-.516.213Z" fill="#000"/></svg>
|
Before Width: | Height: | Size: 481 B After Width: | Height: | Size: 475 B |
|
@ -1 +1 @@
|
|||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.27 11.458a.73.73 0 0 0-.728-.729H3.958a.73.73 0 1 0 0 1.459h3.033l-1.257 1.046-2.708 2.709a.73.73 0 1 0 1.031 1.03l2.709-2.707 1.046-1.257v3.033a.73.73 0 0 0 1.459 0v-4.584Zm1.673-2.401a.73.73 0 0 1-.214-.515V3.958a.73.73 0 1 1 1.459 0v3.033l1.046-1.257 2.709-2.708a.73.73 0 1 1 1.03 1.031l-2.707 2.709-1.257 1.046h3.033a.73.73 0 0 1 0 1.459h-4.584a.73.73 0 0 1-.515-.214Z" fill="#000"/></svg>
|
||||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.27 8.542a.73.73 0 0 1-.728.729H3.958a.73.73 0 0 1 0-1.459h3.033L5.734 6.766 3.026 4.057a.73.73 0 1 1 1.031-1.03l2.709 2.707 1.046 1.257V3.958a.73.73 0 0 1 1.459 0v4.584Zm1.673 2.401a.73.73 0 0 0-.214.515v4.584a.73.73 0 1 0 1.459 0v-3.033l1.046 1.257 2.709 2.708a.73.73 0 1 0 1.03-1.031l-2.707-2.709-1.257-1.046h3.033a.73.73 0 0 0 0-1.459h-4.584a.73.73 0 0 0-.515.214Z" fill="#000"/></svg>
|
Before Width: | Height: | Size: 480 B After Width: | Height: | Size: 475 B |
|
@ -43,7 +43,6 @@
|
|||
"prepare-adhoc-build": "node scripts/prepare_adhoc_build.js",
|
||||
"prepare-adhoc-version": "node scripts/prepare_tagged_version.js adhoc",
|
||||
"prepare-staging-build": "node scripts/prepare_staging_build.js",
|
||||
"prepare-windows-cert": "node scripts/prepare_windows_cert.js",
|
||||
"test": "run-s test-node test-electron test-lint-intl test-eslint",
|
||||
"test-electron": "node ts/scripts/test-electron.js",
|
||||
"test-release": "node ts/scripts/test-release.js",
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
// We have different windows certificates used in each of our build machines, and this
|
||||
// script makes it easier to ready the app to build on a given machine.
|
||||
|
||||
// -------
|
||||
|
||||
const KEY = 'build.win.certificateSha1';
|
||||
const DEFAULT_VALUE = '8C9A0B5C852EC703D83EF7BFBCEB54B796073759';
|
||||
|
||||
const BUILDER_A = '507769334DA990A8DDE858314B0CDFC228E7CFA1';
|
||||
const BUILDER_B = 'C689B0988CA1A7DF99E4CE4433AC7EA8B82F8D41';
|
||||
|
||||
let targetValue = DEFAULT_VALUE;
|
||||
|
||||
if (process.env.WINDOWS_BUILDER === 'A') {
|
||||
targetValue = BUILDER_A;
|
||||
}
|
||||
if (process.env.WINDOWS_BUILDER === 'B') {
|
||||
targetValue = BUILDER_B;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
function checkValue(object, objectPath, expected) {
|
||||
const actual = _.get(object, objectPath);
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${objectPath} was ${actual}; expected ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ------
|
||||
|
||||
checkValue(packageJson, KEY, DEFAULT_VALUE);
|
||||
|
||||
// -------
|
||||
|
||||
_.set(packageJson, KEY, targetValue);
|
||||
|
||||
// -------
|
||||
|
||||
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' '));
|
|
@ -3882,14 +3882,17 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
|
||||
&__background {
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&--blur {
|
||||
background-repeat: no-repeat;
|
||||
|
@ -3989,9 +3992,10 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
.module-ongoing-call {
|
||||
&__remote-video-enabled {
|
||||
background-color: variables.$color-gray-95;
|
||||
// TODO: DESKTOP-8537 remove this; we want blurred avatar not all-black letterboxing
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
&--reconnecting {
|
||||
filter: blur(15px);
|
||||
}
|
||||
|
@ -4002,6 +4006,7 @@ button.module-image__border-overlay:focus {
|
|||
height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -4104,6 +4109,7 @@ button.module-image__border-overlay:focus {
|
|||
// Only apply container-type: size to grid column to prevent size collapse
|
||||
// for implicitly sized participants (PiP)
|
||||
container-type: size;
|
||||
position: relative;
|
||||
|
||||
@container (min-width: 180px) or (min-height: 180px) {
|
||||
.module-ongoing-call__group-call-remote-participant__footer {
|
||||
|
@ -4625,14 +4631,17 @@ button.module-image__border-overlay:focus {
|
|||
.module-calling-pip {
|
||||
backface-visibility: hidden;
|
||||
background-color: variables.$color-gray-95;
|
||||
border-radius: 4px;
|
||||
border-radius: 18px;
|
||||
box-shadow:
|
||||
0px 0px 8px rgba(0, 0, 0, 0.05),
|
||||
0px 8px 20px rgba(0, 0, 0, 0.3);
|
||||
cursor: grab;
|
||||
height: 158px;
|
||||
// This is just a starting height; the component will figure out what height it should
|
||||
// be, given the aspect ratio of the provided video, pinning the width.
|
||||
// These both should be kept in sync with the height/width in CallingPip.tsx
|
||||
height: 286px;
|
||||
width: 160px;
|
||||
position: fixed;
|
||||
width: 120px;
|
||||
z-index: variables.$z-index-calling-pip;
|
||||
|
||||
& .module-ongoing-call__group-call-remote-participant {
|
||||
|
@ -4643,13 +4652,13 @@ button.module-image__border-overlay:focus {
|
|||
&--remote {
|
||||
align-items: center;
|
||||
background-color: variables.$color-gray-95;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border-radius: 18px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 120px; // This height should be kept in sync with <CallingPipRemoteVideo>'s hard-coded height.
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// The avatar image can be dragged on Windows.
|
||||
.module-Avatar img {
|
||||
|
@ -4664,15 +4673,20 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
&--local,
|
||||
&--local-presenting {
|
||||
bottom: 38px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
inset-inline-end: 4px;
|
||||
width: 32px;
|
||||
top: 8px;
|
||||
inset-inline-start: 8px;
|
||||
height: 54px;
|
||||
width: 80px;
|
||||
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background-color: variables.$color-gray-80;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4681,17 +4695,116 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
align-items: center;
|
||||
background-color: variables.$color-gray-02;
|
||||
border-radius: 0 0 4px 4px;
|
||||
&__full-size-local-preview {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
&--presenting {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__pills {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 38px;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
@include mixins.dark-theme {
|
||||
background-color: variables.$color-gray-65;
|
||||
position: absolute;
|
||||
bottom: 66px;
|
||||
inset-inline-start: 8px;
|
||||
transition: bottom 0.3s variables.$ease-out-local-preview 0.3s;
|
||||
|
||||
&--no-controls {
|
||||
bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__pill {
|
||||
height: 28px;
|
||||
border-radius: 14px;
|
||||
padding: 6px;
|
||||
padding-inline-start: 12px;
|
||||
padding-inline-end: 12px;
|
||||
background-color: variables.$color-gray-80;
|
||||
color: variables.$color-gray-05;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@include mixins.font-body-small;
|
||||
}
|
||||
|
||||
&__pill-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 4px;
|
||||
|
||||
&__raised-hands {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/raise_hand/raise_hand.svg',
|
||||
variables.$color-gray-05
|
||||
);
|
||||
}
|
||||
&__group-join {
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/person/person-plus-compact.svg',
|
||||
variables.$color-gray-05
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
inset-inline-start: 4px;
|
||||
inset-inline-end: 4px;
|
||||
padding: 12px;
|
||||
height: 56px;
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
border-radius: 18px;
|
||||
justify-content: space-around;
|
||||
background-color: variables.$color-gray-78;
|
||||
|
||||
&__button {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
&__middle-button {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.CallingButton__icon {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&__un-pip-container {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
inset-inline-end: 16px;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -101,6 +101,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
&--full-screen-call {
|
||||
@include calling-button-icon(
|
||||
'../images/icons/v3/pip/pip-maximize-light.svg',
|
||||
variables.$color-gray-80,
|
||||
variables.$color-gray-15
|
||||
);
|
||||
}
|
||||
|
||||
&--hangup {
|
||||
@include calling-button-icon(
|
||||
'../images/icons/v3/phone/phone-down-fill-light.svg',
|
||||
|
@ -264,3 +272,9 @@
|
|||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-calling-pip {
|
||||
.CallingButton__button-container {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,8 +122,8 @@ export type PropsType = {
|
|||
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
||||
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
||||
setIsCallActive: (_: boolean) => void;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalAudio: SetLocalAudioType;
|
||||
setLocalVideo: SetLocalVideoType;
|
||||
setLocalPreviewContainer: (container: HTMLDivElement | null) => void;
|
||||
setOutgoingRing: (_: boolean) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
|
@ -345,14 +345,19 @@ function ActiveCallManager({
|
|||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
||||
imageDataCache={imageDataCache}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
me={me}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
|
||||
setLocalPreviewContainer={setLocalPreviewContainer}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
switchToPresentationView={switchToPresentationView}
|
||||
switchFromPresentationView={switchFromPresentationView}
|
||||
toggleAudio={setLocalAudio}
|
||||
togglePip={togglePip}
|
||||
toggleVideo={() => {
|
||||
const enabled = !activeCall.hasLocalVideo;
|
||||
setLocalVideo({ enabled });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -122,8 +122,8 @@ export type PropsType = {
|
|||
_: Array<GroupCallVideoRequest>,
|
||||
speakerHeight: number
|
||||
) => void;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalAudio: SetLocalAudioType;
|
||||
setLocalVideo: SetLocalVideoType;
|
||||
setLocalPreviewContainer: (container: HTMLDivElement | null) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
stickyControls: boolean;
|
||||
|
@ -489,10 +489,7 @@ export function CallScreen({
|
|||
)}
|
||||
>
|
||||
{isSendingVideo ? (
|
||||
<div
|
||||
className="module-ongoing-call__local-preview-container"
|
||||
ref={setLocalPreviewContainer}
|
||||
/>
|
||||
<div ref={setLocalPreviewContainer} />
|
||||
) : (
|
||||
<CallBackgroundBlur avatarUrl={me.avatarUrl}>
|
||||
<div className="module-calling__spacer module-calling__camera-is-off-spacer" />
|
||||
|
|
|
@ -13,6 +13,9 @@ export enum CallingButtonType {
|
|||
AUDIO_DISABLED = 'AUDIO_DISABLED',
|
||||
AUDIO_OFF = 'AUDIO_OFF',
|
||||
AUDIO_ON = 'AUDIO_ON',
|
||||
FULL_SCREEN_CALL = 'FULL_SCREEN_CALL',
|
||||
HANGUP_GROUP = 'HANGUP_GROUP',
|
||||
HANGUP_DIRECT = 'HANGUP_DIRECT',
|
||||
MAXIMIZE = 'MAXIMIZE',
|
||||
MINIMIZE = 'MINIMIZE',
|
||||
MORE_OPTIONS = 'MORE_OPTIONS',
|
||||
|
@ -117,6 +120,19 @@ export function CallingButton({
|
|||
} else if (buttonType === CallingButtonType.MINIMIZE) {
|
||||
classNameSuffix = 'minimize';
|
||||
tooltipContent = i18n('icu:calling__preview--minimize');
|
||||
} else if (buttonType === CallingButtonType.FULL_SCREEN_CALL) {
|
||||
classNameSuffix = 'full-screen-call';
|
||||
tooltipContent = i18n('icu:calling__pip--off');
|
||||
} else if (buttonType === CallingButtonType.HANGUP_DIRECT) {
|
||||
classNameSuffix = 'hangup';
|
||||
tooltipContent = i18n(
|
||||
'icu:CallControls__JoinLeaveButton--hangup-1-1-tooltip'
|
||||
);
|
||||
} else if (buttonType === CallingButtonType.HANGUP_GROUP) {
|
||||
classNameSuffix = 'hangup';
|
||||
tooltipContent = i18n(
|
||||
'icu:CallControls__JoinLeaveButton--hangup-group-tooltip'
|
||||
);
|
||||
}
|
||||
|
||||
const handleClick = React.useCallback(
|
||||
|
|
|
@ -72,8 +72,8 @@ export type PropsType = {
|
|||
onJoinCall: () => void;
|
||||
outgoingRing: boolean;
|
||||
peekedParticipants: Array<ConversationType>;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalAudio: SetLocalAudioType;
|
||||
setLocalVideo: SetLocalVideoType;
|
||||
setLocalPreviewContainer: (container: HTMLDivElement | null) => void;
|
||||
setOutgoingRing: (_: boolean) => void;
|
||||
showParticipantsList: boolean;
|
||||
|
|
|
@ -2,43 +2,17 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { sample } from 'lodash';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './CallingParticipantsList';
|
||||
import { CallingParticipantsList } from './CallingParticipantsList';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import type { GroupCallRemoteParticipantType } from '../types/Calling';
|
||||
import { generateAci } from '../types/ServiceId';
|
||||
import { getDefaultConversationWithServiceId } from '../test-both/helpers/getDefaultConversation';
|
||||
import { createCallParticipant } from '../test-both/helpers/createCallParticipant';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
function createParticipant(
|
||||
participantProps: Partial<GroupCallRemoteParticipantType>
|
||||
): GroupCallRemoteParticipantType {
|
||||
return {
|
||||
aci: generateAci(),
|
||||
demuxId: 2,
|
||||
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||
isHandRaised: Boolean(participantProps.isHandRaised),
|
||||
mediaKeysReceived: Boolean(participantProps.mediaKeysReceived),
|
||||
presenting: Boolean(participantProps.presenting),
|
||||
sharingScreen: Boolean(participantProps.sharingScreen),
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversationWithServiceId({
|
||||
avatarUrl: participantProps.avatarUrl,
|
||||
color: sample(AvatarColors),
|
||||
isBlocked: Boolean(participantProps.isBlocked),
|
||||
name: participantProps.name,
|
||||
profileName: participantProps.title,
|
||||
title: String(participantProps.title),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n,
|
||||
conversationId: 'fake-conversation-id',
|
||||
|
@ -60,7 +34,7 @@ export function NoOne(): JSX.Element {
|
|||
export function SoloCall(): JSX.Element {
|
||||
const props = createProps({
|
||||
participants: [
|
||||
createParticipant({
|
||||
createCallParticipant({
|
||||
title: 'Bardock',
|
||||
}),
|
||||
],
|
||||
|
@ -71,37 +45,37 @@ export function SoloCall(): JSX.Element {
|
|||
export function ManyParticipants(): JSX.Element {
|
||||
const props = createProps({
|
||||
participants: [
|
||||
createParticipant({
|
||||
createCallParticipant({
|
||||
title: 'Son Goku',
|
||||
}),
|
||||
createParticipant({
|
||||
createCallParticipant({
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
presenting: true,
|
||||
name: 'Rage Trunks',
|
||||
title: 'Rage Trunks',
|
||||
}),
|
||||
createParticipant({
|
||||
createCallParticipant({
|
||||
hasRemoteAudio: true,
|
||||
title: 'Prince Vegeta',
|
||||
}),
|
||||
createParticipant({
|
||||
createCallParticipant({
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
name: 'Goku Black',
|
||||
title: 'Goku Black',
|
||||
}),
|
||||
createParticipant({
|
||||
createCallParticipant({
|
||||
isHandRaised: true,
|
||||
title: 'Supreme Kai Zamasu',
|
||||
}),
|
||||
createParticipant({
|
||||
createCallParticipant({
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isHandRaised: true,
|
||||
title: 'Chi Chi',
|
||||
}),
|
||||
createParticipant({
|
||||
createCallParticipant({
|
||||
title: 'Someone With A Really Long Name',
|
||||
}),
|
||||
],
|
||||
|
@ -113,7 +87,7 @@ export function Overflow(): JSX.Element {
|
|||
const props = createProps({
|
||||
participants: Array(50)
|
||||
.fill(null)
|
||||
.map(() => createParticipant({ title: 'Kirby' })),
|
||||
.map(() => createCallParticipant({ title: 'Kirby' })),
|
||||
});
|
||||
return <CallingParticipantsList {...props} />;
|
||||
}
|
||||
|
|
|
@ -20,9 +20,18 @@ import { CallMode } from '../types/CallDisposition';
|
|||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
|
||||
import { MINUTE } from '../util/durations';
|
||||
import type { SetRendererCanvasType } from '../state/ducks/calling';
|
||||
import { createCallParticipant } from '../test-both/helpers/createCallParticipant';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const videoScreenshot = new Image(300, 400);
|
||||
videoScreenshot.src = '../../fixtures/cat-screenshot-3x4.png';
|
||||
const localPreviewVideo = document.createElement('video');
|
||||
localPreviewVideo.autoplay = true;
|
||||
localPreviewVideo.loop = true;
|
||||
localPreviewVideo.src = '../../fixtures/pixabay-Soap-Bubble-7141.mp4';
|
||||
|
||||
const conversation: ConversationType = getDefaultConversation({
|
||||
id: '3051234567',
|
||||
avatarUrl: undefined,
|
||||
|
@ -43,7 +52,7 @@ type Overrides = {
|
|||
const getCommonActiveCallData = (overrides: Overrides) => ({
|
||||
conversation,
|
||||
hasLocalAudio: overrides.hasLocalAudio ?? true,
|
||||
hasLocalVideo: overrides.hasLocalVideo ?? false,
|
||||
hasLocalVideo: overrides.hasLocalVideo ?? true,
|
||||
localAudioLevel: overrides.localAudioLevel ?? 0,
|
||||
viewMode: overrides.viewMode ?? CallViewMode.Paginated,
|
||||
joinedAt: Date.now() - MINUTE,
|
||||
|
@ -71,21 +80,27 @@ const getDefaultCall = (overrides: Overrides): ActiveDirectCallType => {
|
|||
|
||||
export default {
|
||||
title: 'Components/CallingPip',
|
||||
argTypes: {
|
||||
hasLocalVideo: { control: { type: 'boolean' } },
|
||||
},
|
||||
args: {
|
||||
activeCall: getDefaultCall({}),
|
||||
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
|
||||
hangUpActiveCall: action('hang-up-active-call'),
|
||||
hasLocalVideo: false,
|
||||
i18n,
|
||||
me: getDefaultConversation({
|
||||
name: 'Lonely InGroup',
|
||||
title: 'Lonely InGroup',
|
||||
}),
|
||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||
setLocalPreviewContainer: action('set-local-preview-container'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
setLocalPreviewContainer: (container: HTMLDivElement | null) => {
|
||||
container?.appendChild(localPreviewVideo);
|
||||
},
|
||||
setRendererCanvas: ({ element }: SetRendererCanvasType) => {
|
||||
element?.current?.getContext('2d')?.drawImage(videoScreenshot, 0, 0);
|
||||
},
|
||||
switchFromPresentationView: action('switch-to-presentation-view'),
|
||||
switchToPresentationView: action('switch-to-presentation-view'),
|
||||
toggleAudio: action('toggle-audio'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleVideo: action('toggle-video'),
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
|
@ -93,6 +108,60 @@ export function Default(args: PropsType): JSX.Element {
|
|||
return <CallingPip {...args} />;
|
||||
}
|
||||
|
||||
// Note: should NOT show speaking indicators
|
||||
export function DefaultBothSpeaking(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getDefaultCall({}),
|
||||
remoteAudioLevel: 0.75,
|
||||
localAudioLevel: 0.75,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Note: should NOT show mute indicator for remote party
|
||||
export function RemoteMuted(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getDefaultCall({}),
|
||||
hasRemoteAudio: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Note: should NOT show show mute indicator in self preview
|
||||
export function NoLocalAudio(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getDefaultCall({
|
||||
hasLocalAudio: false,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoLocalVideo(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getDefaultCall({
|
||||
hasLocalVideo: false,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContactWithAvatarAndNoVideo(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
|
@ -126,7 +195,7 @@ export function ContactNoColor(args: PropsType): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function GroupCall(args: PropsType): JSX.Element {
|
||||
export function LonelyInGroupCall(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
|
@ -151,3 +220,229 @@ export function GroupCall(args: PropsType): JSX.Element {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function LonelyInGroupCallVideoDisabled(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getCommonActiveCallData({
|
||||
hasLocalVideo: false,
|
||||
}),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
isConversationTooBigToRing: false,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
suggestLowerHand: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCall(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getCommonActiveCallData({}),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
isConversationTooBigToRing: false,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [
|
||||
createCallParticipant({}),
|
||||
createCallParticipant({}),
|
||||
],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
suggestLowerHand: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallWithRaisedHands(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getCommonActiveCallData({}),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
isConversationTooBigToRing: false,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [],
|
||||
raisedHands: new Set<number>([1, 2, 3]),
|
||||
remoteParticipants: [
|
||||
createCallParticipant({}),
|
||||
createCallParticipant({}),
|
||||
],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
suggestLowerHand: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallWithPendingParticipants(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getCommonActiveCallData({}),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
isConversationTooBigToRing: false,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [
|
||||
createCallParticipant({}),
|
||||
createCallParticipant({}),
|
||||
],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
suggestLowerHand: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallWithPendingAndRaised(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getCommonActiveCallData({}),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
isConversationTooBigToRing: false,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
raisedHands: new Set<number>([1, 2, 3]),
|
||||
remoteParticipants: [
|
||||
createCallParticipant({}),
|
||||
createCallParticipant({}),
|
||||
],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
suggestLowerHand: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Note: should NOT show muted indicator for remote party
|
||||
export function GroupCallRemoteMuted(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getCommonActiveCallData({}),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
isConversationTooBigToRing: false,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
raisedHands: new Set<number>([1, 2, 3]),
|
||||
remoteParticipants: [
|
||||
{
|
||||
...createCallParticipant({}),
|
||||
demuxId: 1,
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
mediaKeysReceived: true,
|
||||
},
|
||||
],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
suggestLowerHand: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Note: should NOT show speaking indicator
|
||||
export function GroupCallRemoteSpeaking(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingPip
|
||||
{...args}
|
||||
activeCall={{
|
||||
...getCommonActiveCallData({}),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(),
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
isConversationTooBigToRing: false,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
localDemuxId: 1,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
pendingParticipants: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
],
|
||||
raisedHands: new Set<number>([1, 2, 3]),
|
||||
remoteParticipants: [
|
||||
{
|
||||
...createCallParticipant({}),
|
||||
demuxId: 1,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
mediaKeysReceived: true,
|
||||
},
|
||||
],
|
||||
remoteAudioLevels: new Map<number, number>([[1, 0.75]]),
|
||||
suggestLowerHand: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,15 +2,27 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { minBy, debounce, noop } from 'lodash';
|
||||
|
||||
import type { VideoFrameSource } from '@signalapp/ringrtc';
|
||||
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||
import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import { TooltipPlacement } from './Tooltip';
|
||||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { CallingPipRemoteVideo } from './CallingPipRemoteVideo';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { ActiveCallType, GroupCallVideoRequest } from '../types/Calling';
|
||||
import type { SetRendererCanvasType } from '../state/ducks/calling';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting';
|
||||
import type { CallingImageDataCache } from './CallManager';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
|
||||
enum PositionMode {
|
||||
BeingDragged,
|
||||
|
@ -50,9 +62,21 @@ export type PropsType = {
|
|||
activeCall: ActiveCallType;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
hangUpActiveCall: (reason: string) => void;
|
||||
hasLocalVideo: boolean;
|
||||
i18n: LocalizerType;
|
||||
imageDataCache: React.RefObject<CallingImageDataCache>;
|
||||
me: Readonly<
|
||||
Pick<
|
||||
ConversationType,
|
||||
| 'avatarUrl'
|
||||
| 'avatarPlaceholderGradient'
|
||||
| 'color'
|
||||
| 'type'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'title'
|
||||
| 'sharedGroupNames'
|
||||
>
|
||||
>;
|
||||
setGroupCallVideoRequest: (
|
||||
_: Array<GroupCallVideoRequest>,
|
||||
speakerHeight: number
|
||||
|
@ -61,32 +85,42 @@ export type PropsType = {
|
|||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
switchToPresentationView: () => void;
|
||||
switchFromPresentationView: () => void;
|
||||
toggleAudio: () => void;
|
||||
togglePip: () => void;
|
||||
toggleVideo: () => void;
|
||||
};
|
||||
|
||||
const PIP_HEIGHT = 156;
|
||||
const PIP_WIDTH = 120;
|
||||
const PIP_TOP_MARGIN = 56;
|
||||
const PIP_STARTING_HEIGHT = 286;
|
||||
const PIP_WIDTH = 160;
|
||||
const PIP_TOP_MARGIN = 78;
|
||||
const PIP_PADDING = 8;
|
||||
|
||||
// Receiving portrait video will cause the PIP to update to match that video size, but
|
||||
// we need limits
|
||||
export const PIP_MINIMUM_HEIGHT = 180;
|
||||
export const PIP_MAXIMUM_HEIGHT = 360;
|
||||
|
||||
export function CallingPip({
|
||||
activeCall,
|
||||
getGroupCallVideoFrameSource,
|
||||
hangUpActiveCall,
|
||||
hasLocalVideo,
|
||||
imageDataCache,
|
||||
i18n,
|
||||
me,
|
||||
setGroupCallVideoRequest,
|
||||
setLocalPreviewContainer,
|
||||
setRendererCanvas,
|
||||
switchToPresentationView,
|
||||
switchFromPresentationView,
|
||||
toggleAudio,
|
||||
togglePip,
|
||||
toggleVideo,
|
||||
}: PropsType): JSX.Element {
|
||||
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
||||
|
||||
const videoContainerRef = React.useRef<null | HTMLDivElement>(null);
|
||||
|
||||
const [height, setHeight] = React.useState(PIP_STARTING_HEIGHT);
|
||||
const [windowWidth, setWindowWidth] = React.useState(window.innerWidth);
|
||||
const [windowHeight, setWindowHeight] = React.useState(window.innerHeight);
|
||||
const [positionState, setPositionState] = React.useState<PositionState>({
|
||||
|
@ -112,6 +146,8 @@ export function CallingPip({
|
|||
mouseX: ev.clientX,
|
||||
mouseY: ev.clientY,
|
||||
}));
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
},
|
||||
[positionState]
|
||||
|
@ -150,7 +186,7 @@ export function CallingPip({
|
|||
},
|
||||
{
|
||||
mode: PositionMode.SnapToBottom,
|
||||
distanceToEdge: innerHeight - (offsetY + PIP_HEIGHT),
|
||||
distanceToEdge: innerHeight - (offsetY + height),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -179,7 +215,7 @@ export function CallingPip({
|
|||
throw missingCaseError(snapTo.mode);
|
||||
}
|
||||
}
|
||||
}, [isRTL, positionState, setPositionState]);
|
||||
}, [height, isRTL, positionState, setPositionState]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (positionState.mode === PositionMode.BeingDragged) {
|
||||
|
@ -213,6 +249,15 @@ export function CallingPip({
|
|||
}, []);
|
||||
|
||||
const [translateX, translateY] = React.useMemo<[number, number]>(() => {
|
||||
const topMin = PIP_TOP_MARGIN;
|
||||
const bottomMax = windowHeight - PIP_PADDING - height;
|
||||
|
||||
const leftScrollPadding = isRTL ? 1 : 0;
|
||||
const leftMin = PIP_PADDING + leftScrollPadding;
|
||||
|
||||
const rightScrollPadding = isRTL ? 0 : 1;
|
||||
const rightMax = windowWidth - PIP_PADDING - PIP_WIDTH - rightScrollPadding;
|
||||
|
||||
switch (positionState.mode) {
|
||||
case PositionMode.BeingDragged:
|
||||
return [
|
||||
|
@ -225,56 +270,170 @@ export function CallingPip({
|
|||
];
|
||||
case PositionMode.SnapToLeft:
|
||||
return [
|
||||
PIP_PADDING,
|
||||
Math.min(
|
||||
positionState.offsetY,
|
||||
windowHeight - PIP_PADDING - PIP_HEIGHT
|
||||
),
|
||||
leftMin,
|
||||
Math.max(topMin, Math.min(positionState.offsetY, bottomMax)),
|
||||
];
|
||||
case PositionMode.SnapToRight:
|
||||
return [
|
||||
windowWidth - PIP_PADDING - PIP_WIDTH,
|
||||
Math.min(
|
||||
positionState.offsetY,
|
||||
windowHeight - PIP_PADDING - PIP_HEIGHT
|
||||
),
|
||||
rightMax,
|
||||
Math.max(topMin, Math.min(positionState.offsetY, bottomMax)),
|
||||
];
|
||||
case PositionMode.SnapToTop:
|
||||
return [
|
||||
Math.min(
|
||||
positionState.offsetX,
|
||||
windowWidth - PIP_PADDING - PIP_WIDTH
|
||||
),
|
||||
PIP_TOP_MARGIN + PIP_PADDING,
|
||||
Math.max(leftMin, Math.min(positionState.offsetX, rightMax)),
|
||||
topMin,
|
||||
];
|
||||
case PositionMode.SnapToBottom:
|
||||
return [
|
||||
Math.min(
|
||||
positionState.offsetX,
|
||||
windowWidth - PIP_PADDING - PIP_WIDTH
|
||||
),
|
||||
windowHeight - PIP_PADDING - PIP_HEIGHT,
|
||||
Math.max(leftMin, Math.min(positionState.offsetX, rightMax)),
|
||||
bottomMax,
|
||||
];
|
||||
default:
|
||||
throw missingCaseError(positionState);
|
||||
}
|
||||
}, [isRTL, windowWidth, windowHeight, positionState]);
|
||||
}, [height, isRTL, windowWidth, windowHeight, positionState]);
|
||||
const localizedTranslateX = isRTL ? -translateX : translateX;
|
||||
|
||||
const [showControls, setShowControls] = React.useState(false);
|
||||
const onMouseEnter = React.useCallback(() => {
|
||||
setShowControls(true);
|
||||
}, [setShowControls]);
|
||||
const onMouseMove = React.useCallback(() => {
|
||||
setShowControls(true);
|
||||
}, [setShowControls]);
|
||||
|
||||
const [controlsHover, setControlsHover] = React.useState(false);
|
||||
const onControlsMouseEnter = React.useCallback(() => {
|
||||
setControlsHover(true);
|
||||
}, [setControlsHover]);
|
||||
|
||||
const onControlsMouseLeave = React.useCallback(() => {
|
||||
setControlsHover(false);
|
||||
}, [setControlsHover]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!showControls) {
|
||||
return;
|
||||
}
|
||||
if (controlsHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
}, 2000);
|
||||
return clearTimeout.bind(null, timer);
|
||||
}, [showControls, controlsHover, setShowControls]);
|
||||
|
||||
const localVideoClassName = activeCall.presentingSource
|
||||
? 'module-calling-pip__video--local-presenting'
|
||||
: 'module-calling-pip__video--local';
|
||||
|
||||
let raisedHandsCount = 0;
|
||||
let callJoinRequests = 0;
|
||||
if (isGroupOrAdhocActiveCall(activeCall)) {
|
||||
raisedHandsCount = activeCall.raisedHands.size;
|
||||
callJoinRequests = activeCall.pendingParticipants.length;
|
||||
}
|
||||
|
||||
let videoButtonType: CallingButtonType;
|
||||
if (activeCall.presentingSource) {
|
||||
videoButtonType = CallingButtonType.VIDEO_DISABLED;
|
||||
} else if (activeCall.hasLocalVideo) {
|
||||
videoButtonType = CallingButtonType.VIDEO_ON;
|
||||
} else {
|
||||
videoButtonType = CallingButtonType.VIDEO_OFF;
|
||||
}
|
||||
const audioButtonType = activeCall.hasLocalAudio
|
||||
? CallingButtonType.AUDIO_ON
|
||||
: CallingButtonType.AUDIO_OFF;
|
||||
const hangupButtonType =
|
||||
activeCall.callMode === CallMode.Direct
|
||||
? CallingButtonType.HANGUP_DIRECT
|
||||
: CallingButtonType.HANGUP_GROUP;
|
||||
|
||||
let remoteVideoNode: JSX.Element;
|
||||
const isLonelyInCall = !activeCall.remoteParticipants.length;
|
||||
const isSendingVideo =
|
||||
activeCall.hasLocalVideo || activeCall.presentingSource;
|
||||
if (isLonelyInCall) {
|
||||
remoteVideoNode = (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
{isSendingVideo ? (
|
||||
// TODO: DESKTOP-8537 - when black bars go away, need to make some CSS changes
|
||||
<>
|
||||
<CallBackgroundBlur avatarUrl={me.avatarUrl} />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling-pip__full-size-local-preview',
|
||||
activeCall.presentingSource
|
||||
? 'module-calling-pip__full-size-local-preview--presenting'
|
||||
: undefined
|
||||
)}
|
||||
ref={setLocalPreviewContainer}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<CallBackgroundBlur avatarUrl={me.avatarUrl}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
avatarPlaceholderGradient={me.avatarPlaceholderGradient}
|
||||
avatarUrl={me.avatarUrl}
|
||||
badge={undefined}
|
||||
color={me.color || AvatarColors[0]}
|
||||
noteToSelf={false}
|
||||
conversationType={me.type}
|
||||
i18n={i18n}
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
title={me.title}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
sharedGroupNames={[]}
|
||||
/>
|
||||
</div>
|
||||
</CallBackgroundBlur>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
remoteVideoNode = (
|
||||
<CallingPipRemoteVideo
|
||||
activeCall={activeCall}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
imageDataCache={imageDataCache}
|
||||
i18n={i18n}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
height={height}
|
||||
width={PIP_WIDTH}
|
||||
updateHeight={(newHeight: number) => {
|
||||
setHeight(newHeight);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className="module-calling-pip"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={ev => {
|
||||
const node = videoContainerRef.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNode = ev.target as Element;
|
||||
if (targetNode?.tagName === 'BUTTON') {
|
||||
return;
|
||||
}
|
||||
const parentNode = targetNode.parentNode as Element;
|
||||
if (parentNode?.tagName === 'BUTTON') {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = node.getBoundingClientRect();
|
||||
const dragOffsetX = ev.clientX - rect.left;
|
||||
const dragOffsetY = ev.clientY - rect.top;
|
||||
|
@ -289,6 +448,7 @@ export function CallingPip({
|
|||
}}
|
||||
ref={videoContainerRef}
|
||||
style={{
|
||||
height: `${height}px`,
|
||||
cursor:
|
||||
positionState.mode === PositionMode.BeingDragged
|
||||
? '-webkit-grabbing'
|
||||
|
@ -300,32 +460,96 @@ export function CallingPip({
|
|||
: 'transform ease-out 300ms',
|
||||
}}
|
||||
>
|
||||
<CallingPipRemoteVideo
|
||||
activeCall={activeCall}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
imageDataCache={imageDataCache}
|
||||
i18n={i18n}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
/>
|
||||
{hasLocalVideo ? (
|
||||
{remoteVideoNode}
|
||||
|
||||
{!isLonelyInCall && activeCall.hasLocalVideo ? (
|
||||
<div className={localVideoClassName} ref={setLocalPreviewContainer} />
|
||||
) : null}
|
||||
<div className="module-calling-pip__actions">
|
||||
<button
|
||||
aria-label={i18n('icu:calling__hangup')}
|
||||
className="module-calling-pip__button--hangup"
|
||||
onClick={hangUp}
|
||||
type="button"
|
||||
/>
|
||||
<button
|
||||
aria-label={i18n('icu:calling__pip--off')}
|
||||
className="module-calling-pip__button--pip"
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling-pip__un-pip-container',
|
||||
showControls
|
||||
? 'module-calling-pip__un-pip-container--visible'
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<CallingButton
|
||||
buttonType={CallingButtonType.FULL_SCREEN_CALL}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={togglePip}
|
||||
type="button"
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
{raisedHandsCount || callJoinRequests ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling-pip__pills',
|
||||
!showControls ? 'module-calling-pip__pills--no-controls' : undefined
|
||||
)}
|
||||
>
|
||||
<div />
|
||||
</button>
|
||||
{raisedHandsCount ? (
|
||||
<div className="module-calling-pip__pill">
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling-pip__pill-icon',
|
||||
'module-calling-pip__pill-icon__raised-hands'
|
||||
)}
|
||||
/>
|
||||
{raisedHandsCount}
|
||||
</div>
|
||||
) : undefined}
|
||||
{callJoinRequests ? (
|
||||
<div className="module-calling-pip__pill">
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling-pip__pill-icon',
|
||||
'module-calling-pip__pill-icon__group-join'
|
||||
)}
|
||||
/>
|
||||
{callJoinRequests}
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
) : undefined}
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling-pip__actions',
|
||||
showControls ? 'module-calling-pip__actions--visible' : undefined
|
||||
)}
|
||||
>
|
||||
<div className="module-calling-pip__actions__button">
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleVideo}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-calling-pip__actions__button module-calling-pip__actions__middle-button">
|
||||
<CallingButton
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleAudio}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-calling-pip__actions__button">
|
||||
<CallingButton
|
||||
buttonType={hangupButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={hangUp}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import { clamp, maxBy } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import { clamp, isNumber, maxBy } from 'lodash';
|
||||
import type { VideoFrameSource } from '@signalapp/ringrtc';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type {
|
||||
ActiveCallType,
|
||||
GroupCallRemoteParticipantType,
|
||||
GroupCallVideoRequest,
|
||||
import {
|
||||
GroupCallJoinState,
|
||||
type ActiveCallType,
|
||||
type GroupCallRemoteParticipantType,
|
||||
type GroupCallVideoRequest,
|
||||
} from '../types/Calling';
|
||||
import { GroupCallJoinState } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import type { SetRendererCanvasType } from '../state/ducks/calling';
|
||||
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
|
||||
import { MAX_FRAME_WIDTH } from '../calling/constants';
|
||||
import { MAX_FRAME_HEIGHT } from '../calling/constants';
|
||||
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
||||
|
@ -27,21 +27,19 @@ import { isReconnecting } from '../util/callingIsReconnecting';
|
|||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||
import { assertDev } from '../util/assert';
|
||||
import type { CallingImageDataCache } from './CallManager';
|
||||
import { PIP_MAXIMUM_HEIGHT, PIP_MINIMUM_HEIGHT } from './CallingPip';
|
||||
|
||||
// This value should be kept in sync with the hard-coded CSS height. It should also be
|
||||
// less than `MAX_FRAME_HEIGHT`.
|
||||
const PIP_VIDEO_HEIGHT_PX = 120;
|
||||
|
||||
function NoVideo({
|
||||
function BlurredBackground({
|
||||
activeCall,
|
||||
activeGroupCallSpeaker,
|
||||
i18n,
|
||||
}: {
|
||||
activeCall: ActiveCallType;
|
||||
activeGroupCallSpeaker?: undefined | GroupCallRemoteParticipantType;
|
||||
i18n: LocalizerType;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
avatarPlaceholderGradient,
|
||||
avatarUrl,
|
||||
color,
|
||||
type: conversationType,
|
||||
phoneNumber,
|
||||
|
@ -49,28 +47,28 @@ function NoVideo({
|
|||
sharedGroupNames,
|
||||
title,
|
||||
} = activeCall.conversation;
|
||||
const avatarUrl =
|
||||
activeGroupCallSpeaker?.avatarUrl ?? activeCall.conversation.avatarUrl;
|
||||
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<CallBackgroundBlur avatarUrl={avatarUrl}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={color || AvatarColors[0]}
|
||||
noteToSelf={false}
|
||||
conversationType={conversationType}
|
||||
i18n={i18n}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
/>
|
||||
</div>
|
||||
</CallBackgroundBlur>
|
||||
</div>
|
||||
<CallBackgroundBlur avatarUrl={avatarUrl}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
avatarUrl={avatarUrl}
|
||||
badge={undefined}
|
||||
color={color || AvatarColors[0]}
|
||||
noteToSelf={false}
|
||||
conversationType={conversationType}
|
||||
i18n={i18n}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
/>
|
||||
</div>
|
||||
</CallBackgroundBlur>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -84,6 +82,9 @@ export type PropsType = {
|
|||
speakerHeight: number
|
||||
) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
height: number;
|
||||
width: number;
|
||||
updateHeight: (newHeight: number) => void;
|
||||
};
|
||||
|
||||
export function CallingPipRemoteVideo({
|
||||
|
@ -93,6 +94,9 @@ export function CallingPipRemoteVideo({
|
|||
i18n,
|
||||
setGroupCallVideoRequest,
|
||||
setRendererCanvas,
|
||||
height,
|
||||
width,
|
||||
updateHeight,
|
||||
}: PropsType): JSX.Element {
|
||||
const { conversation } = activeCall;
|
||||
|
||||
|
@ -101,7 +105,7 @@ export function CallingPipRemoteVideo({
|
|||
const isPageVisible = usePageVisibility();
|
||||
|
||||
const activeGroupCallSpeaker: undefined | GroupCallRemoteParticipantType =
|
||||
useMemo(() => {
|
||||
React.useMemo(() => {
|
||||
if (!isGroupOrAdhocActiveCall(activeCall)) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -116,53 +120,83 @@ export function CallingPipRemoteVideo({
|
|||
}, [activeCall]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGroupOrAdhocActiveCall(activeCall)) {
|
||||
return;
|
||||
}
|
||||
if (isGroupOrAdhocActiveCall(activeCall)) {
|
||||
if (!activeGroupCallSpeaker || !activeGroupCallSpeaker.hasRemoteVideo) {
|
||||
return;
|
||||
}
|
||||
const { videoAspectRatio } = activeGroupCallSpeaker;
|
||||
if (!isNumber(videoAspectRatio)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPageVisible) {
|
||||
setGroupCallVideoRequest(
|
||||
activeCall.remoteParticipants.map(participant => {
|
||||
const newHeight = clamp(
|
||||
Math.floor(width * (1 / videoAspectRatio)),
|
||||
1,
|
||||
MAX_FRAME_HEIGHT
|
||||
);
|
||||
// Update only for portrait video that fits, otherwise leave things as they are
|
||||
if (
|
||||
newHeight !== height &&
|
||||
newHeight >= PIP_MINIMUM_HEIGHT &&
|
||||
newHeight <= PIP_MAXIMUM_HEIGHT
|
||||
) {
|
||||
updateHeight(newHeight);
|
||||
}
|
||||
|
||||
if (isPageVisible) {
|
||||
const participants = activeCall.remoteParticipants.map(participant => {
|
||||
if (participant === activeGroupCallSpeaker) {
|
||||
return {
|
||||
demuxId: participant.demuxId,
|
||||
width: clamp(
|
||||
Math.floor(PIP_VIDEO_HEIGHT_PX * participant.videoAspectRatio),
|
||||
1,
|
||||
MAX_FRAME_WIDTH
|
||||
),
|
||||
height: PIP_VIDEO_HEIGHT_PX,
|
||||
width,
|
||||
height: newHeight,
|
||||
};
|
||||
}
|
||||
return nonRenderedRemoteParticipant(participant);
|
||||
}),
|
||||
PIP_VIDEO_HEIGHT_PX
|
||||
);
|
||||
});
|
||||
setGroupCallVideoRequest(participants, newHeight);
|
||||
} else {
|
||||
setGroupCallVideoRequest(
|
||||
activeCall.remoteParticipants.map(nonRenderedRemoteParticipant),
|
||||
0
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setGroupCallVideoRequest(
|
||||
activeCall.remoteParticipants.map(nonRenderedRemoteParticipant),
|
||||
0
|
||||
);
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (!activeCall.hasRemoteVideo) {
|
||||
// eslint-disable-next-line no-useless-return
|
||||
return;
|
||||
}
|
||||
// TODO: DESKTOP-8537 - with direct call video stats, call updateHeight as needed
|
||||
}
|
||||
}, [
|
||||
activeCall,
|
||||
activeGroupCallSpeaker,
|
||||
height,
|
||||
isPageVisible,
|
||||
setGroupCallVideoRequest,
|
||||
updateHeight,
|
||||
width,
|
||||
]);
|
||||
|
||||
switch (activeCall.callMode) {
|
||||
case CallMode.Direct: {
|
||||
const { hasRemoteVideo } = activeCall.remoteParticipants[0];
|
||||
if (!hasRemoteVideo) {
|
||||
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<BlurredBackground activeCall={activeCall} i18n={i18n} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
assertDev(
|
||||
conversation.type === 'direct',
|
||||
'CallingPipRemoteVideo for direct call must be associated with direct conversation'
|
||||
);
|
||||
// TODO: DESKTOP-8537 - when black bars go away, we need to make some CSS changes
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<BlurredBackground activeCall={activeCall} i18n={i18n} />
|
||||
<DirectCallRemoteParticipant
|
||||
conversation={conversation}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
|
@ -176,10 +210,19 @@ export function CallingPipRemoteVideo({
|
|||
case CallMode.Group:
|
||||
case CallMode.Adhoc:
|
||||
if (!activeGroupCallSpeaker) {
|
||||
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<BlurredBackground activeCall={activeCall} i18n={i18n} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<BlurredBackground
|
||||
activeCall={activeCall}
|
||||
activeGroupCallSpeaker={activeGroupCallSpeaker}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<GroupCallRemoteParticipant
|
||||
getFrameBuffer={getGroupCallFrameBuffer}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
|
|
|
@ -356,13 +356,19 @@ export type RemoveClientType = ReadonlyDeep<{
|
|||
demuxId: number;
|
||||
}>;
|
||||
|
||||
export type SetLocalAudioType = ReadonlyDeep<{
|
||||
enabled: boolean;
|
||||
}>;
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||
export type SetLocalAudioType = (
|
||||
payload?: ReadonlyDeep<{
|
||||
enabled: boolean;
|
||||
}>
|
||||
) => void;
|
||||
|
||||
export type SetLocalVideoType = ReadonlyDeep<{
|
||||
enabled: boolean;
|
||||
}>;
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||
export type SetLocalVideoType = (
|
||||
payload: ReadonlyDeep<{
|
||||
enabled: boolean;
|
||||
}>
|
||||
) => void;
|
||||
|
||||
export type SetGroupCallVideoRequestType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
|
@ -901,12 +907,12 @@ type SelectPresentingSourceActionType = ReadonlyDeep<{
|
|||
|
||||
type SetLocalAudioActionType = ReadonlyDeep<{
|
||||
type: 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||
payload: SetLocalAudioType;
|
||||
payload: Parameters<SetLocalAudioType>[0];
|
||||
}>;
|
||||
|
||||
type SetLocalVideoFulfilledActionType = ReadonlyDeep<{
|
||||
type: 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||
payload: SetLocalVideoType;
|
||||
payload: Parameters<SetLocalVideoType>[0];
|
||||
}>;
|
||||
|
||||
type SetPresentingFulfilledActionType = ReadonlyDeep<{
|
||||
|
@ -1903,26 +1909,28 @@ function setRendererCanvas(
|
|||
}
|
||||
|
||||
function setLocalAudio(
|
||||
payload: SetLocalAudioType
|
||||
payload?: Parameters<SetLocalAudioType>[0]
|
||||
): ThunkAction<void, RootStateType, unknown, SetLocalAudioActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const activeCall = getActiveCall(getState().calling);
|
||||
if (!activeCall) {
|
||||
const { activeCallState } = getState().calling;
|
||||
if (!activeCallState || activeCallState.state !== 'Active') {
|
||||
log.warn('Trying to set local audio when no call is active');
|
||||
return;
|
||||
}
|
||||
|
||||
calling.setOutgoingAudio(activeCall.conversationId, payload.enabled);
|
||||
|
||||
const enabled = payload?.enabled ?? !activeCallState.hasLocalAudio;
|
||||
calling.setOutgoingAudio(activeCallState.conversationId, enabled);
|
||||
dispatch({
|
||||
type: SET_LOCAL_AUDIO_FULFILLED,
|
||||
payload,
|
||||
payload: {
|
||||
enabled,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setLocalVideo(
|
||||
payload: SetLocalVideoType
|
||||
payload: Parameters<SetLocalVideoType>[0]
|
||||
): ThunkAction<void, RootStateType, unknown, SetLocalVideoFulfilledActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const activeCall = getActiveCall(getState().calling);
|
||||
|
@ -1931,7 +1939,7 @@ function setLocalVideo(
|
|||
return;
|
||||
}
|
||||
|
||||
let enabled: boolean;
|
||||
let enabled = payload?.enabled;
|
||||
if (await requestCameraPermissions()) {
|
||||
if (
|
||||
isGroupOrAdhocCallState(activeCall) ||
|
||||
|
@ -1939,14 +1947,13 @@ function setLocalVideo(
|
|||
) {
|
||||
await calling.setOutgoingVideo(
|
||||
activeCall.conversationId,
|
||||
payload.enabled
|
||||
Boolean(payload?.enabled)
|
||||
);
|
||||
} else if (payload.enabled) {
|
||||
} else if (payload?.enabled) {
|
||||
await calling.enableLocalCamera(activeCall.callMode);
|
||||
} else {
|
||||
calling.disableLocalVideo();
|
||||
}
|
||||
({ enabled } = payload);
|
||||
} else {
|
||||
enabled = false;
|
||||
}
|
||||
|
@ -1954,8 +1961,7 @@ function setLocalVideo(
|
|||
dispatch({
|
||||
type: SET_LOCAL_VIDEO_FULFILLED,
|
||||
payload: {
|
||||
...payload,
|
||||
enabled,
|
||||
enabled: Boolean(enabled),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -3994,7 +4000,7 @@ export function reducer(
|
|||
...state,
|
||||
activeCallState: {
|
||||
...state.activeCallState,
|
||||
hasLocalAudio: action.payload.enabled,
|
||||
hasLocalAudio: Boolean(action.payload?.enabled),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -4009,7 +4015,7 @@ export function reducer(
|
|||
...state,
|
||||
activeCallState: {
|
||||
...state.activeCallState,
|
||||
hasLocalVideo: action.payload.enabled,
|
||||
hasLocalVideo: Boolean(action.payload?.enabled),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
34
ts/test-both/helpers/createCallParticipant.ts
Normal file
34
ts/test-both/helpers/createCallParticipant.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { sample } from 'lodash';
|
||||
|
||||
import { AvatarColors } from '../../types/Colors';
|
||||
import type { GroupCallRemoteParticipantType } from '../../types/Calling';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
|
||||
import { getDefaultConversationWithServiceId } from './getDefaultConversation';
|
||||
|
||||
export function createCallParticipant(
|
||||
participantProps: Partial<GroupCallRemoteParticipantType>
|
||||
): GroupCallRemoteParticipantType {
|
||||
return {
|
||||
aci: generateAci(),
|
||||
demuxId: 2,
|
||||
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||
isHandRaised: Boolean(participantProps.isHandRaised),
|
||||
mediaKeysReceived: Boolean(participantProps.mediaKeysReceived),
|
||||
presenting: Boolean(participantProps.presenting),
|
||||
sharingScreen: Boolean(participantProps.sharingScreen),
|
||||
videoAspectRatio: 1.3,
|
||||
...getDefaultConversationWithServiceId({
|
||||
avatarUrl: participantProps.avatarUrl,
|
||||
color: sample(AvatarColors),
|
||||
isBlocked: Boolean(participantProps.isBlocked),
|
||||
name: participantProps.name,
|
||||
profileName: participantProps.title,
|
||||
title: String(participantProps.title),
|
||||
}),
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue