diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index aa74808d1b..0f23520050 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -6460,6 +6460,10 @@ button.module-image__border-overlay:focus { justify-content: center; position: relative; width: 100%; + + .module-ongoing-call__group-call-remote-participant--audio-muted::before { + display: none; + } } &--local { diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index af3eb871a8..ef5b7023ea 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -8,6 +8,15 @@ import { getInitials } from '../util/getInitials'; import { LocalizerType } from '../types/Util'; import { ColorType } from '../types/Colors'; +export enum AvatarSize { + TWENTY_EIGHT = 28, + THIRTY_TWO = 32, + FIFTY_TWO = 52, + EIGHTY = 80, + NINETY_SIX = 96, + ONE_HUNDRED_TWELVE = 112, +} + export type Props = { avatarPath?: string; color?: ColorType; @@ -18,7 +27,7 @@ export type Props = { name?: string; phoneNumber?: string; profileName?: string; - size: 28 | 32 | 52 | 80 | 96 | 112; + size: AvatarSize; onClick?: () => unknown; diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx index bc0e6f031e..ecca960c99 100644 --- a/ts/components/CallManager.tsx +++ b/ts/components/CallManager.tsx @@ -13,17 +13,15 @@ import { CallMode, CallState, GroupCallJoinState, - GroupCallRemoteParticipantType, VideoFrameSource, } from '../types/Calling'; import { ConversationType } from '../state/ducks/conversations'; import { AcceptCallType, - ActiveCallStateType, + ActiveCallType, CancelCallType, DeclineCallType, DirectCallStateType, - GroupCallStateType, HangUpType, SetLocalAudioType, SetLocalPreviewType, @@ -35,13 +33,6 @@ import { LocalizerType } from '../types/Util'; import { ColorType } from '../types/Colors'; import { missingCaseError } from '../util/missingCaseError'; -interface ActiveCallType { - activeCallState: ActiveCallStateType; - call: DirectCallStateType | GroupCallStateType; - conversation: ConversationType; - groupCallParticipants: Array; -} - export interface PropsType { activeCall?: ActiveCallType; availableCameras: Array; @@ -205,8 +196,7 @@ const ActiveCallManager: React.FC = ({ if (pip) { return ( = ({ return ( <> -): GroupCallStateType { +function getGroupCallState(): GroupCallStateType { return { callMode: CallMode.Group, conversationId: '3051234567', connectionState: 2, joinState: 2, - remoteParticipants, + remoteParticipants: [], }; } @@ -59,24 +60,35 @@ const createProps = ( overrideProps: { callState?: CallState; callTypeState?: DirectCallStateType | GroupCallStateType; + groupCallParticipants?: Array; hasLocalAudio?: boolean; hasLocalVideo?: boolean; hasRemoteVideo?: boolean; - remoteParticipants?: Array; } = {} ): PropsType => ({ - call: overrideProps.callTypeState || getDirectCallState(overrideProps), - conversation: { - id: '3051234567', - avatarPath: undefined, - color: Colors[0], - title: 'Rick Sanchez', - name: 'Rick Sanchez', - phoneNumber: '3051234567', - profileName: 'Rick Sanchez', - markedUnread: false, - type: 'direct', - lastUpdated: Date.now(), + activeCall: { + activeCallState: { + conversationId: '123', + hasLocalAudio: true, + hasLocalVideo: true, + pip: false, + settingsDialogOpen: false, + showParticipantsList: true, + }, + call: overrideProps.callTypeState || getDirectCallState(overrideProps), + conversation: { + id: '3051234567', + avatarPath: undefined, + color: Colors[0], + title: 'Rick Sanchez', + name: 'Rick Sanchez', + phoneNumber: '3051234567', + profileName: 'Rick Sanchez', + markedUnread: false, + type: 'direct', + lastUpdated: Date.now(), + }, + groupCallParticipants: overrideProps.groupCallParticipants || [], }, // We allow `any` here because this is fake and actually comes from RingRTC, which we // can't import. @@ -164,16 +176,17 @@ story.add('hasRemoteVideo', () => { story.add('Group call - 1', () => ( )); @@ -181,32 +194,33 @@ story.add('Group call - 1', () => ( story.add('Group call - Many', () => ( )); diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 9a9d36dfd8..664e8e4ae4 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -4,10 +4,8 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { noop } from 'lodash'; import classNames from 'classnames'; -import { ConversationType } from '../state/ducks/conversations'; import { - DirectCallStateType, - GroupCallStateType, + ActiveCallType, HangUpType, SetLocalAudioType, SetLocalPreviewType, @@ -31,8 +29,7 @@ import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant'; import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants'; export type PropsType = { - call: DirectCallStateType | GroupCallStateType; - conversation: ConversationType; + activeCall: ActiveCallType; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; hangUp: (_: HangUpType) => void; hasLocalAudio: boolean; @@ -58,8 +55,7 @@ export type PropsType = { }; export const CallScreen: React.FC = ({ - call, - conversation, + activeCall, getGroupCallVideoFrameSource, hangUp, hasLocalAudio, @@ -76,6 +72,8 @@ export const CallScreen: React.FC = ({ togglePip, toggleSettings, }) => { + const { call, conversation, groupCallParticipants } = activeCall; + const toggleAudio = useCallback(() => { setLocalAudio({ enabled: !hasLocalAudio, @@ -170,8 +168,9 @@ export const CallScreen: React.FC = ({ isConnected = call.connectionState === GroupCallConnectionState.Connected; remoteParticipantsElement = ( ); break; diff --git a/ts/components/CallingParticipantsList.stories.tsx b/ts/components/CallingParticipantsList.stories.tsx index 0d57f319dc..6a61b033f3 100644 --- a/ts/components/CallingParticipantsList.stories.tsx +++ b/ts/components/CallingParticipantsList.stories.tsx @@ -20,11 +20,13 @@ function createParticipant( return { avatarPath: participantProps.avatarPath, color: Colors[randomColor], + demuxId: 2, hasRemoteAudio: Boolean(participantProps.hasRemoteAudio), hasRemoteVideo: Boolean(participantProps.hasRemoteVideo), isSelf: Boolean(participantProps.isSelf), profileName: participantProps.title, title: String(participantProps.title), + videoAspectRatio: 1.3, }; } diff --git a/ts/components/CallingPip.stories.tsx b/ts/components/CallingPip.stories.tsx index 10a72ec3ba..a563f97cb1 100644 --- a/ts/components/CallingPip.stories.tsx +++ b/ts/components/CallingPip.stories.tsx @@ -9,6 +9,7 @@ import { action } from '@storybook/addon-actions'; import { ColorType } from '../types/Colors'; import { ConversationTypeType } from '../state/ducks/conversations'; +import { ActiveCallType } from '../state/ducks/calling'; import { CallingPip, PropsType } from './CallingPip'; import { CallMode, @@ -43,9 +44,23 @@ const defaultCall = { hasRemoteVideo: true, }; -const createProps = (overrideProps: Partial = {}): PropsType => ({ - call: overrideProps.call || defaultCall, - conversation: overrideProps.conversation || conversation, +const createProps = ( + overrideProps: Partial = {}, + activeCall: Partial = {} +): PropsType => ({ + activeCall: { + activeCallState: { + conversationId: '123', + hasLocalAudio: true, + hasLocalVideo: true, + pip: false, + settingsDialogOpen: false, + showParticipantsList: true, + }, + call: activeCall.call || defaultCall, + conversation: activeCall.conversation || conversation, + groupCallParticipants: [], + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any getGroupCallVideoFrameSource: noop as any, hangUp: action('hang-up'), @@ -59,39 +74,48 @@ const createProps = (overrideProps: Partial = {}): PropsType => ({ const story = storiesOf('Components/CallingPip', module); story.add('Default', () => { - const props = createProps(); + const props = createProps({}); return ; }); story.add('Contact (with avatar)', () => { - const props = createProps({ - conversation: { - ...conversation, - avatarPath: 'https://www.fillmurray.com/64/64', - }, - }); + const props = createProps( + {}, + { + conversation: { + ...conversation, + avatarPath: 'https://www.fillmurray.com/64/64', + }, + } + ); return ; }); story.add('Contact (no color)', () => { - const props = createProps({ - conversation: { - ...conversation, - color: undefined, - }, - }); + const props = createProps( + {}, + { + conversation: { + ...conversation, + color: undefined, + }, + } + ); return ; }); story.add('Group Call', () => { - const props = createProps({ - call: { - callMode: CallMode.Group as CallMode.Group, - conversationId: '3051234567', - connectionState: GroupCallConnectionState.Connected, - joinState: GroupCallJoinState.Joined, - remoteParticipants: [], - }, - }); + const props = createProps( + {}, + { + call: { + callMode: CallMode.Group as CallMode.Group, + conversationId: '3051234567', + connectionState: GroupCallConnectionState.Connected, + joinState: GroupCallJoinState.Joined, + remoteParticipants: [], + }, + } + ); return ; }); diff --git a/ts/components/CallingPip.tsx b/ts/components/CallingPip.tsx index d3d6e4714b..7c59a49ec1 100644 --- a/ts/components/CallingPip.tsx +++ b/ts/components/CallingPip.tsx @@ -5,19 +5,16 @@ import React from 'react'; import { Tooltip } from './Tooltip'; import { CallingPipRemoteVideo } from './CallingPipRemoteVideo'; import { LocalizerType } from '../types/Util'; -import { ConversationType } from '../state/ducks/conversations'; import { VideoFrameSource } from '../types/Calling'; import { - DirectCallStateType, - GroupCallStateType, + ActiveCallType, HangUpType, SetLocalPreviewType, SetRendererCanvasType, } from '../state/ducks/calling'; export type PropsType = { - call: DirectCallStateType | GroupCallStateType; - conversation: ConversationType; + activeCall: ActiveCallType; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; hangUp: (_: HangUpType) => void; hasLocalVideo: boolean; @@ -33,8 +30,7 @@ const PIP_DEFAULT_Y = 56; const PIP_PADDING = 8; export const CallingPip = ({ - call, - conversation, + activeCall, getGroupCallVideoFrameSource, hangUp, hasLocalVideo, @@ -167,8 +163,7 @@ export const CallingPip = ({ }} > { - hangUp({ conversationId: conversation.id }); + hangUp({ conversationId: activeCall.conversation.id }); }} type="button" /> diff --git a/ts/components/CallingPipRemoteVideo.tsx b/ts/components/CallingPipRemoteVideo.tsx index 4e2082cb54..7baa332609 100644 --- a/ts/components/CallingPipRemoteVideo.tsx +++ b/ts/components/CallingPipRemoteVideo.tsx @@ -7,29 +7,24 @@ import { CallBackgroundBlur } from './CallBackgroundBlur'; import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant'; import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant'; import { LocalizerType } from '../types/Util'; -import { ConversationType } from '../state/ducks/conversations'; import { CallMode, VideoFrameSource } from '../types/Calling'; -import { - DirectCallStateType, - GroupCallStateType, - SetRendererCanvasType, -} from '../state/ducks/calling'; +import { ActiveCallType, SetRendererCanvasType } from '../state/ducks/calling'; export interface PropsType { - call: DirectCallStateType | GroupCallStateType; - conversation: ConversationType; + activeCall: ActiveCallType; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; i18n: LocalizerType; setRendererCanvas: (_: SetRendererCanvasType) => void; } export const CallingPipRemoteVideo = ({ - call, - conversation, + activeCall, getGroupCallVideoFrameSource, i18n, setRendererCanvas, }: PropsType): JSX.Element => { + const { call, conversation } = activeCall; + if (call.callMode === CallMode.Direct) { if (!call.hasRemoteVideo) { const { @@ -76,17 +71,17 @@ export const CallingPipRemoteVideo = ({ } if (call.callMode === CallMode.Group) { - const speaker = call.remoteParticipants[0]; + const { groupCallParticipants } = activeCall; + const speaker = groupCallParticipants[0]; return (
); diff --git a/ts/components/GroupCallRemoteParticipant.tsx b/ts/components/GroupCallRemoteParticipant.tsx index 8f88bdd18b..4b02fa1009 100644 --- a/ts/components/GroupCallRemoteParticipant.tsx +++ b/ts/components/GroupCallRemoteParticipant.tsx @@ -11,17 +11,21 @@ import React, { } from 'react'; import classNames from 'classnames'; import { noop } from 'lodash'; -import { VideoFrameSource } from '../types/Calling'; +import { + GroupCallRemoteParticipantType, + VideoFrameSource, +} from '../types/Calling'; +import { LocalizerType } from '../types/Util'; import { CallBackgroundBlur } from './CallBackgroundBlur'; +import { Avatar, AvatarSize } from './Avatar'; // The max size video frame we'll support (in RGBA) const FRAME_BUFFER_SIZE = 1920 * 1080 * 4; interface BasePropsType { - demuxId: number; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; - hasRemoteAudio: boolean; - hasRemoteVideo: boolean; + i18n: LocalizerType; + remoteParticipant: GroupCallRemoteParticipantType; } interface InPipPropsType { @@ -29,23 +33,28 @@ interface InPipPropsType { } interface NotInPipPropsType { - isInPip?: false; - width: number; height: number; + isInPip?: false; left: number; top: number; + width: number; } type PropsType = BasePropsType & (InPipPropsType | NotInPipPropsType); export const GroupCallRemoteParticipant: React.FC = React.memo( props => { + const { getGroupCallVideoFrameSource } = props; + const { + avatarPath, + color, + profileName, + title, demuxId, - getGroupCallVideoFrameSource, hasRemoteAudio, hasRemoteVideo, - } = props; + } = props.remoteParticipant; const [isWide, setIsWide] = useState(true); @@ -132,13 +141,25 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( canvasStyles = { height: '100%' }; } + let avatarSize: number; + // TypeScript isn't smart enough to know that `isInPip` by itself disambiguates the // types, so we have to use `props.isInPip` instead. // eslint-disable-next-line react/destructuring-assignment if (props.isInPip) { containerStyles = canvasStyles; + avatarSize = AvatarSize.FIFTY_TWO; } else { const { top, left, width, height } = props; + const shorterDimension = Math.min(width, height); + + if (shorterDimension >= 240) { + avatarSize = AvatarSize.ONE_HUNDRED_TWELVE; + } else if (shorterDimension >= 180) { + avatarSize = AvatarSize.EIGHTY; + } else { + avatarSize = AvatarSize.FIFTY_TWO; + } containerStyles = { height, @@ -177,9 +198,17 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( }} /> ) : ( - - {/* TODO: Improve the styling here. See DESKTOP-894. */} - + + )} diff --git a/ts/components/GroupCallRemoteParticipants.tsx b/ts/components/GroupCallRemoteParticipants.tsx index 922ca325f7..9e54d96481 100644 --- a/ts/components/GroupCallRemoteParticipants.tsx +++ b/ts/components/GroupCallRemoteParticipants.tsx @@ -4,9 +4,12 @@ import React, { useState, useMemo } from 'react'; import Measure from 'react-measure'; import { takeWhile, chunk, maxBy, flatten } from 'lodash'; -import { VideoFrameSource } from '../types/Calling'; -import { GroupCallParticipantInfoType } from '../state/ducks/calling'; import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant'; +import { + GroupCallRemoteParticipantType, + VideoFrameSource, +} from '../types/Calling'; +import { LocalizerType } from '../types/Util'; const MIN_RENDERED_HEIGHT = 10; const PARTICIPANT_MARGIN = 10; @@ -17,13 +20,14 @@ interface Dimensions { } interface GridArrangement { - rows: Array>; + rows: Array>; scalar: number; } interface PropsType { getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; - remoteParticipants: ReadonlyArray; + i18n: LocalizerType; + remoteParticipants: ReadonlyArray; } // This component lays out group call remote participants. It uses a custom layout @@ -52,6 +56,7 @@ interface PropsType { // 4. Lay out this arrangement on the screen. export const GroupCallRemoteParticipants: React.FC = ({ getGroupCallVideoFrameSource, + i18n, remoteParticipants, }) => { const [containerDimensions, setContainerDimensions] = useState({ @@ -82,7 +87,7 @@ export const GroupCallRemoteParticipants: React.FC = ({ // // This is primarily memoized for clarity, not performance. We only need the result, // not any of the "intermediate" values. - const visibleParticipants: Array = useMemo(() => { + const visibleParticipants: Array = useMemo(() => { // Imagine that we laid out all of the rows end-to-end. That's the maximum total // width. So if there were 5 rows and the container was 100px wide, then we can't // possibly fit more than 500px of participants. @@ -199,12 +204,11 @@ export const GroupCallRemoteParticipants: React.FC = ({ return ( @@ -235,7 +239,7 @@ export const GroupCallRemoteParticipants: React.FC = ({ }; function totalRemoteParticipantWidthAtMinHeight( - remoteParticipants: ReadonlyArray + remoteParticipants: ReadonlyArray ): number { return remoteParticipants.reduce( (result, { videoAspectRatio }) => diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index 9cfe8acf66..32e126b943 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -9,6 +9,7 @@ import { missingCaseError } from '../../util/missingCaseError'; import { notify } from '../../services/notify'; import { calling } from '../../services/calling'; import { StateType as RootStateType } from '../reducer'; +import { ConversationType } from './conversations'; import { CallMode, CallState, @@ -16,6 +17,7 @@ import { ChangeIODevicePayloadType, GroupCallConnectionState, GroupCallJoinState, + GroupCallRemoteParticipantType, MediaDeviceSettings, } from '../../types/Calling'; import { callingTones } from '../../util/callingTones'; @@ -54,6 +56,13 @@ export interface GroupCallStateType { remoteParticipants: Array; } +export interface ActiveCallType { + activeCallState: ActiveCallStateType; + call: DirectCallStateType | GroupCallStateType; + conversation: ConversationType; + groupCallParticipants: Array; +} + export interface ActiveCallStateType { conversationId: string; joinedAt?: number; diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index 1e19f43774..64e8d3baaf 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -65,12 +65,14 @@ const mapStateToActiveCallProp = (state: StateType) => { groupCallParticipants.push({ avatarPath: remoteConversation.avatarPath, color: remoteConversation.color, + demuxId: remoteParticipant.demuxId, firstName: remoteConversation.firstName, hasRemoteAudio: remoteParticipant.hasRemoteAudio, hasRemoteVideo: remoteParticipant.hasRemoteVideo, isSelf: remoteParticipant.isSelf, profileName: remoteConversation.profileName, title: remoteConversation.title, + videoAspectRatio: remoteParticipant.videoAspectRatio, }); } ); diff --git a/ts/types/Calling.ts b/ts/types/Calling.ts index c2f5a2ae1b..28f6fb253d 100644 --- a/ts/types/Calling.ts +++ b/ts/types/Calling.ts @@ -61,12 +61,14 @@ export enum GroupCallJoinState { export interface GroupCallRemoteParticipantType { avatarPath?: string; color?: ColorType; + demuxId: number; firstName?: string; hasRemoteAudio: boolean; hasRemoteVideo: boolean; isSelf: boolean; profileName?: string; title: string; + videoAspectRatio: number; } // Should match RingRTC's VideoFrameSource diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index ade88df303..8ea03d5274 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -14382,7 +14382,7 @@ "rule": "React-useRef", "path": "ts/components/CallScreen.js", "line": " const localVideoRef = react_1.useRef(null);", - "lineNumber": 39, + "lineNumber": 40, "reasonCategory": "usageTrusted", "updated": "2020-10-26T21:35:52.858Z", "reasonDetail": "Used to get the local video element for rendering." @@ -14427,7 +14427,7 @@ "rule": "React-useRef", "path": "ts/components/CallingPip.tsx", "line": " const videoContainerRef = React.useRef(null);", - "lineNumber": 46, + "lineNumber": 42, "reasonCategory": "usageTrusted", "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Element is measured. Its HTML is not used." @@ -14436,7 +14436,7 @@ "rule": "React-useRef", "path": "ts/components/CallingPip.tsx", "line": " const localVideoRef = React.useRef(null);", - "lineNumber": 47, + "lineNumber": 43, "reasonCategory": "usageTrusted", "updated": "2020-10-26T19:12:24.410Z", "reasonDetail": "Used to get the local video element for rendering." @@ -14571,7 +14571,7 @@ "rule": "React-useRef", "path": "ts/components/GroupCallRemoteParticipant.js", "line": " const remoteVideoRef = react_1.useRef(null);", - "lineNumber": 24, + "lineNumber": 26, "reasonCategory": "usageTrusted", "updated": "2020-11-11T21:56:04.179Z", "reasonDetail": "Needed to render the remote video element." @@ -14580,7 +14580,7 @@ "rule": "React-useRef", "path": "ts/components/GroupCallRemoteParticipant.js", "line": " const canvasContextRef = react_1.useRef(null);", - "lineNumber": 25, + "lineNumber": 27, "reasonCategory": "usageTrusted", "updated": "2020-11-17T23:29:38.698Z", "reasonDetail": "Doesn't touch the DOM." @@ -14589,7 +14589,7 @@ "rule": "React-useRef", "path": "ts/components/GroupCallRemoteParticipant.js", "line": " const frameBufferRef = react_1.useRef(new ArrayBuffer(FRAME_BUFFER_SIZE));", - "lineNumber": 26, + "lineNumber": 28, "reasonCategory": "usageTrusted", "updated": "2020-11-17T16:24:25.480Z", "reasonDetail": "Doesn't touch the DOM."