our video off => show our avatar

pull/2015/head
Audric Ackermann 3 years ago
parent dfa04c68f4
commit d50d7eb803
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -148,8 +148,8 @@
"linkPreviewsTitle": "Send Link Previews", "linkPreviewsTitle": "Send Link Previews",
"linkPreviewDescription": "Previews are supported for most urls", "linkPreviewDescription": "Previews are supported for most urls",
"linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.", "linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.",
"mediaPermissionsTitle": "Microphone and Camera", "mediaPermissionsTitle": "Microphone",
"mediaPermissionsDescription": "Allow access to camera and microphone", "mediaPermissionsDescription": "Allow access to microphone",
"spellCheckTitle": "Spell Check", "spellCheckTitle": "Spell Check",
"spellCheckDescription": "Enable spell check of text entered in message composition box", "spellCheckDescription": "Enable spell check of text entered in message composition box",
"spellCheckDirty": "You must restart Session to apply your new settings", "spellCheckDirty": "You must restart Session to apply your new settings",
@ -439,8 +439,8 @@
"accept": "Accept", "accept": "Accept",
"decline": "Decline", "decline": "Decline",
"endCall": "End call", "endCall": "End call",
"micAndCameraPermissionNeededTitle": "Camera and Microphone access required", "cameraPermissionNeededTitle": "Voice/Video Call permissions required",
"micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy", "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.",
"unableToCall": "cancel your ongoing call first", "unableToCall": "cancel your ongoing call first",
"unableToCallTitle": "Cannot start new call", "unableToCallTitle": "Cannot start new call",
"callMissed": "Missed call from $name$", "callMissed": "Missed call from $name$",
@ -449,6 +449,7 @@
"noCameraFound": "No camera found", "noCameraFound": "No camera found",
"noAudioInputFound": "No audio input found", "noAudioInputFound": "No audio input found",
"callMediaPermissionsTitle": "Voice and video calls", "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", "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." "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} rtl={false}
pauseOnFocusLoss={false} pauseOnFocusLoss={false}
draggable={false} draggable={false}
pauseOnHover={false} pauseOnHover={true}
transition={Slide} transition={Slide}
limit={5} limit={5}
/> />

