diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index a7c3742d5b4..c4a9436d6b0 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -1522,7 +1522,16 @@ "verify.subhead": "Enter the verification code we sent to{newline}{email}", "videoCallOverlayCamera": "Camera", "videoCallOverlayConversations": "Conversations", + "videoCallOverlayOpenFullScreen": "Open the call in full screen", + "videoCallOverlayCloseFullScreen": "Go back to minimized view", + "videoCallOverlayParticipantsListLabel": "Participants ({{count}})", + "videoCallOverlayShowParticipantsList": "Show participants list", + "videoCallOverlayHideParticipantsList": "Hide participants list", "videoCallOverlayFitVideoLabel": "Double-click to view full screen", + "videoCallOverlayViewModeLabel": "View mode", + "videoCallOverlayChangeViewMode": "Change view mode", + "videoCallOverlayViewModeAll": "Show all participants", + "videoCallOverlayViewModeSpeakers": "Show active speakers only", "videoCallOverlayFitVideoLabelGoBack": "Double-click to show all participants", "videoCallOverlayHangUp": "Hang Up", "videoCallOverlayMicrophone": "Microphone", @@ -1538,6 +1547,7 @@ "videoCallvideoInputCamera": "Camera", "videoSpeakersTabAll": "All ({{count}})", "videoSpeakersTabSpeakers": "Speakers", + "viewingInAnotherWindow": "Viewing in another window", "warningCallIssues": "This version of {{brandName}} can not participate in the call. Please use", "warningCallQualityPoor": "Poor connection", "warningCallUnsupportedIncoming": "{{user}} is calling. Your browser doesn’t support calls.", diff --git a/src/script/calling/CallState.ts b/src/script/calling/CallState.ts index e1ba3a7ac7b..90fc2e47032 100644 --- a/src/script/calling/CallState.ts +++ b/src/script/calling/CallState.ts @@ -39,9 +39,14 @@ export enum MuteState { } export enum CallingViewMode { - FULL_SCREEN_GRID = 'full-screen-grid', + DETACHED_WINDOW = 'detached_window', MINIMIZED = 'minimized', - DETACHED_WINDOW = 'detached-window', +} + +export enum DesktopScreenShareMenu { + NONE = 'none', + MAIN_WINDOW = 'main_window', + DETACHED_WINDOW = 'detached_window', } type Emoji = {emoji: string; id: string; left: number; from: string}; @@ -60,9 +65,10 @@ export class CallState { public readonly activeCalls: ko.PureComputed; public readonly joinedCall: ko.PureComputed; public readonly activeCallViewTab = ko.observable(CallViewTab.ALL); - readonly isChoosingScreen: ko.PureComputed; + readonly hasAvailableScreensToShare: ko.PureComputed; readonly isSpeakersViewActive: ko.PureComputed; public readonly viewMode = ko.observable(CallingViewMode.MINIMIZED); + public readonly desktopScreenShareMenu = ko.observable(DesktopScreenShareMenu.NONE); constructor() { this.joinedCall = ko.pureComputed(() => this.calls().find(call => call.state() === CALL_STATE.MEDIA_ESTAB)); @@ -72,7 +78,7 @@ export class CallState { call => call.state() === CALL_STATE.INCOMING && call.reason() !== CALL_REASON.ANSWERED_ELSEWHERE, ), ); - this.isChoosingScreen = ko.pureComputed( + this.hasAvailableScreensToShare = ko.pureComputed( () => this.selectableScreens().length > 0 || this.selectableWindows().length > 0, ); @@ -84,7 +90,7 @@ export class CallState { }); this.isSpeakersViewActive = ko.pureComputed(() => this.activeCallViewTab() === CallViewTab.SPEAKERS); - this.isChoosingScreen = ko.pureComputed( + this.hasAvailableScreensToShare = ko.pureComputed( () => this.selectableScreens().length > 0 || this.selectableWindows().length > 0, ); } diff --git a/src/script/calling/CallingRepository.ts b/src/script/calling/CallingRepository.ts index a73463be094..3a4e7f6d8d3 100644 --- a/src/script/calling/CallingRepository.ts +++ b/src/script/calling/CallingRepository.ts @@ -62,7 +62,7 @@ import {createUuid} from 'Util/uuid'; import {Call, SerializedConversationId} from './Call'; import {callingSubscriptions} from './callingSubscriptionsHandler'; -import {CallState, MuteState} from './CallState'; +import {CallingViewMode, CallState, MuteState} from './CallState'; import {CALL_MESSAGE_TYPE} from './enum/CallMessageType'; import {LEAVE_CALL_REASON} from './enum/LeaveCallReason'; import {ClientId, Participant, UserId} from './Participant'; @@ -838,6 +838,7 @@ export class CallingRepository { } async startCall(conversation: Conversation, callType: CALL_TYPE): Promise { + this.callState.viewMode(CallingViewMode.MINIMIZED); if (!this.selfUser || !this.selfClientId) { this.logger.warn( `Calling repository is not initialized correctly \n ${JSON.stringify({ @@ -977,6 +978,7 @@ export class CallingRepository { }; async answerCall(call: Call, callType?: CALL_TYPE): Promise { + this.callState.viewMode(CallingViewMode.MINIMIZED); const {conversation} = call; try { callType ??= call.getSelfParticipant().sharesCamera() ? call.initialType : CALL_TYPE.NORMAL; diff --git a/src/script/components/AppContainer/AppContainer.tsx b/src/script/components/AppContainer/AppContainer.tsx index 27c9ca6221b..c2dab2a1756 100644 --- a/src/script/components/AppContainer/AppContainer.tsx +++ b/src/script/components/AppContainer/AppContainer.tsx @@ -25,6 +25,7 @@ import {container} from 'tsyringe'; import {StyledApp, THEME_ID} from '@wireapp/react-ui-kit'; import {DetachedCallingCell} from 'Components/calling/DetachedCallingCell'; +import {useDetachedCallingFeatureState} from 'Components/calling/DetachedCallingCell/DetachedCallingFeature.state'; import {PrimaryModalComponent} from 'Components/Modals/PrimaryModal/PrimaryModal'; import {SIGN_OUT_REASON} from 'src/script/auth/SignOutReason'; import {useAppSoftLock} from 'src/script/hooks/useAppSoftLock'; @@ -83,6 +84,7 @@ export const AppContainer: FC = ({config, clientType}) => { const {repository: repositories} = app; const {softLockEnabled} = useAppSoftLock(repositories.calling, repositories.notification); + const {isSupported: isDetachedCallingFeatureEnabled} = useDetachedCallingFeatureState(); if (hasOtherInstance) { app.redirectToLogin(SIGN_OUT_REASON.MULTIPLE_TABS); @@ -100,12 +102,13 @@ export const AppContainer: FC = ({config, clientType}) => { - + {isDetachedCallingFeatureEnabled() && ( + + )} ); }; diff --git a/src/script/components/Avatar/PlaceholderAvatar/DefaultAvatarImage.tsx b/src/script/components/Avatar/PlaceholderAvatar/DefaultAvatarImage.tsx index a1541b5bc86..aaae4d3ab90 100644 --- a/src/script/components/Avatar/PlaceholderAvatar/DefaultAvatarImage.tsx +++ b/src/script/components/Avatar/PlaceholderAvatar/DefaultAvatarImage.tsx @@ -42,7 +42,7 @@ export function DefaultAvatarImageSmall({diameter}: {diameter: number}) { export function DefaultAvatarImageLarge({diameter}: {diameter: number}) { return ( - + - - + ); })} diff --git a/src/script/components/DetachedWindow/DetachedWindow.tsx b/src/script/components/DetachedWindow/DetachedWindow.tsx index e42c3476ceb..dddfdc7451f 100644 --- a/src/script/components/DetachedWindow/DetachedWindow.tsx +++ b/src/script/components/DetachedWindow/DetachedWindow.tsx @@ -22,9 +22,11 @@ import {useEffect, useMemo} from 'react'; import createCache from '@emotion/cache'; import {CacheProvider} from '@emotion/react'; import weakMemoize from '@emotion/weak-memoize'; +import {amplify} from 'amplify'; import {createPortal} from 'react-dom'; import {StyledApp, THEME_ID} from '@wireapp/react-ui-kit'; +import {WebAppEvents} from '@wireapp/webapp-events'; import {useActiveWindow} from 'src/script/hooks/useActiveWindow'; import {calculateChildWindowPosition} from 'Util/DOM/caculateChildWindowPosition'; @@ -37,6 +39,7 @@ interface DetachedWindowProps { height?: number; onClose: () => void; name: string; + onNewWindowOpened?: (newWindow: Window) => void; } const memoizedCreateCacheWithContainer = weakMemoize((container: HTMLHeadElement) => { @@ -44,7 +47,14 @@ const memoizedCreateCacheWithContainer = weakMemoize((container: HTMLHeadElement return newCache; }); -export const DetachedWindow = ({children, name, onClose, width = 600, height = 600}: DetachedWindowProps) => { +export const DetachedWindow = ({ + children, + name, + onClose, + width = 600, + height = 600, + onNewWindowOpened, +}: DetachedWindowProps) => { const newWindow = useMemo(() => { const {top, left} = calculateChildWindowPosition(height, width); @@ -88,12 +98,18 @@ export const DetachedWindow = ({children, name, onClose, width = 600, height = 6 newWindow.addEventListener('beforeunload', onClose); window.addEventListener('pagehide', onPageHide); + amplify.subscribe(WebAppEvents.PROPERTIES.UPDATE.INTERFACE.THEME, () => { + newWindow.document.body.className = window.document.body.className; + }); + + onNewWindowOpened?.(newWindow); + return () => { newWindow.close(); newWindow.removeEventListener('beforeunload', onClose); window.removeEventListener('pagehide', onPageHide); }; - }, [height, name, width, onClose, newWindow]); + }, [height, name, width, onClose, newWindow, onNewWindowOpened]); return !newWindow ? null @@ -113,8 +129,12 @@ export const DetachedWindow = ({children, name, onClose, width = 600, height = 6 * @param target the target document object */ const copyStyles = (source: Document, target: Document) => { - source.head.querySelectorAll('link, style').forEach(htmlElement => { - target.head.appendChild(htmlElement.cloneNode(true)); + const targetHead = target.head; + + const elements = source.head.querySelectorAll('link, style'); + + elements.forEach(htmlElement => { + targetHead.insertBefore(htmlElement.cloneNode(true), targetHead.firstChild); }); target.body.className = source.body.className; diff --git a/src/script/components/calling/CallParticipantsListItem/CallParticipantsListItem.styles.ts b/src/script/components/calling/CallParticipantsListItem/CallParticipantsListItem.styles.ts index 7083bc6c757..a05cc0b6ea4 100644 --- a/src/script/components/calling/CallParticipantsListItem/CallParticipantsListItem.styles.ts +++ b/src/script/components/calling/CallParticipantsListItem/CallParticipantsListItem.styles.ts @@ -26,10 +26,7 @@ export const callParticipantListItemWrapper = (isLast = false): CSSObject => ({ '&:hover, &:focus, &:focus-visible': { backgroundColor: 'var(--disabled-call-button-bg)', }, - - ...(isLast && { - borderRadius: '0 0 8px 8px', - }), + borderBottom: isLast ? 'none' : '1px solid var(--border-color)', }); const commonIconStyles = { diff --git a/src/script/components/calling/CallingCell.tsx b/src/script/components/calling/CallingCell.tsx deleted file mode 100644 index 226532c9921..00000000000 --- a/src/script/components/calling/CallingCell.tsx +++ /dev/null @@ -1,654 +0,0 @@ -/* - * Wire - * Copyright (C) 2022 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -import React, {useCallback, useEffect, useState} from 'react'; - -import {DefaultConversationRoleName} from '@wireapp/api-client/lib/conversation/'; -import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; -import cx from 'classnames'; -import {container} from 'tsyringe'; - -import {CALL_TYPE, REASON as CALL_REASON, STATE as CALL_STATE} from '@wireapp/avs'; - -import {Avatar, AVATAR_SIZE, GroupAvatar} from 'Components/Avatar'; -import {Duration} from 'Components/calling/Duration'; -import {GroupVideoGrid} from 'Components/calling/GroupVideoGrid'; -import {useCallAlertState} from 'Components/calling/useCallAlertState'; -import {FadingScrollbar} from 'Components/FadingScrollbar'; -import * as Icon from 'Components/Icon'; -import {ConversationClassifiedBar} from 'Components/input/ClassifiedBar'; -import {usePushToTalk} from 'src/script/hooks/usePushToTalk/usePushToTalk'; -import {useAppMainState, ViewType} from 'src/script/page/state'; -import {useKoSubscribableChildren} from 'Util/ComponentUtil'; -import {isEnterKey, isSpaceOrEnterKey} from 'Util/KeyboardUtil'; -import {t} from 'Util/LocalizerUtil'; -import {sortUsersByPriority} from 'Util/StringUtil'; - -import {CallParticipantsListItem} from './CallParticipantsListItem'; -import {useDetachedCallingFeatureState} from './DetachedCallingCell/DetachedCallingFeature.state'; - -import type {Call} from '../../calling/Call'; -import type {CallingRepository} from '../../calling/CallingRepository'; -import {CallingViewMode, CallState, MuteState} from '../../calling/CallState'; -import type {Participant} from '../../calling/Participant'; -import {useVideoGrid} from '../../calling/videoGridHandler'; -import {generateConversationUrl} from '../../router/routeGenerator'; -import {createNavigate, createNavigateKeyboard} from '../../router/routerBindings'; -import {TeamState} from '../../team/TeamState'; -import {ContextMenuEntry, showContextMenu} from '../../ui/ContextMenu'; -import {CallActions, CallViewTab} from '../../view_model/CallingViewModel'; - -interface VideoCallProps { - hasAccessToCamera?: boolean; - isSelfVerified?: boolean; - teamState?: TeamState; -} - -interface AnsweringControlsProps { - call: Call; - callActions: CallActions; - callingRepository: Pick; - pushToTalkKey: string | null; - isFullUi?: boolean; - callState?: CallState; - classifiedDomains?: string[]; - isTemporaryUser?: boolean; - setMaximizedParticipant?: (participant: Participant | null) => void; -} - -export type CallingCellProps = VideoCallProps & AnsweringControlsProps; - -type labels = {dataUieName: string; text: string}; - -const CallingCell: React.FC = ({ - classifiedDomains, - isTemporaryUser, - call, - callActions, - isFullUi = false, - hasAccessToCamera, - isSelfVerified, - callingRepository, - pushToTalkKey, - setMaximizedParticipant, - teamState = container.resolve(TeamState), - callState = container.resolve(CallState), -}) => { - const {conversation} = call; - const {reason, state, isCbrEnabled, startedAt, participants, maximizedParticipant, muteState} = - useKoSubscribableChildren(call, [ - 'reason', - 'state', - 'isCbrEnabled', - 'startedAt', - 'participants', - 'maximizedParticipant', - 'pages', - 'currentPage', - 'muteState', - ]); - - const { - isGroup, - participating_user_ets: userEts, - selfUser, - display_name: conversationName, - roles, - } = useKoSubscribableChildren(conversation, [ - 'isGroup', - 'participating_user_ets', - 'selfUser', - 'display_name', - 'roles', - ]); - - const {viewMode} = useKoSubscribableChildren(callState, ['viewMode']); - const isFullScreenGrid = viewMode === CallingViewMode.FULL_SCREEN_GRID; - const isDetachedWindow = viewMode === CallingViewMode.DETACHED_WINDOW; - - const {isVideoCallingEnabled} = useKoSubscribableChildren(teamState, ['isVideoCallingEnabled']); - - const {activeCallViewTab} = useKoSubscribableChildren(callState, ['activeCallViewTab']); - const isMuted = muteState !== MuteState.NOT_MUTED; - - const isDeclined = !!reason && [CALL_REASON.STILL_ONGOING, CALL_REASON.ANSWERED_ELSEWHERE].includes(reason); - - const isOutgoing = state === CALL_STATE.OUTGOING; - const isIncoming = state === CALL_STATE.INCOMING; - const isConnecting = state === CALL_STATE.ANSWERED; - const isOngoing = state === CALL_STATE.MEDIA_ESTAB; - - const callStatus: Partial> = { - [CALL_STATE.OUTGOING]: { - dataUieName: 'call-label-outgoing', - text: t('callStateOutgoing'), - }, - [CALL_STATE.INCOMING]: { - dataUieName: 'call-label-incoming', - text: t('callStateIncoming'), - }, - [CALL_STATE.ANSWERED]: { - dataUieName: 'call-label-connecting', - text: t('callStateConnecting'), - }, - }; - - const currentCallStatus = callStatus[state]; - - const isVideoCall = call.initialType === CALL_TYPE.VIDEO; - - const showNoCameraPreview = !hasAccessToCamera && isVideoCall && !isOngoing; - const showVideoButton = isVideoCallingEnabled && (isVideoCall || isOngoing); - const showParticipantsButton = isOngoing && isGroup; - - const videoGrid = useVideoGrid(call); - - const conversationParticipants = conversation && (selfUser ? userEts.concat(selfUser) : userEts); - const conversationUrl = generateConversationUrl(conversation.qualifiedId); - const selfParticipant = call?.getSelfParticipant(); - - const { - sharesScreen: selfSharesScreen, - sharesCamera: selfSharesCamera, - hasActiveVideo: selfHasActiveVideo, - } = useKoSubscribableChildren(selfParticipant, ['sharesScreen', 'sharesCamera', 'hasActiveVideo']); - - const {activeSpeakers} = useKoSubscribableChildren(call, ['activeSpeakers']); - - const isOutgoingVideoCall = isOutgoing && selfSharesCamera; - const isVideoUnsupported = !selfSharesCamera && !conversation?.supportsVideoCall(call.isConference); - const disableVideoButton = isOutgoingVideoCall || isVideoUnsupported; - const disableScreenButton = !callingRepository.supportsScreenSharing; - - const [showParticipants, setShowParticipants] = useState(false); - const isModerator = selfUser && roles[selfUser.id] === DefaultConversationRoleName.WIRE_ADMIN; - - const toggleMute = useCallback( - (shouldMute: boolean) => callActions.toggleMute(call, shouldMute), - [call, callActions], - ); - - const isCurrentlyMuted = useCallback(() => { - const isMuted = call.muteState() === MuteState.SELF_MUTED; - return isMuted; - }, [call]); - - usePushToTalk({ - key: pushToTalkKey, - toggleMute, - isMuted: isCurrentlyMuted, - }); - - const getParticipantContext = (event: React.MouseEvent, participant: Participant) => { - event.preventDefault(); - - const muteParticipant: ContextMenuEntry = { - click: () => callingRepository.sendModeratorMute(conversation.qualifiedId, [participant]), - icon: Icon.MicOffIcon, - identifier: `moderator-mute-participant`, - isDisabled: participant.isMuted(), - label: t('moderatorMenuEntryMute'), - }; - - const muteOthers: ContextMenuEntry = { - click: () => { - callingRepository.sendModeratorMute( - conversation.qualifiedId, - participants.filter(p => p !== participant), - ); - }, - icon: Icon.MicOffIcon, - identifier: 'moderator-mute-others', - label: t('moderatorMenuEntryMuteAllOthers'), - }; - - const entries: ContextMenuEntry[] = [muteOthers].concat(!participant.user.isMe ? muteParticipant : []); - showContextMenu(event, entries, 'participant-moderator-menu'); - }; - - const handleMaximizeKeydown = useCallback( - (event: React.KeyboardEvent) => { - if (!isOngoing || isDetachedWindow) { - return; - } - if (isSpaceOrEnterKey(event.key)) { - callState.viewMode(CallingViewMode.FULL_SCREEN_GRID); - } - }, - [isOngoing, callState], - ); - - const handleMaximizeClick = useCallback(() => { - if (!isOngoing || isDetachedWindow) { - return; - } - callState.viewMode(CallingViewMode.FULL_SCREEN_GRID); - }, [isOngoing, callState]); - - const {setCurrentView} = useAppMainState(state => state.responsiveView); - const {showAlert, clearShowAlert} = useCallAlertState(); - - const answerCall = () => { - callActions.answer(call); - setCurrentView(ViewType.MOBILE_LEFT_SIDEBAR); - }; - - const answerOrRejectCall = useCallback( - (event: KeyboardEvent) => { - const answerCallShortcut = !event.shiftKey && event.ctrlKey && isEnterKey(event); - const hangUpCallShortcut = event.ctrlKey && event.shiftKey && isEnterKey(event); - - const removeEventListener = () => window.removeEventListener('keydown', answerOrRejectCall); - - if (answerCallShortcut || hangUpCallShortcut) { - event.preventDefault(); - event.stopPropagation(); - } - - if (answerCallShortcut) { - answerCall(); - removeEventListener(); - } - - if (hangUpCallShortcut) { - callActions.reject(call); - removeEventListener(); - } - }, - [call, callActions], - ); - - useEffect(() => { - if (isIncoming) { - // Capture will be dispatched to registered element before being dispatched to any EventTarget beneath it in the DOM Tree. - // It's needed because when someone is calling we need to change order of shortcuts to the top of keyboard usage. - // If we didn't pass this prop other Event Listeners will be dispatched in same time. - document.addEventListener('keydown', answerOrRejectCall, {capture: true}); - - return () => { - document.removeEventListener('keydown', answerOrRejectCall, {capture: true}); - }; - } - - return () => { - clearShowAlert(); - }; - }, [answerOrRejectCall, isIncoming]); - - const call1To1StartedAlert = t(isOutgoingVideoCall ? 'startedVideoCallingAlert' : 'startedAudioCallingAlert', { - conversationName, - cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), - }); - - const onGoingCallAlert = t(isOutgoingVideoCall ? 'ongoingVideoCall' : 'ongoingAudioCall', { - conversationName, - cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), - }); - - const callGroupStartedAlert = t(isOutgoingVideoCall ? 'startedVideoGroupCallingAlert' : 'startedGroupCallingAlert', { - conversationName, - cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), - }); - - const onGoingGroupCallAlert = t(isOutgoingVideoCall ? 'ongoingGroupVideoCall' : 'ongoingGroupAudioCall', { - conversationName, - cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), - }); - - const callStartedAlert = isGroup ? callGroupStartedAlert : call1To1StartedAlert; - const ongoingCallAlert = isGroup ? onGoingGroupCallAlert : onGoingCallAlert; - - const toggleDetachedWindow = () => { - if (isDetachedWindow) { - callState.viewMode(CallingViewMode.MINIMIZED); - } else { - callState.viewMode(CallingViewMode.DETACHED_WINDOW); - } - }; - - const isDetachedCallingFeatureEnabled = useDetachedCallingFeatureState(state => state.isSupported()); - - return ( -
- {isIncoming && ( -

- {t('callConversationAcceptOrDecline', conversationName)} -

- )} - - {conversation && (!isDeclined || isTemporaryUser) && ( -
- {muteState === MuteState.REMOTE_MUTED && isFullUi && ( -
{t('muteStateRemoteMute')}
- )} - -
-
{ - if ((isGroup || isOngoing) && showAlert && !isVideoCall) { - element?.focus(); - } - }} - className="conversation-list-cell conversation-list-cell-button" - onClick={createNavigate(conversationUrl)} - onBlur={() => { - if (isGroup || isOngoing) { - clearShowAlert(); - } - }} - onKeyDown={createNavigateKeyboard(conversationUrl)} - tabIndex={TabIndex.FOCUSABLE} - role="button" - aria-label={ - showAlert - ? callStartedAlert - : `${isOngoing ? `${ongoingCallAlert} ` : ''}${t('accessibility.openConversation', conversationName)}` - } - > - {!isTemporaryUser && ( -
- {isGroup && } - {!isGroup && !!conversationParticipants.length && ( - - )} -
- )} - -

- {conversationName} - - {currentCallStatus && ( - - {currentCallStatus.text} - - )} - - {isOngoing && startedAt && ( -
- - - - - {isCbrEnabled && ( - - CBR - - )} -
- )} -

-
- -
- {isOngoing && isDetachedCallingFeatureEnabled && ( - - )} - - {(isConnecting || isOngoing) && ( - - )} -
-
- - {(isOngoing || selfHasActiveVideo) && !isFullScreenGrid && !!videoGrid?.grid?.length && isFullUi ? ( -
- - - {isOngoing && !isDetachedWindow && ( -
- -
- )} -
- ) : ( - showNoCameraPreview && - isFullUi && ( -
- {t('callNoCameraAccess')} -
- ) - )} - - {classifiedDomains && ( - - )} - -
-
    - {isFullUi && ( - <> -
  • - -
  • - - {showVideoButton && ( -
  • - -
  • - )} - - {isOngoing && ( -
  • - -
  • - )} - - )} -
- -
    - {showParticipantsButton && isFullUi && ( -
  • - -
  • - )} - - {(isIncoming || isOutgoing) && !isDeclined && ( -
  • - -
  • - )} - - {isIncoming && ( -
  • - {isDeclined ? ( - - ) : ( - - )} -
  • - )} -
-
- - {isFullUi && ( -
- -
    - {participants - .slice() - .sort((participantA, participantB) => sortUsersByPriority(participantA.user, participantB.user)) - .map((participant, index, participantsArray) => ( -
  • - getParticipantContext(event, participant)} - isLast={participantsArray.length === index} - /> -
  • - ))} -
-
-
- )} -
- )} -
- ); -}; - -export {CallingCell}; diff --git a/src/script/components/calling/CallingCell/CallIngParticipantList/CallingParticipantList.styles.ts b/src/script/components/calling/CallingCell/CallIngParticipantList/CallingParticipantList.styles.ts new file mode 100644 index 00000000000..7f81989fe88 --- /dev/null +++ b/src/script/components/calling/CallingCell/CallIngParticipantList/CallingParticipantList.styles.ts @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +export const labelStyles: CSSObject = { + padding: '12px 10px', + fontWeight: 'var(--font-weight-semibold)', +}; diff --git a/src/script/components/calling/CallingCell/CallIngParticipantList/CallingParticipantList.tsx b/src/script/components/calling/CallingCell/CallIngParticipantList/CallingParticipantList.tsx new file mode 100644 index 00000000000..7d8cc31b3a3 --- /dev/null +++ b/src/script/components/calling/CallingCell/CallIngParticipantList/CallingParticipantList.tsx @@ -0,0 +1,109 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import React from 'react'; + +import cx from 'classnames'; + +import {CallParticipantsListItem} from 'Components/calling/CallParticipantsListItem'; +import {FadingScrollbar} from 'Components/FadingScrollbar'; +import * as Icon from 'Components/Icon'; +import {t} from 'Util/LocalizerUtil'; +import {sortUsersByPriority} from 'Util/StringUtil'; + +import {labelStyles} from './CallingParticipantList.styles'; + +import {CallingRepository} from '../../../../calling/CallingRepository'; +import {Participant} from '../../../../calling/Participant'; +import {Conversation} from '../../../../entity/Conversation'; +import {ContextMenuEntry, showContextMenu} from '../../../../ui/ContextMenu'; + +interface CallingParticipantListProps { + callingRepository: Pick; + conversation: Conversation; + isModerator?: boolean; + isSelfVerified?: boolean; + participants: Participant[]; + showParticipants?: boolean; +} + +export const CallingParticipantList = ({ + callingRepository, + conversation, + isModerator, + isSelfVerified, + participants, + showParticipants, +}: CallingParticipantListProps) => { + const getParticipantContext = (event: React.MouseEvent, participant: Participant) => { + event.preventDefault(); + + const muteParticipant = { + click: () => callingRepository.sendModeratorMute(conversation.qualifiedId, [participant]), + icon: Icon.MicOffIcon, + identifier: `moderator-mute-participant`, + isDisabled: participant.isMuted(), + label: t('moderatorMenuEntryMute'), + }; + + const muteOthers: ContextMenuEntry = { + click: () => { + callingRepository.sendModeratorMute( + conversation.qualifiedId, + participants.filter(p => p !== participant), + ); + }, + icon: Icon.MicOffIcon, + identifier: 'moderator-mute-others', + label: t('moderatorMenuEntryMuteAllOthers'), + }; + + const entries: ContextMenuEntry[] = [muteOthers].concat(!participant.user.isMe ? muteParticipant : []); + showContextMenu(event, entries, 'participant-moderator-menu'); + }; + + return ( +
+

{t('videoCallOverlayParticipantsListLabel', participants.length)}

+ +
    + {participants + .slice() + .sort((participantA, participantB) => sortUsersByPriority(participantA.user, participantB.user)) + .map((participant, index, participantsArray) => ( +
  • + getParticipantContext(event, participant)} + isLast={participantsArray.length === index} + /> +
  • + ))} +
+
+
+ ); +}; diff --git a/src/script/components/calling/CallingCell/CallIngParticipantList/index.ts b/src/script/components/calling/CallingCell/CallIngParticipantList/index.ts new file mode 100644 index 00000000000..9d3a12c8f61 --- /dev/null +++ b/src/script/components/calling/CallingCell/CallIngParticipantList/index.ts @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export * from './CallingParticipantList'; diff --git a/src/script/components/calling/CallingCell/CallingCell.styles.ts b/src/script/components/calling/CallingCell/CallingCell.styles.ts new file mode 100644 index 00000000000..e5830989433 --- /dev/null +++ b/src/script/components/calling/CallingCell/CallingCell.styles.ts @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +export const callingCellWrapper: CSSObject = { + backgroundColor: 'var(--app-bg-secondary)', + border: '1px solid 1px solid var(--border-color)', + borderRadius: '8px', + display: 'flex', + flexDirection: 'column', + padding: '12px 12px 16px', +}; + +export const callingContainer: CSSObject = { + position: 'relative', + display: 'flex', + flexDirection: 'column', + flexShrink: '0', + padding: '10px 12px 20px', + animation: 'show-call-ui @animation-timing-fast ease-in-out 0s 1', +}; + +export const infoBar: CSSObject = { + backgroundColor: 'var(--accent-color)', + borderRadius: '8px', + color: 'var(--app-bg-secondary)', + fontSize: 'var(--line-height-xs)', + fontWeight: 'var(--font-weight-medium)', + margin: '8px 8px 0', + padding: '4px', + textAlign: 'center', +}; diff --git a/src/script/components/calling/CallingCell.test.tsx b/src/script/components/calling/CallingCell/CallingCell.test.tsx similarity index 99% rename from src/script/components/calling/CallingCell.test.tsx rename to src/script/components/calling/CallingCell/CallingCell.test.tsx index ba35584f200..16ca62db913 100644 --- a/src/script/components/calling/CallingCell.test.tsx +++ b/src/script/components/calling/CallingCell/CallingCell.test.tsx @@ -72,7 +72,6 @@ const createProps = async () => { pushToTalkKey: null, conversation, hasAccessToCamera: true, - isSelfVerified: true, teamState: mockTeamState, videoGrid: {grid: [], thumbnail: undefined}, } as CallingCellProps; diff --git a/src/script/components/calling/CallingCell/CallingCell.tsx b/src/script/components/calling/CallingCell/CallingCell.tsx new file mode 100644 index 00000000000..d1ed1af5ed6 --- /dev/null +++ b/src/script/components/calling/CallingCell/CallingCell.tsx @@ -0,0 +1,360 @@ +/* + * Wire + * Copyright (C) 2022 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import React, {useCallback, useEffect} from 'react'; + +import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; +import {container} from 'tsyringe'; + +import {CALL_TYPE, REASON as CALL_REASON, STATE as CALL_STATE} from '@wireapp/avs'; + +import {callingContainer} from 'Components/calling/CallingCell/CallingCell.styles'; +import {CallingControls} from 'Components/calling/CallingCell/CallingControls'; +import {CallingHeader} from 'Components/calling/CallingCell/CallingHeader'; +import {GroupVideoGrid} from 'Components/calling/GroupVideoGrid'; +import {useCallAlertState} from 'Components/calling/useCallAlertState'; +import * as Icon from 'Components/Icon'; +import {ConversationClassifiedBar} from 'Components/input/ClassifiedBar'; +import {usePushToTalk} from 'src/script/hooks/usePushToTalk/usePushToTalk'; +import {useAppMainState, ViewType} from 'src/script/page/state'; +import {useKoSubscribableChildren} from 'Util/ComponentUtil'; +import {isEnterKey, isSpaceOrEnterKey} from 'Util/KeyboardUtil'; +import {t} from 'Util/LocalizerUtil'; + +import type {Call} from '../../../calling/Call'; +import type {CallingRepository} from '../../../calling/CallingRepository'; +import {CallingViewMode, CallState, MuteState} from '../../../calling/CallState'; +import type {Participant} from '../../../calling/Participant'; +import {useVideoGrid} from '../../../calling/videoGridHandler'; +import {generateConversationUrl} from '../../../router/routeGenerator'; +import {TeamState} from '../../../team/TeamState'; +import {CallActions, CallViewTab} from '../../../view_model/CallingViewModel'; + +interface VideoCallProps { + hasAccessToCamera?: boolean; + teamState?: TeamState; +} + +interface AnsweringControlsProps { + call: Call; + callActions: CallActions; + callingRepository: Pick; + pushToTalkKey: string | null; + isFullUi?: boolean; + callState?: CallState; + classifiedDomains?: string[]; + isTemporaryUser?: boolean; + setMaximizedParticipant?: (participant: Participant | null) => void; +} + +export type CallingCellProps = VideoCallProps & AnsweringControlsProps; + +export type CallLabel = {dataUieName: string; text: string}; + +export const CallingCell = ({ + classifiedDomains, + isTemporaryUser, + call, + callActions, + isFullUi = false, + hasAccessToCamera, + callingRepository, + pushToTalkKey, + setMaximizedParticipant, + teamState = container.resolve(TeamState), + callState = container.resolve(CallState), +}: CallingCellProps) => { + const {conversation} = call; + const {reason, state, isCbrEnabled, startedAt, maximizedParticipant, muteState} = useKoSubscribableChildren(call, [ + 'reason', + 'state', + 'isCbrEnabled', + 'startedAt', + 'maximizedParticipant', + 'pages', + 'currentPage', + 'muteState', + ]); + + const { + isGroup, + participating_user_ets: userEts, + selfUser, + display_name: conversationName, + } = useKoSubscribableChildren(conversation, ['isGroup', 'participating_user_ets', 'selfUser', 'display_name']); + const {activeCallViewTab, viewMode} = useKoSubscribableChildren(callState, ['activeCallViewTab', 'viewMode']); + + const selfParticipant = call.getSelfParticipant(); + + const {sharesCamera: selfSharesCamera, hasActiveVideo: selfHasActiveVideo} = useKoSubscribableChildren( + selfParticipant, + ['sharesCamera', 'hasActiveVideo'], + ); + + const {activeSpeakers} = useKoSubscribableChildren(call, ['activeSpeakers']); + + const isVideoCall = call.initialType === CALL_TYPE.VIDEO; + const isDetachedWindow = viewMode === CallingViewMode.DETACHED_WINDOW; + + const isMuted = muteState !== MuteState.NOT_MUTED; + const isCurrentlyMuted = useCallback(() => muteState === MuteState.SELF_MUTED, [muteState]); + + const isDeclined = !!reason && [CALL_REASON.STILL_ONGOING, CALL_REASON.ANSWERED_ELSEWHERE].includes(reason); + + const isOutgoing = state === CALL_STATE.OUTGOING; + const isIncoming = state === CALL_STATE.INCOMING; + const isConnecting = state === CALL_STATE.ANSWERED; + const isOngoing = state === CALL_STATE.MEDIA_ESTAB; + + const callStatus: Partial> = { + [CALL_STATE.OUTGOING]: { + dataUieName: 'call-label-outgoing', + text: t('callStateOutgoing'), + }, + [CALL_STATE.INCOMING]: { + dataUieName: 'call-label-incoming', + text: t('callStateIncoming'), + }, + [CALL_STATE.ANSWERED]: { + dataUieName: 'call-label-connecting', + text: t('callStateConnecting'), + }, + }; + + const currentCallStatus = callStatus[state]; + + const showNoCameraPreview = !hasAccessToCamera && isVideoCall && !isOngoing; + + const videoGrid = useVideoGrid(call); + + const conversationParticipants = selfUser ? userEts.concat(selfUser) : userEts; + const conversationUrl = generateConversationUrl(conversation.qualifiedId); + + const isOutgoingVideoCall = isOutgoing && selfSharesCamera; + + const toggleMute = useCallback( + (shouldMute: boolean) => callActions.toggleMute(call, shouldMute), + [call, callActions], + ); + + usePushToTalk({ + key: pushToTalkKey, + toggleMute, + isMuted: isCurrentlyMuted, + }); + + const handleMaximizeKeydown = useCallback( + (event: React.KeyboardEvent) => { + if (!isOngoing) { + return; + } + if (isSpaceOrEnterKey(event.key)) { + callState.viewMode(CallingViewMode.DETACHED_WINDOW); + } + }, + [isOngoing, callState], + ); + + const handleMaximizeClick = useCallback(() => { + if (!isOngoing) { + return; + } + callState.viewMode(CallingViewMode.DETACHED_WINDOW); + }, [isOngoing, callState]); + + const {setCurrentView} = useAppMainState(state => state.responsiveView); + const {showAlert, clearShowAlert} = useCallAlertState(); + + const answerCall = () => { + callActions.answer(call); + setCurrentView(ViewType.MOBILE_LEFT_SIDEBAR); + }; + + const answerOrRejectCall = useCallback( + (event: KeyboardEvent) => { + const answerCallShortcut = !event.shiftKey && event.ctrlKey && isEnterKey(event); + const hangUpCallShortcut = event.ctrlKey && event.shiftKey && isEnterKey(event); + + const removeEventListener = () => window.removeEventListener('keydown', answerOrRejectCall); + + if (answerCallShortcut || hangUpCallShortcut) { + event.preventDefault(); + event.stopPropagation(); + } + + if (answerCallShortcut) { + answerCall(); + removeEventListener(); + } + + if (hangUpCallShortcut) { + callActions.reject(call); + removeEventListener(); + } + }, + [call, callActions], + ); + + useEffect(() => { + if (isIncoming) { + // Capture will be dispatched to registered element before being dispatched to any EventTarget beneath it in the DOM Tree. + // It's needed because when someone is calling we need to change order of shortcuts to the top of keyboard usage. + // If we didn't pass this prop other Event Listeners will be dispatched in same time. + document.addEventListener('keydown', answerOrRejectCall, {capture: true}); + + return () => { + document.removeEventListener('keydown', answerOrRejectCall, {capture: true}); + }; + } + + return () => { + clearShowAlert(); + }; + }, [answerOrRejectCall, isIncoming]); + + const call1To1StartedAlert = t(isOutgoingVideoCall ? 'startedVideoCallingAlert' : 'startedAudioCallingAlert', { + conversationName, + cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), + }); + + const onGoingCallAlert = t(isOutgoingVideoCall ? 'ongoingVideoCall' : 'ongoingAudioCall', { + conversationName, + cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), + }); + + const callGroupStartedAlert = t(isOutgoingVideoCall ? 'startedVideoGroupCallingAlert' : 'startedGroupCallingAlert', { + conversationName, + cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), + }); + + const onGoingGroupCallAlert = t(isOutgoingVideoCall ? 'ongoingGroupVideoCall' : 'ongoingGroupAudioCall', { + conversationName, + cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), + }); + + const toggleDetachedWindow = () => { + callState.viewMode(isDetachedWindow ? CallingViewMode.MINIMIZED : CallingViewMode.DETACHED_WINDOW); + }; + + return ( +
+ {isIncoming && ( +

+ {t('callConversationAcceptOrDecline', conversationName)} +

+ )} + + {(!isDeclined || isTemporaryUser) && ( +
+ {muteState === MuteState.REMOTE_MUTED && isFullUi && ( +
{t('muteStateRemoteMute')}
+ )} + + + + {(isOngoing || selfHasActiveVideo) && !isDetachedWindow && !!videoGrid?.grid?.length && isFullUi ? ( + <> + {!isDetachedWindow && ( +
+ + + {isOngoing && ( +
+ +
+ )} +
+ )} + + ) : ( + showNoCameraPreview && + isFullUi && ( +
+ {t('callNoCameraAccess')} +
+ ) + )} + + {classifiedDomains && ( + + )} + + +
+ )} +
+ ); +}; diff --git a/src/script/components/calling/CallingCell/CallingControls/CallingControls.styles.ts b/src/script/components/calling/CallingCell/CallingControls/CallingControls.styles.ts new file mode 100644 index 00000000000..f4d85962667 --- /dev/null +++ b/src/script/components/calling/CallingCell/CallingControls/CallingControls.styles.ts @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +export const cellControlsWrapper: CSSObject = { + alignItems: 'center', + display: 'flex', + justifyContent: 'space-between', + width: '100%', +}; + +export const cellControlsList: CSSObject = { + display: 'flex', + gap: '8px', + listStyleType: 'none', + margin: 0, + padding: 0, +}; diff --git a/src/script/components/calling/CallingCell/CallingControls/CallingControls.tsx b/src/script/components/calling/CallingCell/CallingControls/CallingControls.tsx new file mode 100644 index 00000000000..f4e97079dbc --- /dev/null +++ b/src/script/components/calling/CallingCell/CallingControls/CallingControls.tsx @@ -0,0 +1,227 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import cx from 'classnames'; +import {container} from 'tsyringe'; + +import { + cellControlsList, + cellControlsWrapper, +} from 'Components/calling/CallingCell/CallingControls/CallingControls.styles'; +import {useCallAlertState} from 'Components/calling/useCallAlertState'; +import * as Icon from 'Components/Icon'; +import {DesktopScreenShareMenu} from 'src/script/calling/CallState'; +import {useKoSubscribableChildren} from 'Util/ComponentUtil'; +import {t} from 'Util/LocalizerUtil'; + +import {Call} from '../../../../calling/Call'; +import {Participant} from '../../../../calling/Participant'; +import {TeamState} from '../../../../team/TeamState'; +import {CallActions} from '../../../../view_model/CallingViewModel'; + +interface CallingControlsProps { + answerCall: () => void; + call: Call; + callActions: CallActions; + call1To1StartedAlert: string; + isDetachedWindow: boolean; + isFullUi?: boolean; + isMuted?: boolean; + isIncoming: boolean; + isOutgoing: boolean; + isOngoing: boolean; + isDeclined: boolean; + isGroup: boolean; + isVideoCall: boolean; + isConnecting?: boolean; + selfParticipant: Participant; + disableScreenButton: boolean; + teamState: TeamState; + supportsVideoCall: boolean; +} + +export const CallingControls = ({ + answerCall, + call, + callActions, + call1To1StartedAlert, + isFullUi, + isMuted, + isConnecting, + isDetachedWindow, + isIncoming, + isOutgoing, + isDeclined, + disableScreenButton, + isVideoCall, + isOngoing, + isGroup, + selfParticipant, + teamState = container.resolve(TeamState), + supportsVideoCall, +}: CallingControlsProps) => { + const {isVideoCallingEnabled} = useKoSubscribableChildren(teamState, ['isVideoCallingEnabled']); + const {sharesScreen: selfSharesScreen, sharesCamera: selfSharesCamera} = useKoSubscribableChildren(selfParticipant, [ + 'sharesScreen', + 'sharesCamera', + ]); + + const {showAlert, clearShowAlert} = useCallAlertState(); + + const isVideoUnsupported = !selfSharesCamera && !supportsVideoCall; + const showVideoButton = isVideoCallingEnabled && (isVideoCall || isOngoing); + const disableVideoButton = (isOutgoing && selfSharesCamera) || isVideoUnsupported; + + return ( +
+
    + {isFullUi && ( + <> +
  • + +
  • + + {showVideoButton && ( +
  • + +
  • + )} + + {isOngoing && ( +
  • + +
  • + )} + + )} +
+ +
    + {(isIncoming || isOutgoing) && !isDeclined && ( +
  • + +
  • + )} + + {isIncoming && ( +
  • + {isDeclined ? ( + + ) : ( + + )} +
  • + )} + + {(isConnecting || isOngoing) && ( +
  • + +
  • + )} +
+
+ ); +}; diff --git a/src/script/components/calling/CallingCell/CallingControls/index.ts b/src/script/components/calling/CallingCell/CallingControls/index.ts new file mode 100644 index 00000000000..6fcd6a5624a --- /dev/null +++ b/src/script/components/calling/CallingCell/CallingControls/index.ts @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export * from './CallingControls'; diff --git a/src/script/components/calling/CallingCell/CallingHeader/CallingHeader.styles.ts b/src/script/components/calling/CallingCell/CallingHeader/CallingHeader.styles.ts new file mode 100644 index 00000000000..6ceaa2059bc --- /dev/null +++ b/src/script/components/calling/CallingCell/CallingHeader/CallingHeader.styles.ts @@ -0,0 +1,88 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +export const callingHeaderContainer: CSSObject = { + alignItems: 'center', + borderRadius: '8px 8px 0 0', + cursor: 'pointer', + display: 'flex', + fontWeight: 'var(--font-weight-regular)', + marginBottom: '8px', + position: 'relative', + width: '100%', +}; + +export const callingHeaderWrapper: CSSObject = { + alignItems: 'center', + display: 'flex', + gap: '12px', + width: '100%', + overflow: 'hidden', +}; + +export const callAvatar: CSSObject = { + alignItems: 'center', + display: 'flex', +}; + +export const conversationCallName: CSSObject = { + fontSize: 'var(--font-size-medium)', + fontWeight: 'var(--font-weight-medium)', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}; + +export const callDescription: CSSObject = { + color: 'var(--background)', + fontSize: 'var(--font-size-small)', + fontWeight: 'var(--font-weight-regular)', +}; + +export const callDetails: CSSObject = { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + paddingRight: '12px', + width: '100%', +}; + +export const cbrCallState: CSSObject = { + fontWeight: 'var(--font-weight-semibold)', + marginLeft: '6px', +}; + +export const detachedWindowButton: CSSObject = { + alignItems: 'center', + background: 'transparent', + border: 'none', + display: 'flex', + justifyContent: 'center', + padding: '8px 12px', + + '& svg': { + fill: 'var(--text-color)', + }, + + '& svg path': { + fill: 'var(--text-color)', + }, +}; diff --git a/src/script/components/calling/CallingCell/CallingHeader/CallingHeader.tsx b/src/script/components/calling/CallingCell/CallingHeader/CallingHeader.tsx new file mode 100644 index 00000000000..1c652d6eb4a --- /dev/null +++ b/src/script/components/calling/CallingCell/CallingHeader/CallingHeader.tsx @@ -0,0 +1,157 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; + +import {Avatar, AVATAR_SIZE, GroupAvatar} from 'Components/Avatar'; +import {Duration} from 'Components/calling/Duration'; +import * as Icon from 'Components/Icon'; +import {t} from 'Util/LocalizerUtil'; + +import { + callAvatar, + callDescription, + callDetails, + callingHeaderContainer, + callingHeaderWrapper, + cbrCallState, + conversationCallName, + detachedWindowButton, +} from './CallingHeader.styles'; + +import {User} from '../../../../entity/User'; +import {createNavigate, createNavigateKeyboard} from '../../../../router/routerBindings'; + +interface CallingHeaderProps { + isOngoing: boolean; + isGroup: boolean; + + showAlert: boolean; + isVideoCall: boolean; + clearShowAlert: () => void; + conversationUrl: string; + callStartedAlert: string; + ongoingCallAlert: string; + isTemporaryUser: boolean; + conversationParticipants: User[]; + conversationName: string; + currentCallStatus: any; + startedAt?: number; + isCbrEnabled: boolean; + toggleDetachedWindow: () => void; + isDetachedWindow: boolean; +} + +export const CallingHeader = ({ + isGroup, + isOngoing, + showAlert, + isVideoCall, + clearShowAlert, + conversationUrl, + callStartedAlert, + ongoingCallAlert, + isTemporaryUser, + conversationParticipants, + conversationName, + currentCallStatus, + startedAt, + isCbrEnabled, + toggleDetachedWindow, + isDetachedWindow, +}: CallingHeaderProps) => { + return ( +
+
{ + if ((isGroup || isOngoing) && showAlert && !isVideoCall) { + element?.focus(); + } + }} + css={callingHeaderWrapper} + onClick={createNavigate(conversationUrl)} + onBlur={() => { + if (isGroup || isOngoing) { + clearShowAlert(); + } + }} + onKeyDown={createNavigateKeyboard(conversationUrl)} + tabIndex={TabIndex.FOCUSABLE} + role="button" + aria-label={ + showAlert + ? callStartedAlert + : `${isOngoing ? `${ongoingCallAlert} ` : ''}${t('accessibility.openConversation', conversationName)}` + } + > + {isDetachedWindow && !isTemporaryUser && ( +
+ {isGroup && } + {!isGroup && !!conversationParticipants.length && ( + + )} +
+ )} + +

+
{conversationName}
+ + {currentCallStatus && ( +
+ {currentCallStatus.text} +
+ )} + + {isOngoing && startedAt && ( +
+ {isDetachedWindow ? ( + + {t('viewingInAnotherWindow')} + + ) : ( + + + + )} + + {isCbrEnabled && ( + + CBR + + )} +
+ )} +

+
+ + {isOngoing && ( +
+ +
+ )} +
+ ); +}; diff --git a/src/script/components/calling/CallingCell/CallingHeader/index.ts b/src/script/components/calling/CallingCell/CallingHeader/index.ts new file mode 100644 index 00000000000..7d3a24f71a9 --- /dev/null +++ b/src/script/components/calling/CallingCell/CallingHeader/index.ts @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export * from './CallingHeader'; diff --git a/src/script/components/calling/CallingCell/index.ts b/src/script/components/calling/CallingCell/index.ts new file mode 100644 index 00000000000..bdc2f2b8760 --- /dev/null +++ b/src/script/components/calling/CallingCell/index.ts @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export * from './CallingCell'; diff --git a/src/script/components/calling/CallingOverlayContainer.tsx b/src/script/components/calling/CallingOverlayContainer.tsx index 13f38a77e51..802be3bf261 100644 --- a/src/script/components/calling/CallingOverlayContainer.tsx +++ b/src/script/components/calling/CallingOverlayContainer.tsx @@ -26,12 +26,12 @@ import {CALL_TYPE, STATE as CALL_STATE} from '@wireapp/avs'; import {useCallAlertState} from 'Components/calling/useCallAlertState'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; -import {ChooseScreen, Screen} from './ChooseScreen'; +import {ChooseScreen} from './ChooseScreen'; import {FullscreenVideoCall} from './FullscreenVideoCall'; import {Call} from '../../calling/Call'; import {CallingRepository} from '../../calling/CallingRepository'; -import {CallingViewMode, CallState, MuteState} from '../../calling/CallState'; +import {CallingViewMode, CallState, DesktopScreenShareMenu, MuteState} from '../../calling/CallState'; import {LEAVE_CALL_REASON} from '../../calling/enum/LeaveCallReason'; import {Participant} from '../../calling/Participant'; import {useVideoGrid} from '../../calling/videoGridHandler'; @@ -42,7 +42,7 @@ export interface CallingContainerProps { readonly callingRepository: CallingRepository; readonly mediaRepository: MediaRepository; readonly callState?: CallState; - readonly toggleScreenshare: (call: Call) => void; + readonly toggleScreenshare: (call: Call, desktopScreenShareMenu: DesktopScreenShareMenu) => void; } const CallingContainer: React.FC = ({ @@ -53,17 +53,12 @@ const CallingContainer: React.FC = ({ }) => { const {devicesHandler: mediaDevicesHandler} = mediaRepository; const {viewMode} = useKoSubscribableChildren(callState, ['viewMode']); - const isFullScreenGrid = viewMode === CallingViewMode.FULL_SCREEN_GRID; const isDetachedWindow = viewMode === CallingViewMode.DETACHED_WINDOW; - const {activeCallViewTab, joinedCall, selectableScreens, selectableWindows, isChoosingScreen} = - useKoSubscribableChildren(callState, [ - 'activeCallViewTab', - 'joinedCall', - 'selectableScreens', - 'selectableWindows', - 'isChoosingScreen', - ]); + const {activeCallViewTab, joinedCall, hasAvailableScreensToShare, desktopScreenShareMenu} = useKoSubscribableChildren( + callState, + ['activeCallViewTab', 'joinedCall', 'hasAvailableScreensToShare', 'desktopScreenShareMenu'], + ); const { maximizedParticipant, @@ -75,7 +70,7 @@ const CallingContainer: React.FC = ({ useEffect(() => { if (currentCallState === CALL_STATE.MEDIA_ESTAB && joinedCall?.initialType === CALL_TYPE.VIDEO) { - callState.viewMode(CallingViewMode.FULL_SCREEN_GRID); + callState.viewMode(CallingViewMode.DETACHED_WINDOW); } if (currentCallState === undefined) { callState.viewMode(CallingViewMode.MINIMIZED); @@ -84,11 +79,6 @@ const CallingContainer: React.FC = ({ const videoGrid = useVideoGrid(joinedCall!); - const onCancelScreenSelection = () => { - callState.selectableScreens([]); - callState.selectableWindows([]); - }; - const changePage = (newPage: number, call: Call) => callingRepository.changeCallPage(call, newPage); const {clearShowAlert} = useCallAlertState(); @@ -135,26 +125,34 @@ const CallingContainer: React.FC = ({ const conversation = joinedCall?.conversation; - if (isDetachedWindow || !joinedCall || !conversation || conversation.isSelfUserRemoved()) { + if (!joinedCall || !conversation || conversation.isSelfUserRemoved()) { return null; } + const toggleDetachedWindowScreenShare = (call: Call) => { + toggleScreenshare(call, DesktopScreenShareMenu.DETACHED_WINDOW); + }; + + const isScreenshareActive = + hasAvailableScreensToShare && desktopScreenShareMenu === DesktopScreenShareMenu.DETACHED_WINDOW; + return ( - {isFullScreenGrid && !!videoGrid?.grid.length && ( + {isDetachedWindow && !!videoGrid?.grid.length && ( = ({ setActiveCallViewTab={setActiveCallViewTab} toggleMute={toggleMute} toggleCamera={toggleCamera} - toggleScreenshare={toggleScreenshare} + toggleScreenshare={toggleDetachedWindowScreenShare} leave={leave} changePage={changePage} /> )} - {isChoosingScreen && ( - - )} + {isScreenshareActive && } ); }; diff --git a/src/script/components/calling/ChooseScreen.tsx b/src/script/components/calling/ChooseScreen.tsx index 8f8ef659e66..68071776045 100644 --- a/src/script/components/calling/ChooseScreen.tsx +++ b/src/script/components/calling/ChooseScreen.tsx @@ -17,23 +17,38 @@ * */ -import React, {Fragment, useEffect} from 'react'; +import React, {Fragment, useCallback, useEffect} from 'react'; +import {container} from 'tsyringe'; + +import {CallState} from 'src/script/calling/CallState'; +import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; export interface Screen { id: string; - thumbnail: HTMLCanvasElement; + thumbnail?: HTMLCanvasElement; } export interface ChooseScreenProps { - cancel: () => void; choose: (screenId: string) => void; - screens: Screen[]; - windows: Screen[]; + callState?: CallState; } -const ChooseScreen: React.FC = ({cancel, choose, screens = [], windows = []}: ChooseScreenProps) => { +const ChooseScreen: React.FC = ({ + choose, + callState = container.resolve(CallState), +}: ChooseScreenProps) => { + const {selectableScreens, selectableWindows} = useKoSubscribableChildren(callState, [ + 'selectableScreens', + 'selectableWindows', + ]); + + const cancel = useCallback(() => { + callState.selectableScreens([]); + callState.selectableWindows([]); + }, [callState]); + useEffect(() => { const closeOnEsc = ({key}: KeyboardEvent): void => { if (key === 'Escape') { @@ -56,18 +71,18 @@ const ChooseScreen: React.FC = ({cancel, choose, screens = [] data-uie-name={uieName} onClick={() => choose(id)} > - + )); return (
{t('callChooseSharedScreen')}
-
{renderPreviews(screens, 'item-screen')}
- {windows.length > 0 && ( +
{renderPreviews(selectableScreens, 'item-screen')}
+ {selectableWindows.length > 0 && (
{t('callChooseSharedWindow')}
-
{renderPreviews(windows, 'item-window')}
+
{renderPreviews(selectableWindows, 'item-window')}
)}
diff --git a/src/script/components/calling/DetachedCallingCell/DetachedCallingCell.tsx b/src/script/components/calling/DetachedCallingCell/DetachedCallingCell.tsx index 5192ff97a21..db28e359760 100644 --- a/src/script/components/calling/DetachedCallingCell/DetachedCallingCell.tsx +++ b/src/script/components/calling/DetachedCallingCell/DetachedCallingCell.tsx @@ -20,35 +20,30 @@ import {container} from 'tsyringe'; import {DetachedWindow} from 'Components/DetachedWindow'; +import {Call} from 'src/script/calling/Call'; import {CallingRepository} from 'src/script/calling/CallingRepository'; -import {CallState, CallingViewMode} from 'src/script/calling/CallState'; -import {TeamState} from 'src/script/team/TeamState'; +import {CallState, CallingViewMode, DesktopScreenShareMenu} from 'src/script/calling/CallState'; +import {MediaRepository} from 'src/script/media/MediaRepository'; import {UserState} from 'src/script/user/UserState'; -import {CallActions} from 'src/script/view_model/CallingViewModel'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; -import {CallingCell} from '../CallingCell'; +import {CallingContainer} from '../CallingOverlayContainer'; interface DetachedCallingCellProps { - callActions: CallActions; callingRepository: CallingRepository; - pushToTalkKey: string | null; - hasAccessToCamera: boolean; + mediaRepository: MediaRepository; + toggleScreenshare: (call: Call, desktopScreenShareMenu: DesktopScreenShareMenu) => void; callState?: CallState; - teamState?: TeamState; userState?: UserState; } export const DetachedCallingCell = ({ - callActions, callingRepository, - pushToTalkKey, - hasAccessToCamera, + mediaRepository, + toggleScreenshare, callState = container.resolve(CallState), - teamState = container.resolve(TeamState), userState = container.resolve(UserState), }: DetachedCallingCellProps) => { - const {classifiedDomains} = useKoSubscribableChildren(teamState, ['classifiedDomains']); const {joinedCall: activeCall, viewMode} = useKoSubscribableChildren(callState, ['joinedCall', 'viewMode']); const {self: selfUser} = useKoSubscribableChildren(userState, ['self']); @@ -63,17 +58,11 @@ export const DetachedCallingCell = ({ } return ( - - + activeCall.maximizedParticipant(participant)} + mediaRepository={mediaRepository} + toggleScreenshare={toggleScreenshare} /> ); diff --git a/src/script/components/calling/DetachedCallingCell/DetachedCallingFeature.state.ts b/src/script/components/calling/DetachedCallingCell/DetachedCallingFeature.state.ts index 3b7f1e73527..f1babd49bf1 100644 --- a/src/script/components/calling/DetachedCallingCell/DetachedCallingFeature.state.ts +++ b/src/script/components/calling/DetachedCallingCell/DetachedCallingFeature.state.ts @@ -21,15 +21,12 @@ import {create} from 'zustand'; import {Runtime} from '@wireapp/commons'; +import {Config} from 'src/script/Config'; + type DetachedCallingFeatureState = { - isEnabled: boolean; isSupported: () => boolean; - toggle: (shouldEnable: boolean) => void; }; -//TODO: This is a temporary solution for PoC to enable detached calling cell feature export const useDetachedCallingFeatureState = create((set, get) => ({ - isEnabled: false, - isSupported: () => !Runtime.isDesktopApp() && get().isEnabled, - toggle: shouldOpen => set({isEnabled: shouldOpen}), + isSupported: () => !Runtime.isDesktopApp() || Config.getDesktopConfig()?.supportsCallingPopoutWindow === true, })); diff --git a/src/script/components/calling/FullscreenVideoCall.styles.ts b/src/script/components/calling/FullscreenVideoCall.styles.ts index eb19e4fc616..dcfb02017d5 100644 --- a/src/script/components/calling/FullscreenVideoCall.styles.ts +++ b/src/script/components/calling/FullscreenVideoCall.styles.ts @@ -27,6 +27,7 @@ export const classifiedBarStyles: CSSObject = { export const videoControlActiveStyles = css` background-color: var(--main-color); border: 1px solid var(--main-color); + svg, svg > path { fill: var(--app-bg-secondary); } @@ -75,17 +76,30 @@ export const paginationButtonStyles: CSSObject = { }, outline: '1px solid var(--accent-color-focus)', }, - ['&:hover svg > path']: { + ['&:not([disabled]):hover svg > path']: { fill: 'var(--accent-color)', }, + ['&:disabled svg > path']: { + fill: 'var(--disabled-call-button-svg)', + }, + display: 'flex', alignItems: 'center', - backgroundColor: 'var(--app-bg-secondary)', cursor: 'pointer', + height: '100%', +}; + +export const videoTopBarStyles: CSSObject = { + display: 'grid', + gridTemplateColumns: '1fr auto 1fr', + alignItems: 'center', + backgroundColor: 'var(--sidebar-bg)', +}; + +export const paginationWrapperStyles: CSSObject = { display: 'flex', - height: 56, - justifyContent: 'center', - position: 'absolute', - top: 'calc(50% - 75px)', - width: 56, - zIndex: 1, + alignItems: 'center', + width: 'fit-content', + marginLeft: 'auto', + padding: '0 20px', + gap: '10px', }; diff --git a/src/script/components/calling/FullscreenVideoCall.tsx b/src/script/components/calling/FullscreenVideoCall.tsx index c8e0c6fbb3a..05c6c6195fa 100644 --- a/src/script/components/calling/FullscreenVideoCall.tsx +++ b/src/script/components/calling/FullscreenVideoCall.tsx @@ -19,26 +19,30 @@ import React, {useState, useEffect} from 'react'; +import {DefaultConversationRoleName} from '@wireapp/api-client/lib/conversation/'; import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; import classNames from 'classnames'; import {container} from 'tsyringe'; import {CALL_TYPE} from '@wireapp/avs'; -import {IconButton, IconButtonVariant, Select, useMatchMedia} from '@wireapp/react-ui-kit'; +import {GridIcon, IconButton, IconButtonVariant, Select} from '@wireapp/react-ui-kit'; import {useCallAlertState} from 'Components/calling/useCallAlertState'; import * as Icon from 'Components/Icon'; import {ConversationClassifiedBar} from 'Components/input/ClassifiedBar'; import {CallingRepository} from 'src/script/calling/CallingRepository'; import {Config} from 'src/script/Config'; +import {isCallViewOption} from 'src/script/guards/CallView'; import {isMediaDevice} from 'src/script/guards/MediaDevice'; +import {useActiveWindowMatchMedia} from 'src/script/hooks/useActiveWindowMatchMedia'; +import {useToggleState} from 'src/script/hooks/useToggleState'; import {MediaDeviceType} from 'src/script/media/MediaDeviceType'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {handleKeyDown, isEscapeKey} from 'Util/KeyboardUtil'; import {t} from 'Util/LocalizerUtil'; import {preventFocusOutside} from 'Util/util'; -import {ButtonGroup} from './ButtonGroup'; +import {CallingParticipantList} from './CallingCell/CallIngParticipantList'; import {Duration} from './Duration'; import { videoControlActiveStyles, @@ -46,19 +50,20 @@ import { videoControlDisabledStyles, paginationButtonStyles, classifiedBarStyles, + paginationWrapperStyles, + videoTopBarStyles, } from './FullscreenVideoCall.styles'; import {GroupVideoGrid} from './GroupVideoGrid'; import {Pagination} from './Pagination'; import type {Call} from '../../calling/Call'; import {CallingViewMode, CallState, MuteState} from '../../calling/CallState'; -import type {Participant} from '../../calling/Participant'; +import {Participant} from '../../calling/Participant'; import type {Grid} from '../../calling/videoGridHandler'; import type {Conversation} from '../../entity/Conversation'; import {ElectronDesktopCapturerSource, MediaDevicesHandler} from '../../media/MediaDevicesHandler'; -import {useAppState} from '../../page/useAppState'; import {TeamState} from '../../team/TeamState'; -import {CallViewTab, CallViewTabs} from '../../view_model/CallingViewModel'; +import {CallViewTab} from '../../view_model/CallingViewModel'; enum BlurredBackgroundStatus { OFF = 'bluroff', @@ -75,6 +80,7 @@ export interface FullscreenVideoCallProps { isMuted: boolean; leave: (call: Call) => void; maximizedParticipant: Participant | null; + callingRepository: CallingRepository; mediaDevicesHandler: MediaDevicesHandler; muteState: MuteState; setActiveCallViewTab: (tab: CallViewTab) => void; @@ -103,6 +109,7 @@ const FullscreenVideoCall: React.FC = ({ isMuted, muteState, mediaDevicesHandler, + callingRepository, videoGrid, maximizedParticipant, activeCallViewTab, @@ -160,18 +167,41 @@ const FullscreenVideoCall: React.FC = ({ MediaDeviceType.AUDIO_OUTPUT, ]); + const {selfUser, roles} = useKoSubscribableChildren(conversation, ['selfUser', 'roles']); const {emojis} = useKoSubscribableChildren(callState, ['emojis']); const [audioOptionsOpen, setAudioOptionsOpen] = useState(false); const [videoOptionsOpen, setVideoOptionsOpen] = useState(false); const minimize = () => callState.viewMode(CallingViewMode.MINIMIZED); + const [isParticipantsListOpen, toggleParticipantsList] = useToggleState(false); + const [isCallViewOpen, toggleCallView] = useToggleState(false); + const showToggleVideo = isVideoCallingEnabled && (call.initialType === CALL_TYPE.VIDEO || conversation.supportsVideoCall(call.isConference)); const showSwitchMicrophone = audioinput.length > 1; + const callViewOptions = [ + { + label: t('videoCallOverlayViewModeLabel'), + options: [ + { + label: t('videoCallOverlayViewModeAll'), + value: CallViewTab.ALL, + }, + { + label: t('videoCallOverlayViewModeSpeakers'), + value: CallViewTab.SPEAKERS, + }, + ], + }, + ]; + + const selectedCallViewOption = + callViewOptions[0].options.find(option => option.value === activeCallViewTab) ?? callViewOptions[0].options[0]; + const audioOptions = [ { label: t('videoCallaudioInputMicrophone'), @@ -294,17 +324,13 @@ const FullscreenVideoCall: React.FC = ({ }, CallingRepository.EMOJI_TIME_OUT_DURATION); }; - const unreadMessagesCount = useAppState(state => state.unreadMessagesCount); - const hasUnreadMessages = unreadMessagesCount > 0; - const {showAlert, isGroupCall, clearShowAlert} = useCallAlertState(); const totalPages = callPages.length; // To be changed when design chooses a breakpoint, the conditional can be integrated to the ui-kit directly - const horizontalSmBreakpoint = useMatchMedia('max-width: 680px'); - const horizontalXsBreakpoint = useMatchMedia('max-width: 500px'); - const verticalBreakpoint = useMatchMedia('max-height: 420px'); + const horizontalSmBreakpoint = useActiveWindowMatchMedia('max-width: 680px'); + const horizontalXsBreakpoint = useActiveWindowMatchMedia('max-width: 500px'); useEffect(() => { const onKeyDown = (event: KeyboardEvent): void => { @@ -327,99 +353,62 @@ const FullscreenVideoCall: React.FC = ({ cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'), }); - return ( -
-
- {horizontalSmBreakpoint && ( - - )} + const isModerator = selfUser && roles[selfUser.id] === DefaultConversationRoleName.WIRE_ADMIN; - {/* Calling conversation name and duration */} -
{ - if (showAlert) { - element?.focus(); - } - }} - onBlur={() => clearShowAlert()} - > -

{conversationName}

+ return ( +
+
+
+
+ {horizontalSmBreakpoint && ( + + )} -
- -
-
+ {/* Calling conversation name and duration */} +
{ + if (showAlert) { + element?.focus(); + } + }} + onBlur={() => clearShowAlert()} + > +

{conversationName}

- {muteState === MuteState.REMOTE_MUTED && ( -
- {t('muteStateRemoteMute')} -
- )} -
+
+ +
+
-
- setMaximizedParticipant(call, participant)} - /> - {classifiedDomains && ( - - )} - {!maximizedParticipant && activeCallViewTab === CallViewTab.ALL && totalPages > 1 && ( - <> - {currentPage !== totalPages - 1 && ( - + {t('muteStateRemoteMute')} +
)} - {currentPage !== 0 && ( +
+ {!maximizedParticipant && activeCallViewTab === CallViewTab.ALL && totalPages > 1 && ( +
- )} - {!verticalBreakpoint && ( -
- changePage(newPage, call)} - /> -
- )} - - )} -
- {!isChoosingScreen && ( -
- {showEmojisBar && ( -
- {EMOJIS_LIST.map(emoji => { - const isDisabled = disabledEmojis.includes(emoji); - return ( - - ); - })} + changePage(newPage, call)} + /> +
)} +
- {emojis.map(({id, emoji, left, from}) => ( -
- - -
- ))} - -
    - {!horizontalSmBreakpoint && ( -
  • - -
  • +
    + setMaximizedParticipant(call, participant)} + /> + {classifiedDomains && ( + + )} +
    + + {!isChoosingScreen && ( +
    + {showEmojisBar && ( +
    + {EMOJIS_LIST.map(emoji => { + const isDisabled = disabledEmojis.includes(emoji); + return ( + + ); + })} +
    )} -
    -
  • - + {emojis.map(({id, emoji, left, from}) => ( +
    + + +
    + ))} - {showSwitchMicrophone && ( +
      + {!horizontalSmBreakpoint && ( +
    • + )} +
    • + + {showToggleVideo && ( +
    • + + +
    • - )} -
    • - -
    • + {Config.getConfig().FEATURE.ENABLE_IN_CALL_REACTIONS && ( +
    • + +
    • + )} - {Config.getConfig().FEATURE.ENABLE_IN_CALL_REACTIONS && (
    • - )} -
    • - -
    • -
  • - {!horizontalXsBreakpoint && ( -
    - {participants.length > 2 && !horizontalXsBreakpoint && ( - { - setActiveCallViewTab(item as CallViewTab); - setMaximizedParticipant(call, null); - }} - currentItem={activeCallViewTab} - textSubstitute={participants.length.toString()} - /> - )}
    - )} -
-
+ {!horizontalXsBreakpoint && ( +
+ {participants.length > 2 && ( +
  • +