our video off => show our avatar
This commit is contained in:
parent
dfa04c68f4
commit
d50d7eb803
|
@ -148,8 +148,8 @@
|
|||
"linkPreviewsTitle": "Send Link Previews",
|
||||
"linkPreviewDescription": "Previews are supported for most urls",
|
||||
"linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.",
|
||||
"mediaPermissionsTitle": "Microphone and Camera",
|
||||
"mediaPermissionsDescription": "Allow access to camera and microphone",
|
||||
"mediaPermissionsTitle": "Microphone",
|
||||
"mediaPermissionsDescription": "Allow access to microphone",
|
||||
"spellCheckTitle": "Spell Check",
|
||||
"spellCheckDescription": "Enable spell check of text entered in message composition box",
|
||||
"spellCheckDirty": "You must restart Session to apply your new settings",
|
||||
|
@ -439,8 +439,8 @@
|
|||
"accept": "Accept",
|
||||
"decline": "Decline",
|
||||
"endCall": "End call",
|
||||
"micAndCameraPermissionNeededTitle": "Camera and Microphone access required",
|
||||
"micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy",
|
||||
"cameraPermissionNeededTitle": "Voice/Video Call permissions required",
|
||||
"cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.",
|
||||
"unableToCall": "cancel your ongoing call first",
|
||||
"unableToCallTitle": "Cannot start new call",
|
||||
"callMissed": "Missed call from $name$",
|
||||
|
@ -449,6 +449,7 @@
|
|||
"noCameraFound": "No camera found",
|
||||
"noAudioInputFound": "No audio input found",
|
||||
"callMediaPermissionsTitle": "Voice and video calls",
|
||||
"callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.",
|
||||
"callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users",
|
||||
"callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user."
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ const SessionToastContainerPrivate = () => {
|
|||
rtl={false}
|
||||
pauseOnFocusLoss={false}
|
||||
draggable={false}
|
||||
pauseOnHover={false}
|
||||
pauseOnHover={true}
|
||||
transition={Slide}
|
||||
limit={5}
|
||||
/>
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { openConversationWithMessages } from '../../../state/ducks/conversations';
|
||||
import { Avatar, AvatarSize } from '../../Avatar';
|
||||
import { getConversationController } from '../../../session/conversations';
|
||||
import { CallManagerOptionsType } from '../../../session/utils/CallManager';
|
||||
|
||||
export const DraggableCallWindow = styled.div`
|
||||
position: absolute;
|
||||
|
@ -28,11 +29,11 @@ export const DraggableCallWindow = styled.div`
|
|||
border: var(--session-border);
|
||||
`;
|
||||
|
||||
export const StyledVideoElement = styled.video<{ isRemoteVideoMuted: boolean }>`
|
||||
export const StyledVideoElement = styled.video<{ isVideoMuted: boolean }>`
|
||||
padding: 0 1rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: ${props => (props.isRemoteVideoMuted ? 0 : 1)};
|
||||
opacity: ${props => (props.isVideoMuted ? 0 : 1)};
|
||||
`;
|
||||
|
||||
const StyledDraggableVideoElement = styled(StyledVideoElement)`
|
||||
|
@ -97,16 +98,10 @@ export const DraggableCallContainer = () => {
|
|||
useEffect(() => {
|
||||
if (ongoingCallPubkey !== selectedConversationKey) {
|
||||
CallManager.setVideoEventsListener(
|
||||
(
|
||||
_localStream: MediaStream | null,
|
||||
remoteStream: MediaStream | null,
|
||||
_camerasList: any,
|
||||
_audioList: any,
|
||||
remoteVideoIsMuted: boolean
|
||||
) => {
|
||||
({ isRemoteVideoStreamMuted, remoteStream }: CallManagerOptionsType) => {
|
||||
if (mountedState() && videoRefRemote?.current) {
|
||||
videoRefRemote.current.srcObject = remoteStream;
|
||||
setIsRemoteVideoMuted(remoteVideoIsMuted);
|
||||
setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -157,7 +152,7 @@ export const DraggableCallContainer = () => {
|
|||
<StyledDraggableVideoElement
|
||||
ref={videoRefRemote}
|
||||
autoPlay={true}
|
||||
isRemoteVideoMuted={isRemoteVideoMuted}
|
||||
isVideoMuted={isRemoteVideoMuted}
|
||||
/>
|
||||
{isRemoteVideoMuted && (
|
||||
<CenteredAvatarInDraggable>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
|
|||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import styled from 'styled-components';
|
||||
import _ from 'underscore';
|
||||
import { CallManager, ToastUtils } from '../../../session/utils';
|
||||
import { CallManager, ToastUtils, UserUtils } from '../../../session/utils';
|
||||
import {
|
||||
getHasOngoingCall,
|
||||
getHasOngoingCallWith,
|
||||
|
@ -13,7 +13,7 @@ import {
|
|||
} from '../../../state/selectors/conversations';
|
||||
import { SessionIconButton } from '../icon';
|
||||
import { animation, contextMenu, Item, Menu } from 'react-contexify';
|
||||
import { InputItem } from '../../../session/utils/CallManager';
|
||||
import { CallManagerOptionsType, InputItem } from '../../../session/utils/CallManager';
|
||||
import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton';
|
||||
import { StyledVideoElement } from './CallContainer';
|
||||
import { Avatar, AvatarSize } from '../../Avatar';
|
||||
|
@ -124,8 +124,9 @@ const AudioInputMenu = ({
|
|||
};
|
||||
|
||||
const CenteredAvatarInConversation = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -50%;
|
||||
transform: translateY(-50%);
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 50%;
|
||||
|
@ -151,39 +152,50 @@ export const InConversationCallContainer = () => {
|
|||
const videoRefLocal = useRef<any>();
|
||||
const mountedState = useMountedState();
|
||||
|
||||
const [isVideoMuted, setVideoMuted] = useState(true);
|
||||
const [isLocalVideoMuted, setLocalVideoMuted] = useState(true);
|
||||
const [isRemoteVideoMuted, setIsRemoteVideoMuted] = useState(true);
|
||||
|
||||
const [isAudioMuted, setAudioMuted] = useState(false);
|
||||
|
||||
const videoTriggerId = 'video-menu-trigger-id';
|
||||
const audioTriggerId = 'audio-menu-trigger-id';
|
||||
|
||||
const avatarPath = ongoingCallPubkey
|
||||
const remoteAvatarPath = ongoingCallPubkey
|
||||
? getConversationController()
|
||||
.get(ongoingCallPubkey)
|
||||
.getAvatarPath()
|
||||
: undefined;
|
||||
|
||||
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
|
||||
const ourUsername = getConversationController()
|
||||
.get(ourPubkey)
|
||||
.getProfileName();
|
||||
|
||||
const ourAvatarPath = getConversationController()
|
||||
.get(ourPubkey)
|
||||
.getAvatarPath();
|
||||
|
||||
useEffect(() => {
|
||||
if (ongoingCallPubkey === selectedConversationKey) {
|
||||
CallManager.setVideoEventsListener(
|
||||
(
|
||||
localStream: MediaStream | null,
|
||||
remoteStream: MediaStream | null,
|
||||
camerasList: Array<InputItem>,
|
||||
audioInputList: Array<InputItem>,
|
||||
isRemoteVideoStreamMuted: boolean
|
||||
) => {
|
||||
if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) {
|
||||
videoRefLocal.current.srcObject = localStream;
|
||||
setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
|
||||
videoRefRemote.current.srcObject = remoteStream;
|
||||
CallManager.setVideoEventsListener((options: CallManagerOptionsType) => {
|
||||
const {
|
||||
audioInputsList,
|
||||
camerasList,
|
||||
isLocalVideoStreamMuted,
|
||||
isRemoteVideoStreamMuted,
|
||||
localStream,
|
||||
remoteStream,
|
||||
} = options;
|
||||
if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) {
|
||||
videoRefLocal.current.srcObject = localStream;
|
||||
setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
|
||||
setLocalVideoMuted(isLocalVideoStreamMuted);
|
||||
videoRefRemote.current.srcObject = remoteStream;
|
||||
|
||||
setCurrentConnectedCameras(camerasList);
|
||||
setCurrentConnectedAudioInputs(audioInputList);
|
||||
}
|
||||
setCurrentConnectedCameras(camerasList);
|
||||
setCurrentConnectedAudioInputs(audioInputsList);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -204,14 +216,14 @@ export const InConversationCallContainer = () => {
|
|||
|
||||
return;
|
||||
}
|
||||
if (isVideoMuted) {
|
||||
if (isLocalVideoMuted) {
|
||||
// select the first one
|
||||
await CallManager.selectCameraByDeviceId(currentConnectedCameras[0].deviceId);
|
||||
} else {
|
||||
await CallManager.selectCameraByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID);
|
||||
}
|
||||
|
||||
setVideoMuted(!isVideoMuted);
|
||||
setLocalVideoMuted(!isLocalVideoMuted);
|
||||
};
|
||||
|
||||
const handleMicrophoneToggle = async () => {
|
||||
|
@ -263,13 +275,13 @@ export const InConversationCallContainer = () => {
|
|||
<StyledVideoElement
|
||||
ref={videoRefRemote}
|
||||
autoPlay={true}
|
||||
isRemoteVideoMuted={isRemoteVideoMuted}
|
||||
isVideoMuted={isRemoteVideoMuted}
|
||||
/>
|
||||
{isRemoteVideoMuted && (
|
||||
<CenteredAvatarInConversation>
|
||||
<Avatar
|
||||
size={AvatarSize.XL}
|
||||
avatarPath={avatarPath}
|
||||
avatarPath={remoteAvatarPath}
|
||||
name={ongoingCallUsername}
|
||||
pubkey={ongoingCallPubkey}
|
||||
/>
|
||||
|
@ -281,8 +293,18 @@ export const InConversationCallContainer = () => {
|
|||
ref={videoRefLocal}
|
||||
autoPlay={true}
|
||||
muted={true}
|
||||
isRemoteVideoMuted={false}
|
||||
isVideoMuted={isLocalVideoMuted}
|
||||
/>
|
||||
{isLocalVideoMuted && (
|
||||
<CenteredAvatarInConversation>
|
||||
<Avatar
|
||||
size={AvatarSize.XL}
|
||||
avatarPath={ourAvatarPath}
|
||||
name={ourUsername}
|
||||
pubkey={ourPubkey}
|
||||
/>
|
||||
</CenteredAvatarInConversation>
|
||||
)}
|
||||
</VideoContainer>
|
||||
|
||||
<InConvoCallWindowControls>
|
||||
|
@ -298,7 +320,7 @@ export const InConversationCallContainer = () => {
|
|||
/>
|
||||
<DropDownAndToggleButton
|
||||
iconType="camera"
|
||||
isMuted={isVideoMuted}
|
||||
isMuted={isLocalVideoMuted}
|
||||
onMainButtonClick={handleCameraToggle}
|
||||
onArrowClick={showVideoInputMenu}
|
||||
/>
|
||||
|
@ -312,7 +334,7 @@ export const InConversationCallContainer = () => {
|
|||
<VideoInputMenu
|
||||
triggerId={videoTriggerId}
|
||||
onUnmute={() => {
|
||||
setVideoMuted(false);
|
||||
setLocalVideoMuted(false);
|
||||
}}
|
||||
camerasList={currentConnectedCameras}
|
||||
/>
|
||||
|
|
|
@ -373,7 +373,7 @@ export function getStartCallMenuItem(conversationId: string): JSX.Element | null
|
|||
}
|
||||
|
||||
if (!getCallMediaPermissionsSettings()) {
|
||||
ToastUtils.pushMicAndCameraPermissionNeeded();
|
||||
ToastUtils.pushVideoCallPermissionNeeded();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,26 +22,28 @@ export type InputItem = { deviceId: string; label: string };
|
|||
// const VIDEO_WIDTH = 640;
|
||||
// const VIDEO_RATIO = 16 / 9;
|
||||
|
||||
type CallManagerListener =
|
||||
| ((
|
||||
localStream: MediaStream | null,
|
||||
remoteStream: MediaStream | null,
|
||||
camerasList: Array<InputItem>,
|
||||
audioInputsList: Array<InputItem>,
|
||||
isRemoteVideoStreamMuted: boolean
|
||||
) => void)
|
||||
| null;
|
||||
export type CallManagerOptionsType = {
|
||||
localStream: MediaStream | null;
|
||||
remoteStream: MediaStream | null;
|
||||
camerasList: Array<InputItem>;
|
||||
audioInputsList: Array<InputItem>;
|
||||
isLocalVideoStreamMuted: boolean;
|
||||
isRemoteVideoStreamMuted: boolean;
|
||||
};
|
||||
|
||||
export type CallManagerListener = ((options: CallManagerOptionsType) => void) | null;
|
||||
let videoEventsListener: CallManagerListener;
|
||||
|
||||
function callVideoListener() {
|
||||
if (videoEventsListener) {
|
||||
videoEventsListener(
|
||||
mediaDevices,
|
||||
videoEventsListener({
|
||||
localStream: mediaDevices,
|
||||
remoteStream,
|
||||
camerasList,
|
||||
audioInputsList,
|
||||
remoteVideoStreamIsMuted
|
||||
);
|
||||
isRemoteVideoStreamMuted: remoteVideoStreamIsMuted,
|
||||
isLocalVideoStreamMuted: selectedCameraId === INPUT_DISABLED_DEVICE_ID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +81,7 @@ const configuration: RTCConfiguration = {
|
|||
iceTransportPolicy: 'relay',
|
||||
};
|
||||
|
||||
let selectedCameraId: string | undefined;
|
||||
let selectedCameraId: string = INPUT_DISABLED_DEVICE_ID;
|
||||
let selectedAudioInputId: string | undefined;
|
||||
let camerasList: Array<InputItem> = [];
|
||||
let audioInputsList: Array<InputItem> = [];
|
||||
|
@ -115,8 +117,7 @@ async function updateInputLists() {
|
|||
}
|
||||
|
||||
function sendVideoStatusViaDataChannel() {
|
||||
const videoEnabledLocally =
|
||||
selectedCameraId !== undefined && selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
|
||||
const videoEnabledLocally = selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
|
||||
const stringToSend = JSON.stringify({
|
||||
video: videoEnabledLocally,
|
||||
});
|
||||
|
@ -127,7 +128,7 @@ function sendVideoStatusViaDataChannel() {
|
|||
|
||||
export async function selectCameraByDeviceId(cameraDeviceId: string) {
|
||||
if (cameraDeviceId === INPUT_DISABLED_DEVICE_ID) {
|
||||
selectedCameraId = cameraDeviceId;
|
||||
selectedCameraId = INPUT_DISABLED_DEVICE_ID;
|
||||
|
||||
const sender = peerConnection?.getSenders().find(s => {
|
||||
return s.track?.kind === 'video';
|
||||
|
@ -136,6 +137,7 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
|
|||
sender.track.enabled = false;
|
||||
}
|
||||
sendVideoStatusViaDataChannel();
|
||||
callVideoListener();
|
||||
return;
|
||||
}
|
||||
if (camerasList.some(m => m.deviceId === cameraDeviceId)) {
|
||||
|
@ -164,12 +166,15 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
|
|||
mediaDevices?.removeTrack(t);
|
||||
});
|
||||
mediaDevices?.addTrack(videoTrack);
|
||||
|
||||
sendVideoStatusViaDataChannel();
|
||||
callVideoListener();
|
||||
} else {
|
||||
throw new Error('Failed to get sender for selectCameraByDeviceId ');
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.warn('selectCameraByDeviceId failed with', e.message);
|
||||
callVideoListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -301,7 +306,7 @@ async function openMediaDevicesAndAddTracks() {
|
|||
}
|
||||
});
|
||||
} catch (err) {
|
||||
ToastUtils.pushMicAndCameraPermissionNeeded();
|
||||
ToastUtils.pushVideoCallPermissionNeeded();
|
||||
closeVideoCall();
|
||||
}
|
||||
callVideoListener();
|
||||
|
@ -310,7 +315,7 @@ async function openMediaDevicesAndAddTracks() {
|
|||
// tslint:disable-next-line: function-name
|
||||
export async function USER_callRecipient(recipient: string) {
|
||||
if (!getCallMediaPermissionsSettings()) {
|
||||
ToastUtils.pushMicAndCameraPermissionNeeded();
|
||||
ToastUtils.pushVideoCallPermissionNeeded();
|
||||
return;
|
||||
}
|
||||
await updateInputLists();
|
||||
|
@ -420,8 +425,16 @@ function closeVideoCall() {
|
|||
|
||||
mediaDevices = null;
|
||||
remoteStream = null;
|
||||
selectedCameraId = INPUT_DISABLED_DEVICE_ID;
|
||||
if (videoEventsListener) {
|
||||
videoEventsListener(null, null, [], [], true);
|
||||
videoEventsListener({
|
||||
audioInputsList: [],
|
||||
camerasList: [],
|
||||
isLocalVideoStreamMuted: true,
|
||||
isRemoteVideoStreamMuted: true,
|
||||
localStream: null,
|
||||
remoteStream: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -592,7 +605,14 @@ export function handleCallTypeEndCall(sender: string) {
|
|||
if (callingConvos.length === 1 && callingConvos[0].id === sender) {
|
||||
closeVideoCall();
|
||||
if (videoEventsListener) {
|
||||
videoEventsListener(null, null, [], [], true);
|
||||
videoEventsListener({
|
||||
audioInputsList: [],
|
||||
camerasList: [],
|
||||
isLocalVideoStreamMuted: true,
|
||||
isRemoteVideoStreamMuted: true,
|
||||
localStream: null,
|
||||
remoteStream: null,
|
||||
});
|
||||
}
|
||||
window.inboxStore?.dispatch(endCall({ pubkey: sender }));
|
||||
}
|
||||
|
@ -633,21 +653,20 @@ export async function handleCallTypeOffer(
|
|||
|
||||
const convos = getConversationController().getConversations();
|
||||
const callingConvos = convos.filter(convo => convo.callState !== undefined);
|
||||
|
||||
if (!getCallMediaPermissionsSettings()) {
|
||||
await handleMissedCall(sender, incomingOfferTimestamp, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (callingConvos.length > 0) {
|
||||
// we just got a new offer from someone we are NOT already in a call with
|
||||
if (callingConvos.length !== 1 || callingConvos[0].id !== sender) {
|
||||
await handleMissedCall(sender, incomingOfferTimestamp);
|
||||
await handleMissedCall(sender, incomingOfferTimestamp, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!getCallMediaPermissionsSettings()) {
|
||||
await handleMissedCall(sender, incomingOfferTimestamp);
|
||||
// TODO audric show where to turn it on
|
||||
throw new Error('TODO AUDRIC');
|
||||
return;
|
||||
}
|
||||
|
||||
const readyForOffer =
|
||||
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
|
||||
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
|
||||
|
@ -672,6 +691,7 @@ export async function handleCallTypeOffer(
|
|||
await buildAnswerAndSendIt(sender);
|
||||
}
|
||||
}
|
||||
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
|
||||
|
||||
// don't need to do the sending here as we dispatch an answer in a
|
||||
} catch (err) {
|
||||
|
@ -682,16 +702,28 @@ export async function handleCallTypeOffer(
|
|||
callCache.set(sender, new Array());
|
||||
}
|
||||
callCache.get(sender)?.push(callMessage);
|
||||
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
|
||||
}
|
||||
|
||||
async function handleMissedCall(sender: string, incomingOfferTimestamp: number) {
|
||||
async function handleMissedCall(
|
||||
sender: string,
|
||||
incomingOfferTimestamp: number,
|
||||
isBecauseOfCallPermission: boolean
|
||||
) {
|
||||
const incomingCallConversation = await getConversationById(sender);
|
||||
ToastUtils.pushedMissedCall(
|
||||
incomingCallConversation?.getNickname() ||
|
||||
incomingCallConversation?.getProfileName() ||
|
||||
'Unknown'
|
||||
);
|
||||
|
||||
if (!isBecauseOfCallPermission) {
|
||||
ToastUtils.pushedMissedCall(
|
||||
incomingCallConversation?.getNickname() ||
|
||||
incomingCallConversation?.getProfileName() ||
|
||||
'Unknown'
|
||||
);
|
||||
} else {
|
||||
ToastUtils.pushedMissedCallCauseOfPermission(
|
||||
incomingCallConversation?.getNickname() ||
|
||||
incomingCallConversation?.getProfileName() ||
|
||||
'Unknown'
|
||||
);
|
||||
}
|
||||
|
||||
await incomingCallConversation?.addSingleMessage({
|
||||
conversationId: incomingCallConversation.id,
|
||||
|
|
|
@ -148,15 +148,30 @@ export function pushedMissedCall(conversationName: string) {
|
|||
);
|
||||
}
|
||||
|
||||
export function pushMicAndCameraPermissionNeeded() {
|
||||
const openPrivacySettings = () => {
|
||||
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
|
||||
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
|
||||
};
|
||||
|
||||
export function pushedMissedCallCauseOfPermission(conversationName: string) {
|
||||
const id = 'missedCallPermission';
|
||||
toast.info(
|
||||
<SessionToast
|
||||
title={window.i18n('callMissedTitle')}
|
||||
description={window.i18n('callMissedCausePermission', conversationName)}
|
||||
type={SessionToastType.Info}
|
||||
onToastClick={openPrivacySettings}
|
||||
/>,
|
||||
{ toastId: id, updateId: id, autoClose: 10000 }
|
||||
);
|
||||
}
|
||||
|
||||
export function pushVideoCallPermissionNeeded() {
|
||||
pushToastInfo(
|
||||
'micAndCameraPermissionNeeded',
|
||||
window.i18n('micAndCameraPermissionNeededTitle'),
|
||||
window.i18n('micAndCameraPermissionNeeded'),
|
||||
() => {
|
||||
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
|
||||
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
|
||||
}
|
||||
'videoCallPermissionNeeded',
|
||||
window.i18n('cameraPermissionNeededTitle'),
|
||||
window.i18n('cameraPermissionNeeded'),
|
||||
openPrivacySettings
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue