Skip to content

Commit

Permalink
feat: Navigation focus on initiaging call (#14690)
Browse files Browse the repository at this point in the history
* feat: Navigation focus on initiaging call

* feat: navigation focus on initiating a call

* cr fixes, add camera status for a11y
  • Loading branch information
phoenixhdd authored Feb 22, 2023
1 parent dbef0f4 commit 1d1bd29
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 24 deletions.
10 changes: 10 additions & 0 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,17 @@
"callAccept": "Accept",
"callChooseSharedScreen": "Choose a screen to share",
"callChooseSharedWindow": "Choose a window to share",
"cameraStatusOn": "on",
"cameraStatusOff": "off",
"callConversationAcceptOrDecline": "{{conversationName}} is calling. Press control + enter to accept the call or press control + shift + enter to decline the call.",
"startedAudioCallingAlert": "You are calling {{conversationName}}.",
"startedVideoCallingAlert": "You are calling {{conversationName}}, you camera is {{cameraStatus}}.",
"startedGroupCallingAlert": "You started a conference call with {{conversationName}}.",
"startedVideoGroupCallingAlert": "You started a conference call with {{conversationName}}, your camera is {{cameraStatus}}.",
"ongoingAudioCall": "Ongoing audio call with {{conversationName}}.",
"ongoingVideoCall": "Ongoing video call with {{conversationName}}, your camera is {{cameraStatus}}.",
"ongoingGroupAudioCall": "Ongoing conference call with {{conversationName}}.",
"ongoingGroupVideoCall": "Ongoing video conference call with {{conversationName}}, your camera is {{cameraStatus}}.",
"callDecline": "Decline",
"callDegradationAction": "OK",
"callDegradationDescription": "The call was disconnected because {{username}} is no longer a verified contact.",
Expand Down
32 changes: 27 additions & 5 deletions src/script/components/TitleBar/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
*/

import React, {useMemo, useEffect, useCallback} from 'react';
import React, {useMemo, useEffect, useCallback, useRef} from 'react';

import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums';
import {amplify} from 'amplify';
Expand All @@ -27,6 +27,7 @@ import {container} from 'tsyringe';
import {IconButton, IconButtonVariant, useMatchMedia} from '@wireapp/react-ui-kit';
import {WebAppEvents} from '@wireapp/webapp-events';

import {useCallAlertState} from 'Components/calling/useCallAlertState';
import {Icon} from 'Components/Icon';
import {LegalHoldDot} from 'Components/LegalHoldDot';
import {useAppMainState, ViewType} from 'src/script/page/state';
Expand Down Expand Up @@ -101,9 +102,11 @@ export const TitleBar: React.FC<TitleBarProps> = ({
]);

const {isActivatedAccount} = useKoSubscribableChildren(userState, ['isActivatedAccount']);
const {joinedCall} = useKoSubscribableChildren(callState, ['joinedCall']);
const {joinedCall, activeCalls} = useKoSubscribableChildren(callState, ['joinedCall', 'activeCalls']);
const {isVideoCallingEnabled} = useKoSubscribableChildren(teamState, ['isVideoCallingEnabled']);

const currentFocusedElementRef = useRef<HTMLButtonElement | null>(null);

const badgeLabelCopy = useMemo(() => {
if (is1to1 && isRequest) {
return '';
Expand Down Expand Up @@ -196,11 +199,22 @@ export const TitleBar: React.FC<TitleBarProps> = ({

const onClickStartAudio = () => {
callActions.startAudio(conversation);
showStartedCallAlert(isGroup);

if (smBreakpoint) {
setLeftSidebar();
}
};

useEffect(() => {
if (!activeCalls.length && currentFocusedElementRef.current) {
currentFocusedElementRef.current.focus();
currentFocusedElementRef.current = null;
}
}, [activeCalls.length]);

const {showStartedCallAlert} = useCallAlertState();

return (
<ul
id="conversation-title-bar"
Expand Down Expand Up @@ -236,7 +250,7 @@ export const TitleBar: React.FC<TitleBarProps> = ({
onClick={onClickDetails}
title={peopleTooltip}
aria-label={peopleTooltip}
onKeyDown={e => handleKeyDown(e, onClickDetails)}
onKeyDown={event => handleKeyDown(event, onClickDetails)}
data-placement="bottom"
role="button"
tabIndex={TabIndex.FOCUSABLE}
Expand Down Expand Up @@ -277,7 +291,11 @@ export const TitleBar: React.FC<TitleBarProps> = ({
className="conversation-title-bar-icon"
title={t('tooltipConversationVideoCall')}
aria-label={t('tooltipConversationVideoCall')}
onClick={() => callActions.startVideo(conversation)}
onClick={event => {
currentFocusedElementRef.current = event.target as HTMLButtonElement;
callActions.startVideo(conversation);
showStartedCallAlert(isGroup, true);
}}
data-uie-name="do-video-call"
>
<Icon.Camera />
Expand All @@ -289,7 +307,11 @@ export const TitleBar: React.FC<TitleBarProps> = ({
className="conversation-title-bar-icon"
title={t('tooltipConversationCall')}
aria-label={t('tooltipConversationCall')}
onClick={() => callActions.startAudio(conversation)}
onClick={event => {
currentFocusedElementRef.current = event.target as HTMLButtonElement;
callActions.startAudio(conversation);
showStartedCallAlert(isGroup);
}}
data-uie-name="do-call"
>
<Icon.Pickup />
Expand Down
49 changes: 43 additions & 6 deletions src/script/components/calling/CallingCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {Avatar, AVATAR_SIZE} from 'Components/Avatar';
import {GroupAvatar} from 'Components/avatar/GroupAvatar';
import {Duration} from 'Components/calling/Duration';
import {GroupVideoGrid} from 'Components/calling/GroupVideoGrid';
import {useCallAlertState} from 'Components/calling/useCallAlertState';
import {FadingScrollbar} from 'Components/FadingScrollbar';
import {Icon} from 'Components/Icon';
import {ClassifiedBar} from 'Components/input/ClassifiedBar';
Expand Down Expand Up @@ -101,6 +102,7 @@ const CallingCell: React.FC<CallingCellProps> = ({
'currentPage',
'muteState',
]);

const {
isGroup,
participating_user_ets: userEts,
Expand Down Expand Up @@ -146,8 +148,10 @@ const CallingCell: React.FC<CallingCellProps> = ({

const currentCallStatus = callStatus[state];

const showNoCameraPreview = !hasAccessToCamera && call.initialType === CALL_TYPE.VIDEO && !isOngoing;
const showVideoButton = isVideoCallingEnabled && (call.initialType === CALL_TYPE.VIDEO || isOngoing);
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);
Expand Down Expand Up @@ -221,6 +225,7 @@ const CallingCell: React.FC<CallingCellProps> = ({
}, [isOngoing, multitasking]);

const {setCurrentView} = useAppMainState(state => state.responsiveView);
const {showAlert, clearShowAlert} = useCallAlertState();

const answerCall = () => {
callActions.answer(call);
Expand Down Expand Up @@ -253,9 +258,26 @@ const CallingCell: React.FC<CallingCellProps> = ({
};
}

return () => undefined;
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'),
});

return (
<div className="conversation-calling-cell">
{isIncoming && (
Expand All @@ -267,7 +289,7 @@ const CallingCell: React.FC<CallingCellProps> = ({
{showJoinButton && isFullUi && (
<button
className="call-ui__button call-ui__button--green call-ui__button--join"
style={{margin: '40px 16px 0px 16px'}}
style={{margin: '40px 16px 0px'}}
onClick={() => callActions.answer(call)}
type="button"
data-uie-name="do-call-controls-call-join"
Expand All @@ -286,14 +308,25 @@ const CallingCell: React.FC<CallingCellProps> = ({
{muteState === MuteState.REMOTE_MUTED && isFullUi && (
<div className="conversation-list-calling-cell__info-bar">{t('muteStateRemoteMute')}</div>
)}

<div className="conversation-list-cell-right__calling">
<div
ref={element => {
if (isGroup && showAlert && !isVideoCall) {
element?.focus();
}
}}
className="conversation-list-cell conversation-list-cell-button"
onClick={createNavigate(conversationUrl)}
onBlur={() => clearShowAlert()}
onKeyDown={createNavigateKeyboard(conversationUrl)}
tabIndex={TabIndex.FOCUSABLE}
role="button"
aria-label={t('accessibility.openConversation', conversationName)}
aria-label={
showAlert
? callGroupStartedAlert
: `${isOngoing ? `${onGoingCallAlert} ` : ''}${t('accessibility.openConversation', conversationName)}`
}
>
{!temporaryUserStyle && (
<div className="conversation-list-cell-left">
Expand Down Expand Up @@ -485,9 +518,13 @@ const CallingCell: React.FC<CallingCellProps> = ({
{(isIncoming || isOutgoing) && (
<li className="conversation-list-calling-cell-controls-item">
<button
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={!isGroup}
className="call-ui__button call-ui__button--red call-ui__button--large"
onClick={() => (isIncoming ? callActions.reject(call) : callActions.leave(call))}
title={t('videoCallOverlayHangUp')}
onBlur={() => clearShowAlert()}
title={!isGroup && showAlert ? call1To1StartedAlert : t('videoCallOverlayHangUp')}
aria-label={!isGroup && showAlert ? call1To1StartedAlert : t('videoCallOverlayHangUp')}
type="button"
data-uie-name="do-call-controls-call-decline"
>
Expand Down
16 changes: 7 additions & 9 deletions src/script/components/calling/CallingOverlayContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {container} from 'tsyringe';

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';
Expand Down Expand Up @@ -90,14 +91,15 @@ const CallingContainer: React.FC<CallingContainerProps> = ({
callState.selectableWindows([]);
};

const changePage = (newPage: number, call: Call) => {
callingRepository.changeCallPage(call, newPage);
};
const changePage = (newPage: number, call: Call) => callingRepository.changeCallPage(call, newPage);

const {clearShowAlert} = useCallAlertState();

const leave = (call: Call) => {
callingRepository.leaveCall(call.conversationId, LEAVE_CALL_REASON.MANUAL_LEAVE_BY_UI_CLICK);
callState.activeCallViewTab(CallViewTab.ALL);
call.maximizedParticipant(null);
clearShowAlert();
};

const setMaximizedParticipant = (call: Call, participant: Participant | null) => {
Expand All @@ -121,13 +123,9 @@ const CallingContainer: React.FC<CallingContainerProps> = ({
callingRepository.refreshAudioInput();
};

const toggleCamera = (call: Call) => {
callingRepository.toggleCamera(call);
};
const toggleCamera = (call: Call) => callingRepository.toggleCamera(call);

const toggleMute = (call: Call, muteState: boolean) => {
callingRepository.muteCall(call, muteState);
};
const toggleMute = (call: Call, muteState: boolean) => callingRepository.muteCall(call, muteState);

const toggleScreenshare = async (call: Call): Promise<void> => {
if (call.getSelfParticipant().sharesScreen()) {
Expand Down
32 changes: 28 additions & 4 deletions src/script/components/calling/FullscreenVideoCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {container} from 'tsyringe';
import {CALL_TYPE, CONV_TYPE} from '@wireapp/avs';
import {IconButton, IconButtonVariant, useMatchMedia} from '@wireapp/react-ui-kit';

import {useCallAlertState} from 'Components/calling/useCallAlertState';
import {Icon} from 'Components/Icon';
import {ClassifiedBar} from 'Components/input/ClassifiedBar';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
Expand Down Expand Up @@ -128,6 +129,7 @@ const FullscreenVideoCall: React.FC<FullscreenVideoCallProps> = ({
const {videoInput: currentCameraDevice} = useKoSubscribableChildren(mediaDevicesHandler.currentDeviceId, [
DeviceTypes.VIDEO_INPUT,
]);

const minimize = () => multitasking.isMinimized(true);
const {videoInput} = useKoSubscribableChildren(mediaDevicesHandler.availableDevices, [DeviceTypes.VIDEO_INPUT]);
const showToggleVideo =
Expand Down Expand Up @@ -155,16 +157,17 @@ const FullscreenVideoCall: React.FC<FullscreenVideoCallProps> = ({
const {unreadMessagesCount} = useAppState();
const hasUnreadMessages = unreadMessagesCount > 0;

const {showAlert, isGroupCall, clearShowAlert} = useCallAlertState();

const totalPages = callPages.length;

const isSpaceOrEnterKey = (event: React.KeyboardEvent<HTMLDivElement>) => {
return event.key === KEY.ENTER || event.key === KEY.SPACE;
};
const isSpaceOrEnterKey = (event: React.KeyboardEvent<HTMLDivElement>) => [KEY.ENTER, KEY.SPACE].includes(event.key);

const handleToggleCameraKeydown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (isSpaceOrEnterKey(event)) {
toggleCamera(call);
}

return true;
};

Expand All @@ -183,6 +186,16 @@ const FullscreenVideoCall: React.FC<FullscreenVideoCallProps> = ({
};
}, []);

const callGroupStartedAlert = t(isGroupCall ? 'startedVideoGroupCallingAlert' : 'startedVideoCallingAlert', {
conversationName,
cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'),
});

const onGoingGroupCallAlert = t(isGroupCall ? 'ongoingGroupVideoCall' : 'ongoingVideoCall', {
conversationName,
cameraStatus: t(selfSharesCamera ? 'cameraStatusOn' : 'cameraStatusOff'),
});

return (
<div id="video-calling" className="video-calling">
<div id="video-title" className="video-title">
Expand Down Expand Up @@ -211,7 +224,18 @@ const FullscreenVideoCall: React.FC<FullscreenVideoCallProps> = ({
/>
)}

<div className="video-remote-name">
{/* Calling conversation name and duration */}
<div
className="video-remote-name"
aria-label={showAlert ? callGroupStartedAlert : onGoingGroupCallAlert}
tabIndex={TabIndex.FOCUSABLE}
ref={element => {
if (showAlert) {
element?.focus();
}
}}
onBlur={() => clearShowAlert()}
>
<h2 className="video-remote-title">{conversationName}</h2>

<div data-uie-name="video-timer" className="video-timer label-xs">
Expand Down
45 changes: 45 additions & 0 deletions src/script/components/calling/useCallAlertState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 {create} from 'zustand';

type CallAlertState = {
showAlert: boolean;
isGroupCall: boolean;
showStartedCallAlert: (isGroupCall: boolean, isVideoCall?: boolean) => void;
clearShowAlert: () => void;
};

const useCallAlertState = create<CallAlertState>((set, get) => ({
showAlert: false,
isGroupCall: false,
showStartedCallAlert: (isGroupCall = false, isVideoCall = false) =>
set(state => ({
...state,
showAlert: true,
isGroupCall,
})),
clearShowAlert: () =>
set(state => ({
...state,
showAlert: false,
})),
}));

export {useCallAlertState};

0 comments on commit 1d1bd29

Please sign in to comment.