From 99b139f5c46169315b0397bc7598cf443d31ac1f Mon Sep 17 00:00:00 2001
From: Audric Ackermann <audric@loki.network>
Date: Thu, 21 Oct 2021 13:18:50 +1100
Subject: [PATCH] split settings folder and start adding the settings for video
 call

---
 _locales/en/messages.json                     |   5 +-
 js/background.js                              |  28 +
 main.js                                       |  22 +-
 preload.js                                    |   9 +-
 .../session/LeftPaneSettingSection.tsx        |   2 +-
 ts/components/session/SessionToggle.tsx       |  11 +-
 .../conversation/SessionCompositionBox.tsx    |   5 +-
 ts/components/session/menu/Menu.tsx           |   6 +
 .../session/settings/BlockedUserSettings.ts   |  58 ++
 .../session/settings/LocalSettings.ts         | 388 +++++++++++++
 .../settings/SessionSettingListItem.tsx       |   2 +-
 .../session/settings/SessionSettings.tsx      | 532 +++---------------
 ts/session/onions/onionPath.ts                |  23 +-
 ts/session/utils/CallManager.ts               |  22 +-
 ts/session/utils/Toast.tsx                    |   9 +-
 ts/state/ducks/section.tsx                    |   2 +-
 ts/state/selectors/section.ts                 |   2 +-
 ts/window.d.ts                                |   4 +-
 18 files changed, 633 insertions(+), 497 deletions(-)
 create mode 100644 ts/components/session/settings/BlockedUserSettings.ts
 create mode 100644 ts/components/session/settings/LocalSettings.ts

diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 9e0bf53cb..4f08617ae 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -456,5 +456,8 @@
   "callMissedTitle": "Call missed",
   "startVideoCall": "Start Video Call",
   "noCameraFound": "No camera found",
-  "noAudioInputFound": "No audio input found"
+  "noAudioInputFound": "No audio input found",
+  "callMediaPermissionsTitle": "Voice and video calls",
+  "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."
 }
diff --git a/js/background.js b/js/background.js
index f7250e14d..ca8d8d190 100644
--- a/js/background.js
+++ b/js/background.js
@@ -321,12 +321,40 @@
 
     window.toggleMediaPermissions = async () => {
       const value = window.getMediaPermissions();
+
+      if (value === true) {
+        const valueCallPermissions = window.getCallMediaPermissions();
+        if (valueCallPermissions) {
+          window.log.info('toggleMediaPermissions : forcing callPermissions to false');
+
+          window.toggleCallMediaPermissionsTo(false);
+        }
+      }
+
       if (value === false && Signal.OS.isMacOS()) {
         await window.askForMediaAccess();
       }
       window.setMediaPermissions(!value);
     };
 