@ -15,6 +15,7 @@ import {
import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { openConversationWithMessages } from '../../../state/ducks/conversations';
import { Avatar, AvatarSize } from '../../Avatar'; import { Avatar, AvatarSize } from '../../Avatar';
import { getConversationController } from '../../../session/conversations'; import { getConversationController } from '../../../session/conversations';
import { CallManagerOptionsType } from '../../../session/utils/CallManager';
export const DraggableCallWindow = styled.div` export const DraggableCallWindow = styled.div`
position: absolute; position: absolute;
@ -28,11 +29,11 @@ export const DraggableCallWindow = styled.div`
border: var(--session-border); border: var(--session-border);
`; `;
export const StyledVideoElement = styled.video<{ isRemoteVideoMuted: boolean }>` export const StyledVideoElement = styled.video<{ isVideoMuted: boolean }>`
padding: 0 1rem; padding: 0 1rem;
height: 100%; height: 100%;
width: 100%; width: 100%;
opacity: ${props => (props.isRemoteVideoMuted ? 0 : 1)}; opacity: ${props => (props.isVideoMuted ? 0 : 1)};
`; `;
const StyledDraggableVideoElement = styled(StyledVideoElement)` const StyledDraggableVideoElement = styled(StyledVideoElement)`
@ -97,16 +98,10 @@ export const DraggableCallContainer = () => {
useEffect(() => { useEffect(() => {
if (ongoingCallPubkey !== selectedConversationKey) { if (ongoingCallPubkey !== selectedConversationKey) {
CallManager.setVideoEventsListener( CallManager.setVideoEventsListener(
( ({ isRemoteVideoStreamMuted, remoteStream }: CallManagerOptionsType) => {
_localStream: MediaStream | null,
remoteStream: MediaStream | null,
_camerasList: any,
_audioList: any,
remoteVideoIsMuted: boolean
) => {
if (mountedState() && videoRefRemote?.current) { if (mountedState() && videoRefRemote?.current) {
videoRefRemote.current.srcObject = remoteStream; videoRefRemote.current.srcObject = remoteStream;
setIsRemoteVideoMuted(remoteVideoIsMuted); setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
} }
} }
); );
@ -157,7 +152,7 @@ export const DraggableCallContainer = () => {
<StyledDraggableVideoElement <StyledDraggableVideoElement
ref={videoRefRemote} ref={videoRefRemote}
autoPlay={true} autoPlay={true}
isRemoteVideoMuted={isRemoteVideoMuted} isVideoMuted={isRemoteVideoMuted}
/> />
{isRemoteVideoMuted && ( {isRemoteVideoMuted && (
<CenteredAvatarInDraggable> <CenteredAvatarInDraggable>

@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import useMountedState from 'react-use/lib/useMountedState'; import useMountedState from 'react-use/lib/useMountedState';
import styled from 'styled-components'; import styled from 'styled-components';
import _ from 'underscore'; import _ from 'underscore';
import { CallManager, ToastUtils } from '../../../session/utils'; import { CallManager, ToastUtils, UserUtils } from '../../../session/utils';
import { import {
getHasOngoingCall, getHasOngoingCall,
getHasOngoingCallWith, getHasOngoingCallWith,
@ -13,7 +13,7 @@ import {
} from '../../../state/selectors/conversations'; } from '../../../state/selectors/conversations';
import { SessionIconButton } from '../icon'; import { SessionIconButton } from '../icon';
import { animation, contextMenu, Item, Menu } from 'react-contexify'; 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 { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton';
import { StyledVideoElement } from './CallContainer'; import { StyledVideoElement } from './CallContainer';
import { Avatar, AvatarSize } from '../../Avatar'; import { Avatar, AvatarSize } from '../../Avatar';
@ -124,8 +124,9 @@ const AudioInputMenu = ({
}; };
const CenteredAvatarInConversation = styled.div` const CenteredAvatarInConversation = styled.div`
position: absolute; top: -50%;
top: 0; transform: translateY(-50%);
position: relative;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 50%; right: 50%;
@ -151,39 +152,50 @@ export const InConversationCallContainer = () => {
const videoRefLocal = useRef<any>(); const videoRefLocal = useRef<any>();
const mountedState = useMountedState(); const mountedState = useMountedState();
const [isVideoMuted, setVideoMuted] = useState(true); const [isLocalVideoMuted, setLocalVideoMuted] = useState(true);
const [isRemoteVideoMuted, setIsRemoteVideoMuted] = useState(true); const [isRemoteVideoMuted, setIsRemoteVideoMuted] = useState(true);
const [isAudioMuted, setAudioMuted] = useState(false); const [isAudioMuted, setAudioMuted] = useState(false);
const videoTriggerId = 'video-menu-trigger-id'; const videoTriggerId = 'video-menu-trigger-id';
const audioTriggerId = 'audio-menu-trigger-id'; const audioTriggerId = 'audio-menu-trigger-id';
const avatarPath = ongoingCallPubkey const remoteAvatarPath = ongoingCallPubkey
? getConversationController() ? getConversationController()
.get(ongoingCallPubkey) .get(ongoingCallPubkey)
.getAvatarPath() .getAvatarPath()
: undefined; : undefined;
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
const ourUsername = getConversationController()
.get(ourPubkey)
.getProfileName();
const ourAvatarPath = getConversationController()
.get(ourPubkey)
.getAvatarPath();
useEffect(() => { useEffect(() => {
if (ongoingCallPubkey === selectedConversationKey) { if (ongoingCallPubkey === selectedConversationKey) {
CallManager.setVideoEventsListener( CallManager.setVideoEventsListener((options: CallManagerOptionsType) => {
( const {
localStream: MediaStream | null, audioInputsList,
remoteStream: MediaStream | null, camerasList,
camerasList: Array<InputItem>, isLocalVideoStreamMuted,
audioInputList: Array<InputItem>, isRemoteVideoStreamMuted,
isRemoteVideoStreamMuted: boolean localStream,
) => { remoteStream,
if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) { } = options;
videoRefLocal.current.srcObject = localStream; if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) {
setIsRemoteVideoMuted(isRemoteVideoStreamMuted); videoRefLocal.current.srcObject = localStream;
videoRefRemote.current.srcObject = remoteStream; setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
setLocalVideoMuted(isLocalVideoStreamMuted);
setCurrentConnectedCameras(camerasList); videoRefRemote.current.srcObject = remoteStream;
setCurrentConnectedAudioInputs(audioInputList);
} setCurrentConnectedCameras(camerasList);
setCurrentConnectedAudioInputs(audioInputsList);
} }
); });
} }
return () => { return () => {
@ -204,14 +216,14 @@ export const InConversationCallContainer = () => {
return; return;
} }
if (isVideoMuted) { if (isLocalVideoMuted) {
// select the first one // select the first one
await CallManager.selectCameraByDeviceId(currentConnectedCameras[0].deviceId); await CallManager.selectCameraByDeviceId(currentConnectedCameras[0].deviceId);
} else { } else {
await CallManager.selectCameraByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID); await CallManager.selectCameraByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID);
} }
setVideoMuted(!isVideoMuted); setLocalVideoMuted(!isLocalVideoMuted);
}; };
const handleMicrophoneToggle = async () => { const handleMicrophoneToggle = async () => {
@ -263,13 +275,13 @@ export const InConversationCallContainer = () => {
<StyledVideoElement <StyledVideoElement
ref={videoRefRemote} ref={videoRefRemote}
autoPlay={true} autoPlay={true}
isRemoteVideoMuted={isRemoteVideoMuted} isVideoMuted={isRemoteVideoMuted}
/> />
{isRemoteVideoMuted && ( {isRemoteVideoMuted && (
<CenteredAvatarInConversation> <CenteredAvatarInConversation>
<Avatar <Avatar
size={AvatarSize.XL} size={AvatarSize.XL}
avatarPath={avatarPath} avatarPath={remoteAvatarPath}
name={ongoingCallUsername} name={ongoingCallUsername}
pubkey={ongoingCallPubkey} pubkey={ongoingCallPubkey}
/> />
@ -281,8 +293,18 @@ export const InConversationCallContainer = () => {
ref={videoRefLocal} ref={videoRefLocal}
autoPlay={true} autoPlay={true}
muted={true} muted={true}
isRemoteVideoMuted={false} isVideoMuted={isLocalVideoMuted}
/> />
{isLocalVideoMuted && (
<CenteredAvatarInConversation>
<Avatar
size={AvatarSize.XL}
avatarPath={ourAvatarPath}
name={ourUsername}
pubkey={ourPubkey}
/>
</CenteredAvatarInConversation>
)}
</VideoContainer> </VideoContainer>
<InConvoCallWindowControls> <InConvoCallWindowControls>
@ -298,7 +320,7 @@ export const InConversationCallContainer = () => {
/> />
<DropDownAndToggleButton <DropDownAndToggleButton
iconType="camera" iconType="camera"
isMuted={isVideoMuted} isMuted={isLocalVideoMuted}
onMainButtonClick={handleCameraToggle} onMainButtonClick={handleCameraToggle}
onArrowClick={showVideoInputMenu} onArrowClick={showVideoInputMenu}
/> />
@ -312,7 +334,7 @@ export const InConversationCallContainer = () => {
<VideoInputMenu <VideoInputMenu
triggerId={videoTriggerId} triggerId={videoTriggerId}
onUnmute={() => { onUnmute={() => {
setVideoMuted(false); setLocalVideoMuted(false);
}} }}
camerasList={currentConnectedCameras} camerasList={currentConnectedCameras}
/> />

@ -373,7 +373,7 @@ export function getStartCallMenuItem(conversationId: string): JSX.Element | null
} }
if (!getCallMediaPermissionsSettings()) { if (!getCallMediaPermissionsSettings()) {
ToastUtils.pushMicAndCameraPermissionNeeded(); ToastUtils.pushVideoCallPermissionNeeded();
return; return;
} }

@ -22,26 +22,28 @@ export type InputItem = { deviceId: string; label: string };
// const VIDEO_WIDTH = 640; // const VIDEO_WIDTH = 640;
// const VIDEO_RATIO = 16 / 9; // const VIDEO_RATIO = 16 / 9;
type CallManagerListener = export type CallManagerOptionsType = {
| (( localStream: MediaStream | null;
localStream: MediaStream | null, remoteStream: MediaStream | null;
remoteStream: MediaStream | null, camerasList: Array<InputItem>;
camerasList: Array<InputItem>, audioInputsList: Array<InputItem>;
audioInputsList: Array<InputItem>, isLocalVideoStreamMuted: boolean;
isRemoteVideoStreamMuted: boolean isRemoteVideoStreamMuted: boolean;
) => void) };
| null;
export type CallManagerListener = ((options: CallManagerOptionsType) => void) | null;
let videoEventsListener: CallManagerListener; let videoEventsListener: CallManagerListener;
function callVideoListener() { function callVideoListener() {
if (videoEventsListener) { if (videoEventsListener) {
videoEventsListener( videoEventsListener({
mediaDevices, localStream: mediaDevices,
remoteStream, remoteStream,
camerasList, camerasList,
audioInputsList, audioInputsList,
remoteVideoStreamIsMuted isRemoteVideoStreamMuted: remoteVideoStreamIsMuted,
); isLocalVideoStreamMuted: selectedCameraId === INPUT_DISABLED_DEVICE_ID,
});
} }
} }
@ -79,7 +81,7 @@ const configuration: RTCConfiguration = {
iceTransportPolicy: 'relay', iceTransportPolicy: 'relay',
}; };
let selectedCameraId: string | undefined; let selectedCameraId: string = INPUT_DISABLED_DEVICE_ID;
let selectedAudioInputId: string | undefined; let selectedAudioInputId: string | undefined;
let camerasList: Array<InputItem> = []; let camerasList: Array<InputItem> = [];
let audioInputsList: Array<InputItem> = []; let audioInputsList: Array<InputItem> = [];
@ -115,8 +117,7 @@ async function updateInputLists() {
} }
function sendVideoStatusViaDataChannel() { function sendVideoStatusViaDataChannel() {
const videoEnabledLocally = const videoEnabledLocally = selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
selectedCameraId !== undefined && selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
const stringToSend = JSON.stringify({ const stringToSend = JSON.stringify({
video: videoEnabledLocally, video: videoEnabledLocally,
}); });
@ -127,7 +128,7 @@ function sendVideoStatusViaDataChannel() {
export async function selectCameraByDeviceId(cameraDeviceId: string) { export async function selectCameraByDeviceId(cameraDeviceId: string) {
if (cameraDeviceId === INPUT_DISABLED_DEVICE_ID) { if (cameraDeviceId === INPUT_DISABLED_DEVICE_ID) {
selectedCameraId = cameraDeviceId; selectedCameraId = INPUT_DISABLED_DEVICE_ID;
const sender = peerConnection?.getSenders().find(s => { const sender = peerConnection?.getSenders().find(s => {
return s.track?.kind === 'video'; return s.track?.kind === 'video';
@ -136,6 +137,7 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
sender.track.enabled = false; sender.track.enabled = false;
} }
sendVideoStatusViaDataChannel(); sendVideoStatusViaDataChannel();
callVideoListener();
return; return;
} }
if (camerasList.some(m => m.deviceId === cameraDeviceId)) { if (camerasList.some(m => m.deviceId === cameraDeviceId)) {
@ -164,12 +166,15 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
mediaDevices?.removeTrack(t); mediaDevices?.removeTrack(t);
}); });
mediaDevices?.addTrack(videoTrack); mediaDevices?.addTrack(videoTrack);
sendVideoStatusViaDataChannel(); sendVideoStatusViaDataChannel();
callVideoListener();
} else { } else {
throw new Error('Failed to get sender for selectCameraByDeviceId '); throw new Error('Failed to get sender for selectCameraByDeviceId ');
} }
} catch (e) { } catch (e) {
window.log.warn('selectCameraByDeviceId failed with', e.message); window.log.warn('selectCameraByDeviceId failed with', e.message);
callVideoListener();
} }
} }
} }
@ -301,7 +306,7 @@ async function openMediaDevicesAndAddTracks() {
} }
}); });
} catch (err) { } catch (err) {
ToastUtils.pushMicAndCameraPermissionNeeded(); ToastUtils.pushVideoCallPermissionNeeded();
closeVideoCall(); closeVideoCall();
} }
callVideoListener(); callVideoListener();
@ -310,7 +315,7 @@ async function openMediaDevicesAndAddTracks() {
// tslint:disable-next-line: function-name // tslint:disable-next-line: function-name
export async function USER_callRecipient(recipient: string) { export async function USER_callRecipient(recipient: string) {
if (!getCallMediaPermissionsSettings()) { if (!getCallMediaPermissionsSettings()) {
ToastUtils.pushMicAndCameraPermissionNeeded(); ToastUtils.pushVideoCallPermissionNeeded();
return; return;
} }
await updateInputLists(); await updateInputLists();
@ -420,8 +425,16 @@ function closeVideoCall() {
mediaDevices = null; mediaDevices = null;
remoteStream = null; remoteStream = null;
selectedCameraId = INPUT_DISABLED_DEVICE_ID;
if (videoEventsListener) { 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) { if (callingConvos.length === 1 && callingConvos[0].id === sender) {
closeVideoCall(); closeVideoCall();
if (videoEventsListener) { if (videoEventsListener) {
videoEventsListener(null, null, [], [], true); videoEventsListener({
audioInputsList: [],
camerasList: [],
isLocalVideoStreamMuted: true,
isRemoteVideoStreamMuted: true,
localStream: null,
remoteStream: null,
});
} }
window.inboxStore?.dispatch(endCall({ pubkey: sender })); window.inboxStore?.dispatch(endCall({ pubkey: sender }));
} }
@ -633,21 +653,20 @@ export async function handleCallTypeOffer(
const convos = getConversationController().getConversations(); const convos = getConversationController().getConversations();
const callingConvos = convos.filter(convo => convo.callState !== undefined); const callingConvos = convos.filter(convo => convo.callState !== undefined);
if (!getCallMediaPermissionsSettings()) {
await handleMissedCall(sender, incomingOfferTimestamp, true);
return;
}
if (callingConvos.length > 0) { if (callingConvos.length > 0) {
// we just got a new offer from someone we are NOT already in a call with // we just got a new offer from someone we are NOT already in a call with
if (callingConvos.length !== 1 || callingConvos[0].id !== sender) { if (callingConvos.length !== 1 || callingConvos[0].id !== sender) {
await handleMissedCall(sender, incomingOfferTimestamp); await handleMissedCall(sender, incomingOfferTimestamp, false);
return; return;
} }
} }
if (!getCallMediaPermissionsSettings()) {
await handleMissedCall(sender, incomingOfferTimestamp);
// TODO audric show where to turn it on
throw new Error('TODO AUDRIC');
return;
}
const readyForOffer = const readyForOffer =
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending); !makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp; const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
@ -672,6 +691,7 @@ export async function handleCallTypeOffer(
await buildAnswerAndSendIt(sender); await buildAnswerAndSendIt(sender);
} }
} }
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
// don't need to do the sending here as we dispatch an answer in a // don't need to do the sending here as we dispatch an answer in a
} catch (err) { } catch (err) {
@ -682,16 +702,28 @@ export async function handleCallTypeOffer(
callCache.set(sender, new Array()); callCache.set(sender, new Array());
} }
callCache.get(sender)?.push(callMessage); 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); const incomingCallConversation = await getConversationById(sender);
ToastUtils.pushedMissedCall(
incomingCallConversation?.getNickname() || if (!isBecauseOfCallPermission) {
incomingCallConversation?.getProfileName() || ToastUtils.pushedMissedCall(
'Unknown' incomingCallConversation?.getNickname() ||
); incomingCallConversation?.getProfileName() ||
'Unknown'
);
} else {
ToastUtils.pushedMissedCallCauseOfPermission(
incomingCallConversation?.getNickname() ||
incomingCallConversation?.getProfileName() ||
'Unknown'
);
}
await incomingCallConversation?.addSingleMessage({ await incomingCallConversation?.addSingleMessage({
conversationId: incomingCallConversation.id, 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( pushToastInfo(
'micAndCameraPermissionNeeded', 'videoCallPermissionNeeded',
window.i18n('micAndCameraPermissionNeededTitle'), window.i18n('cameraPermissionNeededTitle'),
window.i18n('micAndCameraPermissionNeeded'), window.i18n('cameraPermissionNeeded'),
() => { openPrivacySettings
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
}
); );
} }

Loading…
Cancel
Save