+    window.toggleCallMediaPermissionsTo = async enabled => {
+      const previousValue = window.getCallMediaPermissions();
+      if (previousValue === enabled) {
+        return;
+      }
+      if (previousValue === false) {
+        // value was false and we toggle it so we turn it on
+        if (Signal.OS.isMacOS()) {
+          await window.askForMediaAccess();
+        }
+        window.log.info('toggleCallMediaPermissionsTo : forcing audio/video to true');
+        // turning ON "call permissions" forces turning on "audio/video permissions"
+        window.setMediaPermissions(true);
+      }
+      console.warn('toggling toggleCallMediaPermissionsTo to ', enabled);
+      window.setCallMediaPermissions(enabled);
+    };
+
     Whisper.Notifications.on('click', async (id, messageId) => {
       window.showWindow();
       if (id) {
diff --git a/main.js b/main.js
index e6c44f877..7dd77022f 100644
--- a/main.js
+++ b/main.js
@@ -949,19 +949,35 @@ ipc.on('close-debug-log', () => {
 
 // This should be called with an ipc sendSync
 ipc.on('get-media-permissions', event => {
+  console.warn('get-media-permissions', userConfig.get('mediaPermissions'));
+
   // eslint-disable-next-line no-param-reassign
   event.returnValue = userConfig.get('mediaPermissions') || false;
 });
 ipc.on('set-media-permissions', (event, value) => {
   userConfig.set('mediaPermissions', value);
+  console.warn('set-media-permissions', value);
 
   // We reinstall permissions handler to ensure that a revoked permission takes effect
   installPermissionsHandler({ session, userConfig });
 
   event.sender.send('set-success-media-permissions', null);
-  if (mainWindow && mainWindow.webContents) {
-    mainWindow.webContents.send('mediaPermissionsChanged');
-  }
+});
+
+// This should be called with an ipc sendSync
+ipc.on('get-call-media-permissions', event => {
+  console.warn('get-call-media-permissions', userConfig.get('callMediaPermissions'));
+  // eslint-disable-next-line no-param-reassign
+  event.returnValue = userConfig.get('callMediaPermissions') || false;
+});
+ipc.on('set-call-media-permissions', (event, value) => {
+  userConfig.set('callMediaPermissions', value);
+  console.warn('set-call-media-permissions', value);
+
+  // We reinstall permissions handler to ensure that a revoked permission takes effect
+  installPermissionsHandler({ session, userConfig });
+
+  event.sender.send('set-success-call-media-permissions', null);
 });
 
 // Loki - Auto updating
diff --git a/preload.js b/preload.js
index c753c72d4..e528d678c 100644
--- a/preload.js
+++ b/preload.js
@@ -145,10 +145,6 @@ window.restart = () => {
   ipc.send('restart');
 };
 
-ipc.on('mediaPermissionsChanged', () => {
-  Whisper.events.trigger('mediaPermissionsChanged');
-});
-
 window.closeAbout = () => ipc.send('close-about');
 window.readyForUpdates = () => ipc.send('ready-for-updates');
 
@@ -165,6 +161,8 @@ window.getSettingValue = (settingID, comparisonValue = null) => {
   // We need to get specific settings from the main process
   if (settingID === 'media-permissions') {
     return window.getMediaPermissions();
+  } else if (settingID === 'call-media-permissions') {
+    return window.getCallMediaPermissions();
   } else if (settingID === 'auto-update') {
     return window.getAutoUpdateEnabled();
   }
@@ -192,6 +190,9 @@ window.setSettingValue = (settingID, value) => {
 window.getMediaPermissions = () => ipc.sendSync('get-media-permissions');
 window.setMediaPermissions = value => ipc.send('set-media-permissions', !!value);
 
+window.getCallMediaPermissions = () => ipc.sendSync('get-call-media-permissions');
+window.setCallMediaPermissions = value => ipc.send('set-call-media-permissions', !!value);
+
 window.askForMediaAccess = () => ipc.send('media-access');
 
 // Auto update setting
diff --git a/ts/components/session/LeftPaneSettingSection.tsx b/ts/components/session/LeftPaneSettingSection.tsx
index 42c1d8a08..011b00d6e 100644
--- a/ts/components/session/LeftPaneSettingSection.tsx
+++ b/ts/components/session/LeftPaneSettingSection.tsx
@@ -2,12 +2,12 @@ import React from 'react';
 import classNames from 'classnames';
 import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
 import { SessionIcon } from './icon';
-import { SessionSettingCategory } from './settings/SessionSettings';
 import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
 import { useDispatch, useSelector } from 'react-redux';
 import { showSettingsSection } from '../../state/ducks/section';
 import { getFocusedSettingsSection } from '../../state/selectors/section';
 import { recoveryPhraseModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog';
+import { SessionSettingCategory } from './settings/LocalSettings';
 
 const getCategories = () => {
   return [
diff --git a/ts/components/session/SessionToggle.tsx b/ts/components/session/SessionToggle.tsx
index 9afc88164..231488ac3 100644
--- a/ts/components/session/SessionToggle.tsx
+++ b/ts/components/session/SessionToggle.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 import classNames from 'classnames';
 import { updateConfirmModal } from '../../state/ducks/modalDialog';
 import { useDispatch } from 'react-redux';
@@ -10,17 +10,10 @@ type Props = {
 };
 
 export const SessionToggle = (props: Props) => {
-  const [active, setActive] = useState(false);
-
   const dispatch = useDispatch();
 
-  useEffect(() => {
-    setActive(props.active);
-  }, []);
-
   const clickHandler = (event: any) => {
     const stateManager = (e: any) => {
-      setActive(!active);
       e.stopPropagation();
       props.onClick();
     };
@@ -53,7 +46,7 @@ export const SessionToggle = (props: Props) => {
 
   return (
     <div
-      className={classNames('session-toggle', active ? 'active' : '')}
+      className={classNames('session-toggle', props.active ? 'active' : '')}
       role="button"
       onClick={clickHandler}
     >
diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx
index 2235d84af..27f2d87f6 100644
--- a/ts/components/session/conversation/SessionCompositionBox.tsx
+++ b/ts/components/session/conversation/SessionCompositionBox.tsx
@@ -28,7 +28,7 @@ import { getConversationController } from '../../../session/conversations';
 import { ReduxConversationType } from '../../../state/ducks/conversations';
 import { SessionMemberListItem } from '../SessionMemberListItem';
 import autoBind from 'auto-bind';
-import { SessionSettingCategory } from '../settings/SessionSettings';
+import { getMediaPermissionsSettings } from '../settings/SessionSettings';
 import { updateConfirmModal } from '../../../state/ducks/modalDialog';
 import {
   SectionType,
@@ -51,6 +51,7 @@ import { connect } from 'react-redux';
 import { StateType } from '../../../state/reducer';
 import { getTheme } from '../../../state/selectors/theme';
 import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
+import { SessionSettingCategory } from '../settings/LocalSettings';
 
 export interface ReplyingToMessageProps {
   convoId: string;
@@ -999,7 +1000,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
 
   private async onLoadVoiceNoteView() {
     // Do stuff for component, then run callback to SessionConversation
-    const mediaSetting = await window.getSettingValue('media-permissions');
+    const mediaSetting = getMediaPermissionsSettings();
 
     if (mediaSetting) {
       this.setState({
diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx
index a781c8355..6e9b569c5 100644
--- a/ts/components/session/menu/Menu.tsx
+++ b/ts/components/session/menu/Menu.tsx
@@ -33,6 +33,7 @@ import {
 import { SessionButtonColor } from '../SessionButton';
 import { getTimerOptions } from '../../../state/selectors/timerOptions';
 import { CallManager, ToastUtils } from '../../../session/utils';
+import { getCallMediaPermissionsSettings } from '../settings/SessionSettings';
 
 const maxNumberOfPinnedConversations = 5;
 
@@ -342,6 +343,11 @@ export function getStartCallMenuItem(conversationId: string): JSX.Element | null
             return;
           }
 
+          if (!getCallMediaPermissionsSettings()) {
+            ToastUtils.pushMicAndCameraPermissionNeeded();
+            return;
+          }
+
           if (convo) {
             convo.callState = 'connecting';
             await convo.commit();
diff --git a/ts/components/session/settings/BlockedUserSettings.ts b/ts/components/session/settings/BlockedUserSettings.ts
new file mode 100644
index 000000000..dd74609ae
--- /dev/null
+++ b/ts/components/session/settings/BlockedUserSettings.ts
@@ -0,0 +1,58 @@
+import { unblockConvoById } from '../../../interactions/conversationInteractions';
+import { getConversationController } from '../../../session/conversations';
+import { BlockedNumberController } from '../../../util';
+import { SessionButtonColor } from '../SessionButton';
+import { LocalSettingType, SessionSettingType, SessionSettingCategory } from './LocalSettings';
+
+export function getBlockedUserSettings(): Array<LocalSettingType> {
+  const results: Array<LocalSettingType> = [];
+  const blockedNumbers = BlockedNumberController.getBlockedNumbers();
+
+  for (const blockedNumber of blockedNumbers) {
+    let title: string;
+
+    const currentModel = getConversationController().get(blockedNumber);
+    if (currentModel) {
+      title = currentModel.getProfileName() || currentModel.getName() || window.i18n('anonymous');
+    } else {
+      title = window.i18n('anonymous');
+    }
+
+    results.push({
+      id: blockedNumber,
+      title,
+      description: '',
+      type: SessionSettingType.Button,
+      category: SessionSettingCategory.Blocked,
+      content: {
+        buttonColor: SessionButtonColor.Danger,
+        buttonText: window.i18n('unblockUser'),
+      },
+      comparisonValue: undefined,
+      setFn: async () => {
+        await unblockConvoById(blockedNumber);
+      },
+      hidden: false,
+      onClick: undefined,
+    });
+  }
+
+  if (blockedNumbers.length === 0) {
+    return [
+      {
+        id: 'noBlockedContacts',
+        title: '',
+        description: window.i18n('noBlockedContacts'),
+        type: undefined,
+        category: SessionSettingCategory.Blocked,
+        content: undefined,
+        comparisonValue: undefined,
+        setFn: undefined,
+        hidden: false,
+        onClick: undefined,
+      },
+    ];
+  }
+
+  return results;
+}
diff --git a/ts/components/session/settings/LocalSettings.ts b/ts/components/session/settings/LocalSettings.ts
new file mode 100644
index 000000000..f42167af0
--- /dev/null
+++ b/ts/components/session/settings/LocalSettings.ts
@@ -0,0 +1,388 @@
+import { shell } from 'electron';
+import { createOrUpdateItem, hasLinkPreviewPopupBeenDisplayed } from '../../../data/data';
+import { ToastUtils } from '../../../session/utils';
+import { sessionPassword, updateConfirmModal } from '../../../state/ducks/modalDialog';
+import { toggleAudioAutoplay } from '../../../state/ducks/userConfig';
+import { PasswordAction } from '../../dialog/SessionPasswordDialog';
+import { SessionButtonColor } from '../SessionButton';
+
+export enum SessionSettingCategory {
+  Appearance = 'appearance',
+  Account = 'account',
+  Privacy = 'privacy',
+  Permissions = 'permissions',
+  Notifications = 'notifications',
+  Blocked = 'blocked',
+}
+
+export enum SessionSettingType {
+  Toggle = 'toggle',
+  Options = 'options',
+  Button = 'button',
+  Slider = 'slider',
+}
+
+export type LocalSettingType = {
+  category: SessionSettingCategory;
+  description: string | undefined;
+  comparisonValue: string | undefined;
+  id: any;
+  value?: any;
+  content: any | undefined;
+  hidden: any;
+  title?: string;
+  type: SessionSettingType | undefined;
+  setFn: any;
+  onClick: any;
+};
+
+function setNotificationSetting(settingID: string, selectedValue: string) {
+  window.setSettingValue(settingID, selectedValue);
+}
+
+function displayPasswordModal(
+  passwordAction: PasswordAction,
+  onPasswordUpdated: (action: string) => void
+) {
+  window.inboxStore?.dispatch(
+    sessionPassword({
+      passwordAction,
+      onOk: () => {
+        onPasswordUpdated(passwordAction);
+      },
+    })
+  );
+}
+
+// tslint:disable-next-line: max-func-body-length
+export function getLocalSettings(
+  hasPassword: boolean | null,
+  onPasswordUpdated: (action: string) => void,
+  forceUpdate: () => void
+): Array<LocalSettingType> {
+  const { Settings } = window.Signal.Types;
+
+  return [
+    {
+      id: 'hide-menu-bar',
+      title: window.i18n('hideMenuBarTitle'),
+      description: window.i18n('hideMenuBarDescription'),
+      hidden: !Settings.isHideMenuBarSupported(),
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Appearance,
+      setFn: window.toggleMenuBar,
+      content: { defaultValue: true },
+      comparisonValue: undefined,
+      onClick: undefined,
+    },
+    {
+      id: 'spell-check',
+      title: window.i18n('spellCheckTitle'),
+      description: window.i18n('spellCheckDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Appearance,
+      setFn: window.toggleSpellCheck,
+      content: { defaultValue: true },
+      comparisonValue: undefined,
+      onClick: undefined,
+    },
+    {
+      id: 'link-preview-setting',
+      title: window.i18n('linkPreviewsTitle'),
+      description: window.i18n('linkPreviewDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Appearance,
+      setFn: async () => {
+        const newValue = !window.getSettingValue('link-preview-setting');
+        window.setSettingValue('link-preview-setting', newValue);
+        if (!newValue) {
+          await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: false });
+        } else {
+          window.inboxStore?.dispatch(
+            updateConfirmModal({
+              title: window.i18n('linkPreviewsTitle'),
+              message: window.i18n('linkPreviewsConfirmMessage'),
+              okTheme: SessionButtonColor.Danger,
+              // onClickOk:
+            })
+          );
+        }
+      },
+      content: undefined,
+      comparisonValue: undefined,
+      onClick: undefined,
+    },
+
+    {
+      id: 'start-in-tray-setting',
+      title: window.i18n('startInTrayTitle'),
+      description: window.i18n('startInTrayDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Appearance,
+      setFn: async () => {
+        try {
+          const newValue = !(await window.getStartInTray());
+
+          // make sure to write it here too, as this is the value used on the UI to mark the toggle as true/false
+          window.setSettingValue('start-in-tray-setting', newValue);
+          await window.setStartInTray(newValue);
+          if (!newValue) {
+            ToastUtils.pushRestartNeeded();
+          }
+        } catch (e) {
+          window.log.warn('start in tray change error:', e);
+        }
+      },
+      content: undefined,
+      comparisonValue: undefined,
+      onClick: undefined,
+    },
+    {
+      id: 'audio-message-autoplay-setting',
+      title: window.i18n('audioMessageAutoplayTitle'),
+      description: window.i18n('audioMessageAutoplayDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Appearance,
+      setFn: () => {
+        window.inboxStore?.dispatch(toggleAudioAutoplay());
+      },
+      content: {
+        defaultValue: window.inboxStore?.getState().userConfig.audioAutoplay,
+      },
+      comparisonValue: undefined,
+      onClick: undefined,
+    },
+
+    {
+      id: 'notification-setting',
+      title: window.i18n('notificationSettingsDialog'),
+      type: SessionSettingType.Options,
+      category: SessionSettingCategory.Notifications,
+      comparisonValue: undefined,
+      description: undefined,
+      hidden: undefined,
+      onClick: undefined,
+      setFn: (selectedValue: string) => {
+        setNotificationSetting('notification-setting', selectedValue);
+        forceUpdate();
+      },
+      content: {
+        options: {
+          group: 'notification-setting',
+          initialItem: window.getSettingValue('notification-setting') || 'message',
+          items: [
+            {
+              label: window.i18n('nameAndMessage'),
+              value: 'message',
+            },
+            {
+              label: window.i18n('nameOnly'),
+              value: 'name',
+            },
+            {
+              label: window.i18n('noNameOrMessage'),
+              value: 'count',
+            },
+            {
+              label: window.i18n('disableNotifications'),
+              value: 'off',
+            },
+          ],
+        },
+      },
+    },
+    {
+      id: 'zoom-factor-setting',
+      title: window.i18n('zoomFactorSettingTitle'),
+      description: undefined,
+      hidden: false,
+      type: SessionSettingType.Slider,
+      category: SessionSettingCategory.Appearance,
+      setFn: undefined,
+      comparisonValue: undefined,
+      onClick: undefined,
+      content: {
+        dotsEnabled: true,
+        step: 20,
+        min: 60,
+        max: 200,
+        defaultValue: 100,
+        info: (value: number) => `${value}%`,
+      },
+    },
+    {
+      id: 'session-survey',
+      title: window.i18n('surveyTitle'),
+      description: undefined,
+      hidden: false,
+      type: SessionSettingType.Button,
+      category: SessionSettingCategory.Appearance,
+      setFn: undefined,
+      comparisonValue: undefined,
+      onClick: () => {
+        void shell.openExternal('https://getsession.org/survey');
+      },
+      content: {
+        buttonText: window.i18n('goToOurSurvey'),
+        buttonColor: SessionButtonColor.Primary,
+      },
+    },
+    {
+      id: 'help-translation',
+      title: window.i18n('translation'),
+      description: undefined,
+      hidden: false,
+      type: SessionSettingType.Button,
+      category: SessionSettingCategory.Appearance,
+      setFn: undefined,
+      comparisonValue: undefined,
+      onClick: () => {
+        void shell.openExternal('https://crowdin.com/project/session-desktop/');
+      },
+      content: {
+        buttonText: window.i18n('helpUsTranslateSession'),
+        buttonColor: SessionButtonColor.Primary,
+      },
+    },
+    {
+      id: 'media-permissions',
+      title: window.i18n('mediaPermissionsTitle'),
+      description: window.i18n('mediaPermissionsDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Privacy,
+      setFn: async () => {
+        await window.toggleMediaPermissions();
+        forceUpdate();
+      },
+      content: undefined,
+      comparisonValue: undefined,
+      onClick: undefined,
+    },
+    {
+      id: 'call-media-permissions',
+      title: window.i18n('callMediaPermissionsTitle'),
+      description: window.i18n('callMediaPermissionsDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Privacy,
+      setFn: async () => {
+        const currentValue = window.getCallMediaPermissions();
+        if (!currentValue) {
+          window.inboxStore?.dispatch(
+            updateConfirmModal({
+              message: window.i18n('callMediaPermissionsDialogContent'),
+              okTheme: SessionButtonColor.Green,
+              onClickOk: async () => {
+                await window.toggleCallMediaPermissionsTo(true);
+                forceUpdate();
+              },
+              onClickCancel: async () => {
+                await window.toggleCallMediaPermissionsTo(false);
+                forceUpdate();
+              },
+            })
+          );
+        } else {
+          await window.toggleCallMediaPermissionsTo(false);
+          forceUpdate();
+        }
+      },
+
+      content: undefined,
+      comparisonValue: undefined,
+      onClick: undefined,
+    },
+    {
+      id: 'read-receipt-setting',
+      title: window.i18n('readReceiptSettingTitle'),
+      description: window.i18n('readReceiptSettingDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Privacy,
+      setFn: undefined,
+      comparisonValue: undefined,
+      onClick: undefined,
+      content: {},
+    },
+    {
+      id: 'typing-indicators-setting',
+      title: window.i18n('typingIndicatorsSettingTitle'),
+      description: window.i18n('typingIndicatorsSettingDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Privacy,
+      setFn: undefined,
+      comparisonValue: undefined,
+      onClick: undefined,
+      content: {},
+    },
+    {
+      id: 'auto-update',
+      title: window.i18n('autoUpdateSettingTitle'),
+      description: window.i18n('autoUpdateSettingDescription'),
+      hidden: false,
+      type: SessionSettingType.Toggle,
+      category: SessionSettingCategory.Privacy,
+      setFn: undefined,
+      comparisonValue: undefined,
+      onClick: undefined,
+      content: {},
+    },
+    {
+      id: 'set-password',
+      title: window.i18n('setAccountPasswordTitle'),
+      description: window.i18n('setAccountPasswordDescription'),
+      hidden: hasPassword,
+      type: SessionSettingType.Button,
+      category: SessionSettingCategory.Privacy,
+      setFn: undefined,
+      comparisonValue: undefined,
+      content: {
+        buttonText: window.i18n('setPassword'),
+        buttonColor: SessionButtonColor.Primary,
+      },
+      onClick: () => {
+        displayPasswordModal('set', onPasswordUpdated);
+      },
+    },
+    {
+      id: 'change-password',
+      title: window.i18n('changeAccountPasswordTitle'),
+      description: window.i18n('changeAccountPasswordDescription'),
+      hidden: !hasPassword,
+      type: SessionSettingType.Button,
+      category: SessionSettingCategory.Privacy,
+      setFn: undefined,
+      comparisonValue: undefined,
+      content: {
+        buttonText: window.i18n('changePassword'),
+        buttonColor: SessionButtonColor.Primary,
+      },
+      onClick: () => {
+        displayPasswordModal('change', onPasswordUpdated);
+      },
+    },
+    {
+      id: 'remove-password',
+      title: window.i18n('removeAccountPasswordTitle'),
+      description: window.i18n('removeAccountPasswordDescription'),
+      hidden: !hasPassword,
+      type: SessionSettingType.Button,
+      category: SessionSettingCategory.Privacy,
+      setFn: undefined,
+      comparisonValue: undefined,
+      content: {
+        buttonText: window.i18n('removePassword'),
+        buttonColor: SessionButtonColor.Danger,
+      },
+      onClick: () => {
+        displayPasswordModal('remove', onPasswordUpdated);
+      },
+    },
+  ];
+}
diff --git a/ts/components/session/settings/SessionSettingListItem.tsx b/ts/components/session/settings/SessionSettingListItem.tsx
index a48e2d829..4f36063cb 100644
--- a/ts/components/session/settings/SessionSettingListItem.tsx
+++ b/ts/components/session/settings/SessionSettingListItem.tsx
@@ -5,9 +5,9 @@ import Slider from 'rc-slider';
 
 import { SessionToggle } from '../SessionToggle';
 import { SessionButton } from '../SessionButton';
-import { SessionSettingType } from './SessionSettings';
 import { SessionRadioGroup } from '../SessionRadioGroup';
 import { SessionConfirmDialogProps } from '../../dialog/SessionConfirm';
+import { SessionSettingType } from './LocalSettings';
 
 type Props = {
   title?: string;
diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx
index 06fb472f6..6db6900ed 100644
--- a/ts/components/session/settings/SessionSettings.tsx
+++ b/ts/components/session/settings/SessionSettings.tsx
@@ -3,41 +3,30 @@ import React from 'react';
 import { SettingsHeader } from './SessionSettingsHeader';
 import { SessionSettingListItem } from './SessionSettingListItem';
 import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton';
-import { BlockedNumberController, PasswordUtil } from '../../../util';
+import { PasswordUtil } from '../../../util';
 import { ConversationLookupType } from '../../../state/ducks/conversations';
 import { StateType } from '../../../state/reducer';
-import { getConversationController } from '../../../session/conversations';
 import { getConversationLookup } from '../../../state/selectors/conversations';
 import { connect } from 'react-redux';
-import {
-  createOrUpdateItem,
-  getPasswordHash,
-  hasLinkPreviewPopupBeenDisplayed,
-} from '../../../../ts/data/data';
+import { getPasswordHash } from '../../../../ts/data/data';
 import { shell } from 'electron';
 import { mapDispatchToProps } from '../../../state/actions';
-import { unblockConvoById } from '../../../interactions/conversationInteractions';
-import { toggleAudioAutoplay } from '../../../state/ducks/userConfig';
-import { sessionPassword, updateConfirmModal } from '../../../state/ducks/modalDialog';
-import { PasswordAction } from '../../dialog/SessionPasswordDialog';
 import { SessionIconButton } from '../icon';
-import { ToastUtils } from '../../../session/utils';
 import autoBind from 'auto-bind';
-
-export enum SessionSettingCategory {
-  Appearance = 'appearance',
-  Account = 'account',
-  Privacy = 'privacy',
-  Permissions = 'permissions',
-  Notifications = 'notifications',
-  Blocked = 'blocked',
+import {
+  getLocalSettings,
+  LocalSettingType,
+  SessionSettingCategory,
+  SessionSettingType,
+} from './LocalSettings';
+import { getBlockedUserSettings } from './BlockedUserSettings';
+
+export function getMediaPermissionsSettings() {
+  return window.getSettingValue('media-permissions');
 }
 
-export enum SessionSettingType {
-  Toggle = 'toggle',
-  Options = 'options',
-  Button = 'button',
-  Slider = 'slider',
+export function getCallMediaPermissionsSettings() {
+  return window.getSettingValue('call-media-permissions');
 }
 
 export interface SettingsViewProps {
@@ -51,22 +40,50 @@ interface State {
   hasPassword: boolean | null;
   pwdLockError: string | null;
   mediaSetting: boolean | null;
+  callMediaSetting: boolean | null;
   shouldLockSettings: boolean | null;
 }
 
-interface LocalSettingType {
-  category: SessionSettingCategory;
-  description: string | undefined;
-  comparisonValue: string | undefined;
-  id: any;
-  value?: any;
-  content: any | undefined;
-  hidden: any;
-  title?: string;
-  type: SessionSettingType | undefined;
-  setFn: any;
-  onClick: any;
-}
+export const PasswordLock = ({
+  pwdLockError,
+  validatePasswordLock,
+}: {
+  pwdLockError: string | null;
+  validatePasswordLock: () => Promise<boolean>;
+}) => {
+  return (
+    <div className="session-settings__password-lock">
+      <div className="session-settings__password-lock-box">
+        <h3>{window.i18n('password')}</h3>
+        <input type="password" id="password-lock-input" defaultValue="" placeholder="Password" />
+
+        {pwdLockError && <div className="session-label warning">{pwdLockError}</div>}
+
+        <SessionButton
+          buttonType={SessionButtonType.BrandOutline}
+          buttonColor={SessionButtonColor.Green}
+          text={window.i18n('ok')}
+          onClick={validatePasswordLock}
+        />
+      </div>
+    </div>
+  );
+};
+
+const SessionInfo = () => {
+  const openOxenWebsite = () => {
+    void shell.openExternal('https://oxen.io/');
+  };
+  return (
+    <div className="session-settings__version-info">
+      <span className="text-selectable">v{window.versionInfo.version}</span>
+      <span>
+        <SessionIconButton iconSize={'medium'} iconType="oxen" onClick={openOxenWebsite} />
+      </span>
+      <span className="text-selectable">{window.versionInfo.commitHash}</span>
+    </div>
+  );
+};
 
 class SettingsViewInner extends React.Component<SettingsViewProps, State> {
   public settingsViewRef: React.RefObject<HTMLDivElement>;
@@ -78,6 +95,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
       hasPassword: null,
       pwdLockError: null,
       mediaSetting: null,
+      callMediaSetting: null,
       shouldLockSettings: true,
     };
 
@@ -90,8 +108,9 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
   public componentDidMount() {
     window.addEventListener('keyup', this.onKeyUp);
 
-    const mediaSetting = window.getSettingValue('media-permissions');
-    this.setState({ mediaSetting });
+    const mediaSetting = getMediaPermissionsSettings();
+    const callMediaSetting = getCallMediaPermissionsSettings();
+    this.setState({ mediaSetting, callMediaSetting });
 
     setTimeout(() => ($('#password-lock-input') as any).focus(), 100);
   }
@@ -108,13 +127,13 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
 
     if (category === SessionSettingCategory.Blocked) {
       // special case for blocked user
-      settings = this.getBlockedUserSettings();
+      settings = getBlockedUserSettings();
     } else {
       // Grab initial values from database on startup
       // ID corresponds to installGetter parameters in preload.js
       // They are NOT arbitrary; add with caution
 
-      settings = this.getLocalSettings();
+      settings = getLocalSettings(this.state.hasPassword, this.onPasswordUpdated, this.forceUpdate);
     }
 
     return (
@@ -132,6 +151,8 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
                 ? storedSetting
                 : setting.content && setting.content.defaultValue;
 
+            if (setting.id.startsWith('call-media')) console.warn('storedSetting call: ', value);
+
             const sliderFn =
               setting.type === SessionSettingType.Slider
                 ? (settingValue: any) => window.setSettingValue(setting.id, settingValue)
@@ -163,28 +184,6 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
     );
   }
 
-  public renderPasswordLock() {
-    return (
-      <div className="session-settings__password-lock">
-        <div className="session-settings__password-lock-box">
-          <h3>{window.i18n('password')}</h3>
-          <input type="password" id="password-lock-input" defaultValue="" placeholder="Password" />
-
-          {this.state.pwdLockError && (
-            <div className="session-label warning">{this.state.pwdLockError}</div>
-          )}
-
-          <SessionButton
-            buttonType={SessionButtonType.BrandOutline}
-            buttonColor={SessionButtonColor.Green}
-            text={window.i18n('ok')}
-            onClick={this.validatePasswordLock}
-          />
-        </div>
-      </div>
-    );
-  }
-
   public async validatePasswordLock() {
     const enteredPassword = String(jQuery('#password-lock-input').val());
 
@@ -228,37 +227,21 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
 
         <div className="session-settings-view">
           {shouldRenderPasswordLock ? (
-            this.renderPasswordLock()
+            <PasswordLock
+              pwdLockError={this.state.pwdLockError}
+              validatePasswordLock={this.validatePasswordLock}
+            />
           ) : (
             <div ref={this.settingsViewRef} className="session-settings-list">
               {this.renderSettingInCategory()}
             </div>
           )}
-          {this.renderSessionInfo()}
+          <SessionInfo />
         </div>
       </div>
     );
   }
 
-  public renderSessionInfo(): JSX.Element {
-    const openOxenWebsite = () => {
-      void shell.openExternal('https://oxen.io/');
-    };
-    return (
-      <div className="session-settings__version-info">
-        <span className="text-selectable">v{window.versionInfo.version}</span>
-        <span>
-          <SessionIconButton iconSize={'medium'} iconType="oxen" onClick={openOxenWebsite} />
-        </span>
-        <span className="text-selectable">{window.versionInfo.commitHash}</span>
-      </div>
-    );
-  }
-
-  public setOptionsSetting(settingID: string, selectedValue: string) {
-    window.setSettingValue(settingID, selectedValue);
-  }
-
   public async hasPassword() {
     const hash = await getPasswordHash();
 
@@ -274,19 +257,15 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
    */
   public updateSetting(item: any, value?: string) {
     if (item.setFn) {
-      if (value) {
-        item.setFn(value);
-      } else {
-        item.setFn();
-      }
-    } else {
-      if (item.type === SessionSettingType.Toggle) {
-        // If no custom afterClick function given, alter values in storage here
-
-        // Switch to opposite state
-        const newValue = !window.getSettingValue(item.id);
-        window.setSettingValue(item.id, newValue);
-      }
+      item.setFn(value);
+      this.forceUpdate();
+    } else if (item.type === SessionSettingType.Toggle) {
+      // If no custom afterClick function given, alter values in storage here
+
+      // Switch to opposite state
+      const newValue = !window.getSettingValue(item.id);
+      window.setSettingValue(item.id, newValue);
+      this.forceUpdate();
     }
   }
 
@@ -306,363 +285,6 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
     }
   }
 
-  // tslint:disable-next-line: max-func-body-length
-  private getLocalSettings(): Array<LocalSettingType> {
-    const { Settings } = window.Signal.Types;
-
-    return [
-      {
-        id: 'hide-menu-bar',
-        title: window.i18n('hideMenuBarTitle'),
-        description: window.i18n('hideMenuBarDescription'),
-        hidden: !Settings.isHideMenuBarSupported(),
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Appearance,
-        setFn: window.toggleMenuBar,
-        content: { defaultValue: true },
-        comparisonValue: undefined,
-        onClick: undefined,
-      },
-      {
-        id: 'spell-check',
-        title: window.i18n('spellCheckTitle'),
-        description: window.i18n('spellCheckDescription'),
-        hidden: false,
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Appearance,
-        setFn: window.toggleSpellCheck,
-        content: { defaultValue: true },
-        comparisonValue: undefined,
-        onClick: undefined,
-      },
-      {
-        id: 'link-preview-setting',
-        title: window.i18n('linkPreviewsTitle'),
-        description: window.i18n('linkPreviewDescription'),
-        hidden: false,
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Appearance,
-        setFn: async () => {
-          const newValue = !window.getSettingValue('link-preview-setting');
-          window.setSettingValue('link-preview-setting', newValue);
-          if (!newValue) {
-            await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: false });
-          } else {
-            window.inboxStore?.dispatch(
-              updateConfirmModal({
-                title: window.i18n('linkPreviewsTitle'),
-                message: window.i18n('linkPreviewsConfirmMessage'),
-                okTheme: SessionButtonColor.Danger,
-                // onClickOk:
-              })
-            );
-          }
-        },
-        content: undefined,
-        comparisonValue: undefined,
-        onClick: undefined,
-      },
-
-      {
-        id: 'start-in-tray-setting',
-        title: window.i18n('startInTrayTitle'),
-        description: window.i18n('startInTrayDescription'),
-        hidden: false,
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Appearance,
-        setFn: async () => {
-          try {
-            const newValue = !(await window.getStartInTray());
-
-            // make sure to write it here too, as this is the value used on the UI to mark the toggle as true/false
-            window.setSettingValue('start-in-tray-setting', newValue);
-            await window.setStartInTray(newValue);
-            if (!newValue) {
-              ToastUtils.pushRestartNeeded();
-            }
-          } catch (e) {
-            window.log.warn('start in tray change error:', e);
-          }
-        },
-        content: undefined,
-        comparisonValue: undefined,
-        onClick: undefined,
-      },
-      {
-        id: 'audio-message-autoplay-setting',
-        title: window.i18n('audioMessageAutoplayTitle'),
-        description: window.i18n('audioMessageAutoplayDescription'),
-        hidden: false,
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Appearance,
-        setFn: () => {
-          window.inboxStore?.dispatch(toggleAudioAutoplay());
-        },
-        content: {
-          defaultValue: window.inboxStore?.getState().userConfig.audioAutoplay,
-        },
-        comparisonValue: undefined,
-        onClick: undefined,
-      },
-
-      {
-        id: 'notification-setting',
-        title: window.i18n('notificationSettingsDialog'),
-        type: SessionSettingType.Options,
-        category: SessionSettingCategory.Notifications,
-        comparisonValue: undefined,
-        description: undefined,
-        hidden: undefined,
-        onClick: undefined,
-        setFn: (selectedValue: string) => {
-          this.setOptionsSetting('notification-setting', selectedValue);
-        },
-        content: {
-          options: {
-            group: 'notification-setting',
-            initialItem: window.getSettingValue('notification-setting') || 'message',
-            items: [
-              {
-                label: window.i18n('nameAndMessage'),
-                value: 'message',
-              },
-              {
-                label: window.i18n('nameOnly'),
-                value: 'name',
-              },
-              {
-                label: window.i18n('noNameOrMessage'),
-                value: 'count',
-              },
-              {
-                label: window.i18n('disableNotifications'),
-                value: 'off',
-              },
-            ],
-          },
-        },
-      },
-      {
-        id: 'zoom-factor-setting',
-        title: window.i18n('zoomFactorSettingTitle'),
-        description: undefined,
-        hidden: false,
-        type: SessionSettingType.Slider,
-        category: SessionSettingCategory.Appearance,
-        setFn: undefined,
-        comparisonValue: undefined,
-        onClick: undefined,
-        content: {
-          dotsEnabled: true,
-          step: 20,
-          min: 60,
-          max: 200,
-          defaultValue: 100,
-          info: (value: number) => `${value}%`,
-        },
-      },
-      {
-        id: 'session-survey',
-        title: window.i18n('surveyTitle'),
-        description: undefined,
-        hidden: false,
-        type: SessionSettingType.Button,
-        category: SessionSettingCategory.Appearance,
-        setFn: undefined,
-        comparisonValue: undefined,
-        onClick: () => {
-          void shell.openExternal('https://getsession.org/survey');
-        },
-        content: {
-          buttonText: window.i18n('goToOurSurvey'),
-          buttonColor: SessionButtonColor.Primary,
-        },
-      },
-      {
-        id: 'help-translation',
-        title: window.i18n('translation'),
-        description: undefined,
-        hidden: false,
-        type: SessionSettingType.Button,
-        category: SessionSettingCategory.Appearance,
-        setFn: undefined,
-        comparisonValue: undefined,
-        onClick: () => {
-          void shell.openExternal('https://crowdin.com/project/session-desktop/');
-        },
-        content: {
-          buttonText: window.i18n('helpUsTranslateSession'),
-          buttonColor: SessionButtonColor.Primary,
-        },
-      },
-      {
-        id: 'media-permissions',
-        title: window.i18n('mediaPermissionsTitle'),
-        description: window.i18n('mediaPermissionsDescription'),
-        hidden: false,
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Privacy,
-        setFn: window.toggleMediaPermissions,
-        content: undefined,
-        comparisonValue: undefined,
-        onClick: undefined,
-      },
-      {
-        id: 'read-receipt-setting',
-        title: window.i18n('readReceiptSettingTitle'),
-        description: window.i18n('readReceiptSettingDescription'),
-        hidden: false,
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Privacy,
-        setFn: undefined,
-        comparisonValue: undefined,
-        onClick: undefined,
-        content: {},
-      },
-      {
-        id: 'typing-indicators-setting',
-        title: window.i18n('typingIndicatorsSettingTitle'),
-        description: window.i18n('typingIndicatorsSettingDescription'),
-        hidden: false,
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Privacy,
-        setFn: undefined,
-        comparisonValue: undefined,
-        onClick: undefined,
-        content: {},
-      },
-      {
-        id: 'auto-update',
-        title: window.i18n('autoUpdateSettingTitle'),
-        description: window.i18n('autoUpdateSettingDescription'),
-        hidden: false,
-        type: SessionSettingType.Toggle,
-        category: SessionSettingCategory.Privacy,
-        setFn: undefined,
-        comparisonValue: undefined,
-        onClick: undefined,
-        content: {},
-      },
-      {
-        id: 'set-password',
-        title: window.i18n('setAccountPasswordTitle'),
-        description: window.i18n('setAccountPasswordDescription'),
-        hidden: this.state.hasPassword,
-        type: SessionSettingType.Button,
-        category: SessionSettingCategory.Privacy,
-        setFn: undefined,
-        comparisonValue: undefined,
-        content: {
-          buttonText: window.i18n('setPassword'),
-          buttonColor: SessionButtonColor.Primary,
-        },
-        onClick: () => {
-          this.displayPasswordModal('set');
-        },
-      },
-      {
-        id: 'change-password',
-        title: window.i18n('changeAccountPasswordTitle'),
-        description: window.i18n('changeAccountPasswordDescription'),
-        hidden: !this.state.hasPassword,
-        type: SessionSettingType.Button,
-        category: SessionSettingCategory.Privacy,
-        setFn: undefined,
-        comparisonValue: undefined,
-        content: {
-          buttonText: window.i18n('changePassword'),
-          buttonColor: SessionButtonColor.Primary,
-        },
-        onClick: () => {
-          this.displayPasswordModal('change');
-        },
-      },
-      {
-        id: 'remove-password',
-        title: window.i18n('removeAccountPasswordTitle'),
-        description: window.i18n('removeAccountPasswordDescription'),
-        hidden: !this.state.hasPassword,
-        type: SessionSettingType.Button,
-        category: SessionSettingCategory.Privacy,
-        setFn: undefined,
-        comparisonValue: undefined,
-        content: {
-          buttonText: window.i18n('removePassword'),
-          buttonColor: SessionButtonColor.Danger,
-        },
-        onClick: () => {
-          this.displayPasswordModal('remove');
-        },
-      },
-    ];
-  }
-
-  private displayPasswordModal(passwordAction: PasswordAction) {
-    window.inboxStore?.dispatch(
-      sessionPassword({
-        passwordAction,
-        onOk: () => {
-          this.onPasswordUpdated(passwordAction);
-        },
-      })
-    );
-  }
-
-  private getBlockedUserSettings(): Array<LocalSettingType> {
-    const results: Array<LocalSettingType> = [];
-    const blockedNumbers = BlockedNumberController.getBlockedNumbers();
-
-    for (const blockedNumber of blockedNumbers) {
-      let title: string;
-
-      const currentModel = getConversationController().get(blockedNumber);
-      if (currentModel) {
-        title = currentModel.getProfileName() || currentModel.getName() || window.i18n('anonymous');
-      } else {
-        title = window.i18n('anonymous');
-      }
-
-      results.push({
-        id: blockedNumber,
-        title,
-        description: '',
-        type: SessionSettingType.Button,
-        category: SessionSettingCategory.Blocked,
-        content: {
-          buttonColor: SessionButtonColor.Danger,
-          buttonText: window.i18n('unblockUser'),
-        },
-        comparisonValue: undefined,
-        setFn: async () => {
-          await unblockConvoById(blockedNumber);
-
-          this.forceUpdate();
-        },
-        hidden: false,
-        onClick: undefined,
-      });
-    }
-
-    if (blockedNumbers.length === 0) {
-      return [
-        {
-          id: 'noBlockedContacts',
-          title: '',
-          description: window.i18n('noBlockedContacts'),
-          type: undefined,
-          category: SessionSettingCategory.Blocked,
-          content: undefined,
-          comparisonValue: undefined,
-          setFn: undefined,
-          hidden: false,
-          onClick: undefined,
-        },
-      ];
-    }
-
-    return results;
-  }
-
   private async onKeyUp(event: any) {
     const lockPasswordFocussed = ($('#password-lock-input') as any).is(':focus');
 
diff --git a/ts/session/onions/onionPath.ts b/ts/session/onions/onionPath.ts
index b70fad49d..8e71b7a64 100644
--- a/ts/session/onions/onionPath.ts
+++ b/ts/session/onions/onionPath.ts
@@ -470,16 +470,20 @@ async function buildNewOnionPathsWorker() {
         const lastDot = e.ip.lastIndexOf('.');
         return e.ip.substr(0, lastDot);
       });
-      const oneNodeForEachSubnet24 = _.map(allNodesGroupedBySubnet24, group => {
-        return _.sample(group) as Data.Snode;
-      });
-      if (oneNodeForEachSubnet24.length <= SnodePool.minSnodePoolCount) {
+      const oneNodeForEachSubnet24KeepingRatio = _.flatten(
+        _.map(allNodesGroupedBySubnet24, group => {
+          return _.fill(Array(group.length), _.sample(group) as Data.Snode);
+        })
+      );
+      if (oneNodeForEachSubnet24KeepingRatio.length <= SnodePool.minSnodePoolCount) {
         throw new Error(
           'Too few nodes "unique by ip" to build an onion path. Even after fetching from seed.'
         );
       }
-      const otherNodes = _.shuffle(
-        _.differenceBy(oneNodeForEachSubnet24, guardNodes, 'pubkey_ed25519')
+      let otherNodes = _.differenceBy(
+        oneNodeForEachSubnet24KeepingRatio,
+        guardNodes,
+        'pubkey_ed25519'
       );
       const guards = _.shuffle(guardNodes);
 
@@ -492,13 +496,16 @@ async function buildNewOnionPathsWorker() {
         `Building ${maxPath} onion paths based on guard nodes length: ${guards.length}, other nodes length ${otherNodes.length} `
       );
 
-      // TODO: might want to keep some of the existing paths
       onionPaths = [];
 
       for (let i = 0; i < maxPath; i += 1) {
         const path = [guards[i]];
         for (let j = 0; j < nodesNeededPerPaths; j += 1) {
-          path.push(otherNodes[i * nodesNeededPerPaths + j]);
+          const randomWinner = _.sample(otherNodes) as Data.Snode;
+          otherNodes = otherNodes.filter(n => {
+            return n.pubkey_ed25519 !== randomWinner?.pubkey_ed25519;
+          });
+          path.push(randomWinner);
         }
         onionPaths.push(path);
       }
diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts
index 032dacea2..e27114994 100644
--- a/ts/session/utils/CallManager.ts
+++ b/ts/session/utils/CallManager.ts
@@ -1,6 +1,6 @@
 import _ from 'lodash';
 import { ToastUtils } from '.';
-import { SessionSettingCategory } from '../../components/session/settings/SessionSettings';
+import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings';
 import { getConversationById } from '../../data/data';
 import { MessageModelType } from '../../models/messageType';
 import { SignalService } from '../../protobuf';
@@ -11,7 +11,6 @@ import {
   incomingCall,
   startingCallWith,
 } from '../../state/ducks/conversations';
-import { SectionType, showLeftPaneSection, showSettingsSection } from '../../state/ducks/section';
 import { getConversationController } from '../conversations';
 import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
 import { ed25519Str } from '../onions/onionPath';
@@ -305,16 +304,18 @@ async function openMediaDevicesAndAddTracks() {
       }
     });
   } catch (err) {
-    ToastUtils.pushMicAndCameraPermissionNeeded(() => {
-      window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
-      window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
-    });
+    ToastUtils.pushMicAndCameraPermissionNeeded();
+    closeVideoCall();
   }
   callVideoListener();
 }
 
 // tslint:disable-next-line: function-name
 export async function USER_callRecipient(recipient: string) {
+  if (!getCallMediaPermissionsSettings()) {
+    ToastUtils.pushMicAndCameraPermissionNeeded();
+    return;
+  }
   await updateInputLists();
   window?.log?.info(`starting call with ${ed25519Str(recipient)}..`);
   window.inboxStore?.dispatch(startingCallWith({ pubkey: recipient }));
@@ -467,8 +468,6 @@ function createOrGetPeerConnection(withPubkey: string, createDataChannel: boolea
   };
 
   if (createDataChannel) {
-    // console.warn('createOrGetPeerConnection: createDataChannel');
-
     dataChannel = peerConnection.createDataChannel('session-datachannel');
 
     dataChannel.onmessage = onDataChannelReceivedMessage;
@@ -644,6 +643,13 @@ export async function handleCallTypeOffer(
       }
     }
 
+    if (!getCallMediaPermissionsSettings()) {
+      await handleMissedCall(sender, incomingOfferTimestamp);
+      // TODO audric show where to turn it on
+      debugger;
+      return;
+    }
+
     const readyForOffer =
       !makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
     const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx
index 8306676e8..77ce284ba 100644
--- a/ts/session/utils/Toast.tsx
+++ b/ts/session/utils/Toast.tsx
@@ -2,6 +2,8 @@ import React from 'react';
 import { toast } from 'react-toastify';
 import { SessionIconType } from '../../components/session/icon';
 import { SessionToast, SessionToastType } from '../../components/session/SessionToast';
+import { SessionSettingCategory } from '../../components/session/settings/LocalSettings';
+import { SectionType, showLeftPaneSection, showSettingsSection } from '../../state/ducks/section';
 
 // if you push a toast manually with toast...() be sure to set the type attribute of the SessionToast component
 export function pushToastError(id: string, title: string, description?: string) {
@@ -146,12 +148,15 @@ export function pushedMissedCall(conversationName: string) {
   );
 }
 
-export function pushMicAndCameraPermissionNeeded(onClicked: () => void) {
+export function pushMicAndCameraPermissionNeeded() {
   pushToastInfo(
     'micAndCameraPermissionNeeded',
     window.i18n('micAndCameraPermissionNeededTitle'),
     window.i18n('micAndCameraPermissionNeeded'),
-    onClicked
+    () => {
+      window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
+      window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
+    }
   );
 }
 
diff --git a/ts/state/ducks/section.tsx b/ts/state/ducks/section.tsx
index 0d3d2b95c..9db8e810a 100644
--- a/ts/state/ducks/section.tsx
+++ b/ts/state/ducks/section.tsx
@@ -1,4 +1,4 @@
-import { SessionSettingCategory } from '../../components/session/settings/SessionSettings';
+import { SessionSettingCategory } from '../../components/session/settings/LocalSettings';
 
 export const FOCUS_SECTION = 'FOCUS_SECTION';
 export const FOCUS_SETTINGS_SECTION = 'FOCUS_SETTINGS_SECTION';
diff --git a/ts/state/selectors/section.ts b/ts/state/selectors/section.ts
index 65558bd8c..20c1a9411 100644
--- a/ts/state/selectors/section.ts
+++ b/ts/state/selectors/section.ts
@@ -2,7 +2,7 @@ import { createSelector } from 'reselect';
 
 import { StateType } from '../reducer';
 import { SectionStateType, SectionType } from '../ducks/section';
-import { SessionSettingCategory } from '../../components/session/settings/SessionSettings';
+import { SessionSettingCategory } from '../../components/session/settings/LocalSettings';
 
 export const getSection = (state: StateType): SectionStateType => state.section;
 
diff --git a/ts/window.d.ts b/ts/window.d.ts
index 449167a80..55518cd8c 100644
--- a/ts/window.d.ts
+++ b/ts/window.d.ts
@@ -57,7 +57,9 @@ declare global {
     setSettingValue: any;
     storage: any;
     textsecure: LibTextsecure;
-    toggleMediaPermissions: any;
+    toggleMediaPermissions: () => Promise<void>;
+    toggleCallMediaPermissionsTo: (enabled: boolean) => Promise<void>;
+    getCallMediaPermissions: () => boolean;
     toggleMenuBar: any;
     toggleSpellCheck: any;
     setTheme: (newTheme: string) => any;