diff --git a/.gitignore b/.gitignore
index 8e39ac479..ec7052e25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,4 +49,4 @@ yarn-error.log
 libloki/test/test.js
 
 playwright.config.js
-playwright.config.js.map
+*.js.map
diff --git a/background.html b/background.html
index 41933bde8..2d9493789 100644
--- a/background.html
+++ b/background.html
@@ -27,19 +27,11 @@
     <link href="images/sesion/session_icon_128.png" rel="shortcut icon" />
     <link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
 
-    <!--
-    When making changes to these templates, be sure to update test/index.html as well
-  -->
-
     <script type="text/javascript" src="js/components.js"></script>
     <script type="text/javascript" src="js/libtextsecure.js"></script>
 
     <script type="text/javascript" src="js/views/react_wrapper_view.js"></script>
     <script type="text/javascript" src="js/views/whisper_view.js"></script>
-
-    <script type="text/javascript" src="js/views/session_inbox_view.js"></script>
-    <script type="text/javascript" src="js/views/session_registration_view.js"></script>
-    <script type="text/javascript" src="js/views/app_view.js"></script>
   </head>
 
   <body>
diff --git a/debug_log.html b/debug_log.html
index b9b4bb311..473f787f7 100644
--- a/debug_log.html
+++ b/debug_log.html
@@ -17,36 +17,9 @@
     <link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
     <style></style>
   </head>
-  <body class="debug-log-window"></body>
-  <script type="text/x-tmpl-mustache" id="debug-log">
-    <div class='content'>
-      <div>
-        <a class='x close' alt='close debug log' href='#'></a>
-        <h1> {{ title }} </h1>
-        <p> {{ debugLogExplanation }}</p>
-      </div>
-      <textarea spellcheck='false' rows='5'></textarea>
-      <div class='buttons'>
-        <button class='grey submit'>{{ saveLogToDesktop }}</button>
-      </div>
-      <div class='result'>
-      </div>
-    </div>
-  </script>
-  <script type="text/x-tmpl-mustache" id="debug-log-link">
-    <div class='input-group clearfix'>
-      <input type='text' class='link' readonly value='{{ url }}' />
-      <a class='open' alt='open in a new browser tab' target='_blank' href='{{ url }}'></a>
-    </div>
-    <p>
-      <a class='report-link' target='_blank'
-          href='https://github.com/oxen-io/session-desktop/issues/new/'>
-          {{ reportIssue }}
-      </a>
-    </p>
-  </script>
-  <script type="text/javascript" src="js/components.js"></script>
-  <script type="text/javascript" src="js/views/whisper_view.js"></script>
-  <script type="text/javascript" src="js/views/debug_log_view.js"></script>
+  <body>
+    <div id="app"></div>
+  </body>
+
   <script type="text/javascript" src="js/debug_log_start.js"></script>
 </html>
diff --git a/debug_log_preload.js b/debug_log_preload.js
index 3c495e2d8..eac1bb437 100644
--- a/debug_log_preload.js
+++ b/debug_log_preload.js
@@ -3,6 +3,7 @@
 const { ipcRenderer } = require('electron');
 const url = require('url');
 const i18n = require('./js/modules/i18n');
+const { DebugLogView } = require('./ts/views/DebugLogView');
 
 const config = url.parse(window.location.toString(), true).query;
 const { locale } = config;
@@ -13,6 +14,9 @@ global.dcodeIO.ByteBuffer = require('bytebuffer');
 
 window._ = require('lodash');
 
+window.React = require('react');
+window.ReactDOM = require('react-dom');
+
 window.getVersion = () => config.version;
 window.theme = config.theme;
 window.i18n = i18n.setup(locale, localeMessages);
@@ -31,4 +35,7 @@ window.getCommitHash = () => config.commitHash;
 
 window.closeDebugLog = () => ipcRenderer.send('close-debug-log');
 
+window.Views = {};
+window.Views.DebugLogView = DebugLogView;
+
 window.saveLog = logText => ipcRenderer.send('save-debug-log', logText);
diff --git a/js/debug_log_start.js b/js/debug_log_start.js
index 0c55b6415..ed4f1f3c0 100644
--- a/js/debug_log_start.js
+++ b/js/debug_log_start.js
@@ -9,10 +9,4 @@ $(document).on('keyup', e => {
   }
 });
 
-const $body = $(document.body);
-
-// got.js appears to need this to successfully submit debug logs to the cloud
-window.setImmediate = window.nodeSetImmediate;
-
-window.view = new Whisper.DebugLogView();
-window.view.$el.appendTo($body);
+window.ReactDOM.render(<window.Views.DebugLogView />, document.getElementById('app'));
diff --git a/js/main_start.js b/js/main_start.js
deleted file mode 100644
index ab8d87e51..000000000
--- a/js/main_start.js
+++ /dev/null
@@ -1,430 +0,0 @@
-/* global
-  $,
-  _,
-  Backbone,
-  storage,
-  Whisper,
-  BlockedNumberController,
-  Signal
-*/
-
-// eslint-disable-next-line func-names
-(async function() {
-  'use strict';
-
-  // Globally disable drag and drop
-  document.body.addEventListener(
-    'dragover',
-    e => {
-      e.preventDefault();
-      e.stopPropagation();
-    },
-    false
-  );
-  document.body.addEventListener(
-    'drop',
-    e => {
-      e.preventDefault();
-      e.stopPropagation();
-    },
-    false
-  );
-
-  // Load these images now to ensure that they don't flicker on first use
-  const images = [];
-  function preload(list) {
-    for (let index = 0, max = list.length; index < max; index += 1) {
-      const image = new Image();
-      image.src = `./images/${list[index]}`;
-      images.push(image);
-    }
-  }
-  preload([
-    'alert-outline.svg',
-    'check.svg',
-    'error.svg',
-    'file-gradient.svg',
-    'file.svg',
-    'image.svg',
-    'microphone.svg',
-    'movie.svg',
-    'open_link.svg',
-    'play.svg',
-    'save.svg',
-    'shield.svg',
-    'timer.svg',
-    'video.svg',
-    'warning.svg',
-    'x.svg',
-  ]);
-
-  // We add this to window here because the default Node context is erased at the end
-  //   of preload.js processing
-  window.setImmediate = window.nodeSetImmediate;
-  window.globalOnlineStatus = true; // default to true as we don't get an event on app start
-  window.getGlobalOnlineStatus = () => window.globalOnlineStatus;
-
-  window.log.info('background page reloaded');
-  window.log.info('environment:', window.getEnvironment());
-
-  let initialLoadComplete = false;
-  let newVersion = false;
-
-  window.document.title = window.getTitle();
-
-  Whisper.events = _.clone(Backbone.Events);
-  Whisper.events.isListenedTo = eventName =>
-    Whisper.events._events ? !!Whisper.events._events[eventName] : false;
-
-  window.log.info('Storage fetch');
-  storage.fetch();
-
-  function mapOldThemeToNew(theme) {
-    switch (theme) {
-      case 'dark':
-      case 'light':
-        return theme;
-      case 'android-dark':
-        return 'dark';
-      case 'android':
-      case 'ios':
-      default:
-        return 'light';
-    }
-  }
-
-  // We need this 'first' check because we don't want to start the app up any other time
-  //   than the first time. And storage.fetch() will cause onready() to fire.
-  let first = true;
-  storage.onready(async () => {
-    if (!first) {
-      return;
-    }
-    first = false;
-
-    // Update zoom
-    window.updateZoomFactor();
-
-    // Ensure accounts created prior to 1.0.0-beta8 do have their
-    // 'primaryDevicePubKey' defined.
-
-    if (Whisper.Registration.isDone() && !storage.get('primaryDevicePubKey', null)) {
-      storage.put(
-        'primaryDevicePubKey',
-        window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache()
-      );
-    }
-
-    // These make key operations available to IPC handlers created in preload.js
-    window.Events = {
-      getThemeSetting: () => storage.get('theme-setting', 'light'),
-      setThemeSetting: value => {
-        storage.put('theme-setting', value);
-      },
-      getHideMenuBar: () => storage.get('hide-menu-bar'),
-      setHideMenuBar: value => {
-        storage.put('hide-menu-bar', value);
-        window.setAutoHideMenuBar(false);
-        window.setMenuBarVisibility(!value);
-      },
-
-      getSpellCheck: () => storage.get('spell-check', true),
-      setSpellCheck: value => {
-        storage.put('spell-check', value);
-      },
-
-      shutdown: async () => {
-        // Stop background processing
-        window.libsession.Utils.AttachmentDownloads.stop();
-        // Stop processing incoming messages
-        // FIXME audric stop polling opengroupv2 and swarm nodes
-
-        // Shut down the data interface cleanly
-        await window.Signal.Data.shutdown();
-      },
-    };
-
-    const currentVersion = window.getVersion();
-    const lastVersion = storage.get('version');
-    newVersion = !lastVersion || currentVersion !== lastVersion;
-    await storage.put('version', currentVersion);
-
-    if (newVersion) {
-      window.log.info(`New version detected: ${currentVersion}; previous: ${lastVersion}`);
-
-      await window.Signal.Data.cleanupOrphanedAttachments();
-
-      await window.Signal.Logs.deleteAll();
-    }
-
-    const themeSetting = window.Events.getThemeSetting();
-    const newThemeSetting = mapOldThemeToNew(themeSetting);
-    window.Events.setThemeSetting(newThemeSetting);
-
-    try {
-      window.libsession.Utils.AttachmentDownloads.initAttachmentPaths();
-
-      await Promise.all([
-        window.getConversationController().load(),
-        BlockedNumberController.load(),
-      ]);
-    } catch (error) {
-      window.log.error(
-        'main_start.js: ConversationController failed to load:',
-        error && error.stack ? error.stack : error
-      );
-    } finally {
-      start();
-    }
-  });
-
-  function manageExpiringData() {
-    window.Signal.Data.cleanSeenMessages();
-    window.Signal.Data.cleanLastHashes();
-    setTimeout(manageExpiringData, 1000 * 60 * 60);
-  }
-
-  async function start() {
-    manageExpiringData();
-    window.dispatchEvent(new Event('storage_ready'));
-
-    window.log.info('Cleanup: starting...');
-
-    const results = await Promise.all([window.Signal.Data.getOutgoingWithoutExpiresAt()]);
-
-    // Combine the models
-    const messagesForCleanup = results.reduce(
-      (array, current) => array.concat(current.toArray()),
-      []
-    );
-
-    window.log.info(`Cleanup: Found ${messagesForCleanup.length} messages for cleanup`);
-    await Promise.all(
-      messagesForCleanup.map(async message => {
-        const sentAt = message.get('sent_at');
-
-        if (message.hasErrors()) {
-          return;
-        }
-
-        window.log.info(`Cleanup: Deleting unsent message ${sentAt}`);
-        await window.Signal.Data.removeMessage(message.id);
-      })
-    );
-    window.log.info('Cleanup: complete');
-
-    window.log.info('listening for registration events');
-    Whisper.events.on('registration_done', async () => {
-      window.log.info('handling registration event');
-
-      // Disable link previews as default per Kee
-      storage.onready(async () => {
-        storage.put('link-preview-setting', false);
-      });
-
-      connect(true);
-    });
-
-    const appView = new Whisper.AppView({
-      el: $('body'),
-    });
-
-    // FIXME audric2 ExpiringMessagesListener  Whisper.Expi
-    // throw new Error('plop');
-    Whisper.ExpiringMessagesListener.init(Whisper.events);
-
-    if (Whisper.Registration.isDone() && !window.textsecure.storage.user.isSignInByLinking()) {
-      connect();
-      appView.openInbox({
-        initialLoadComplete,
-      });
-    } else {
-      appView.openStandalone();
-    }
-
-    Whisper.events.on('showDebugLog', () => {
-      appView.openDebugLog();
-    });
-
-    window.addEventListener('focus', () => Whisper.Notifications.clear());
-    window.addEventListener('unload', () => Whisper.Notifications.fastClear());
-
-    // Set user's launch count.
-    const prevLaunchCount = window.getSettingValue('launch-count');
-    const launchCount = !prevLaunchCount ? 1 : prevLaunchCount + 1;
-    window.setSettingValue('launch-count', launchCount);
-
-    // On first launch
-    if (launchCount === 1) {
-      // Initialise default settings
-      window.setSettingValue('hide-menu-bar', true);
-      window.setSettingValue('link-preview-setting', false);
-    }
-
-    window.setTheme = newTheme => {
-      window.Events.setThemeSetting(newTheme);
-    };
-
-    window.toggleMenuBar = () => {
-      const current = window.getSettingValue('hide-menu-bar');
-      if (current === undefined) {
-        window.Events.setHideMenuBar(false);
-        return;
-      }
-
-      window.Events.setHideMenuBar(!current);
-    };
-
-    window.toggleSpellCheck = () => {
-      const currentValue = window.getSettingValue('spell-check');
-      // if undefined, it means 'default' so true. but we have to toggle it, so false
-      // if not undefined, we take the opposite
-      const newValue = currentValue !== undefined ? !currentValue : false;
-      window.Events.setSpellCheck(newValue);
-      window.libsession.Utils.ToastUtils.pushRestartNeeded();
-    };
-
-    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);
-      }
-      window.setCallMediaPermissions(enabled);
-    };
-
-    window.openFromNotification = async conversationKey => {
-      window.showWindow();
-      if (conversationKey) {
-        // do not put the messageId here so the conversation is loaded on the last unread instead
-        await window.openConversationWithMessages({ conversationKey, messageId: null });
-      } else {
-        appView.openInbox({
-          initialLoadComplete,
-        });
-      }
-    };
-
-    Whisper.events.on('openInbox', () => {
-      appView.openInbox({
-        initialLoadComplete,
-      });
-    });
-
-    Whisper.events.on('password-updated', () => {
-      if (appView && appView.inboxView) {
-        appView.inboxView.trigger('password-updated');
-      }
-    });
-  }
-
-  let disconnectTimer = null;
-  function onOffline() {
-    window.log.info('offline');
-    window.globalOnlineStatus = false;
-
-    window.removeEventListener('offline', onOffline);
-    window.addEventListener('online', onOnline);
-
-    // We've received logs from Linux where we get an 'offline' event, then 30ms later
-    //   we get an online event. This waits a bit after getting an 'offline' event
-    //   before disconnecting the socket manually.
-    disconnectTimer = setTimeout(disconnect, 1000);
-  }
-
-  function onOnline() {
-    window.log.info('online');
-    window.globalOnlineStatus = true;
-
-    window.removeEventListener('online', onOnline);
-    window.addEventListener('offline', onOffline);
-
-    if (disconnectTimer) {
-      window.log.warn('Already online. Had a blip in online/offline status.');
-      clearTimeout(disconnectTimer);
-      disconnectTimer = null;
-      return;
-    }
-    if (disconnectTimer) {
-      clearTimeout(disconnectTimer);
-      disconnectTimer = null;
-    }
-
-    connect();
-  }
-
-  async function disconnect() {
-    window.log.info('disconnect');
-
-    // Clear timer, since we're only called when the timer is expired
-    disconnectTimer = null;
-    window.libsession.Utils.AttachmentDownloads.stop();
-  }
-
-  let connectCount = 0;
-  async function connect() {
-    window.log.info('connect');
-
-    // Bootstrap our online/offline detection, only the first time we connect
-    if (connectCount === 0 && navigator.onLine) {
-      window.addEventListener('offline', onOffline);
-    }
-    if (connectCount === 0 && !navigator.onLine) {
-      window.log.warn('Starting up offline; will connect when we have network access');
-      window.addEventListener('online', onOnline);
-      onEmpty(); // this ensures that the loading screen is dismissed
-      return;
-    }
-
-    if (!Whisper.Registration.everDone()) {
-      return;
-    }
-
-    connectCount += 1;
-    Whisper.Notifications.disable(); // avoid notification flood until empty
-    setTimeout(() => {
-      Whisper.Notifications.enable();
-    }, 10 * 1000); // 10 sec
-
-    window.NewReceiver.queueAllCached();
-    window.libsession.Utils.AttachmentDownloads.start({
-      logger: window.log,
-    });
-
-    window.textsecure.messaging = true;
-  }
-
-  function onEmpty() {
-    initialLoadComplete = true;
-
-    window.readyForUpdates();
-
-    Whisper.Notifications.enable();
-  }
-})();
diff --git a/js/main_start.ts b/js/main_start.ts
new file mode 100644
index 000000000..707c000d9
--- /dev/null
+++ b/js/main_start.ts
@@ -0,0 +1,569 @@
+import _ from 'lodash';
+import { MessageModel } from '../ts/models/message';
+import { isMacOS } from '../ts/OS';
+import { queueAllCached } from '../ts/receiver/receiver';
+import { getConversationController } from '../ts/session/conversations';
+import { AttachmentDownloads } from '../ts/session/utils';
+import { getOurPubKeyStrFromCache } from '../ts/session/utils/User';
+import { BlockedNumberController } from '../ts/util';
+import { ExpirationTimerOptions } from '../ts/util/expiringMessages';
+import { Notifications } from '../ts/util/notifications';
+import { Registration } from '../ts/util/registration';
+import { isSignInByLinking, Storage } from '../ts/util/storage';
+
+import * as Data from '../ts/data/data';
+import Backbone from 'backbone';
+import { SessionRegistrationView } from '../ts/components/registration/SessionRegistrationView';
+import { SessionInboxView } from '../ts/components/SessionInboxView';
+// tslint:disable: max-classes-per-file
+
+// Globally disable drag and drop
+document.body.addEventListener(
+  'dragover',
+  e => {
+    e.preventDefault();
+    e.stopPropagation();
+  },
+  false
+);
+document.body.addEventListener(
+  'drop',
+  e => {
+    e.preventDefault();
+    e.stopPropagation();
+  },
+  false
+);
+
+// Load these images now to ensure that they don't flicker on first use
+const images = [];
+function preload(list: Array<string>) {
+  // tslint:disable-next-line: one-variable-per-declaration
+  for (let index = 0, max = list.length; index < max; index += 1) {
+    const image = new Image();
+    image.src = `./images/${list[index]}`;
+    images.push(image);
+  }
+}
+preload([
+  'alert-outline.svg',
+  'check.svg',
+  'error.svg',
+  'file-gradient.svg',
+  'file.svg',
+  'image.svg',
+  'microphone.svg',
+  'movie.svg',
+  'open_link.svg',
+  'play.svg',
+  'save.svg',
+  'shield.svg',
+  'timer.svg',
+  'video.svg',
+  'warning.svg',
+  'x.svg',
+]);
+
+// We add this to window here because the default Node context is erased at the end
+//   of preload.js processing
+window.setImmediate = window.nodeSetImmediate;
+window.globalOnlineStatus = true; // default to true as we don't get an event on app start
+window.getGlobalOnlineStatus = () => window.globalOnlineStatus;
+
+window.log.info('background page reloaded');
+window.log.info('environment:', window.getEnvironment());
+
+let newVersion = false;
+
+window.document.title = window.getTitle();
+
+// Whisper.events =
+// window.Whisper.Events = WhisperEvents ?
+const WhisperEvents = _.clone(Backbone.Events);
+
+window.log.info('Storage fetch');
+
+void Storage.fetch();
+
+function mapOldThemeToNew(theme: string) {
+  switch (theme) {
+    case 'dark':
+    case 'light':
+      return theme;
+    case 'android-dark':
+      return 'dark';
+    case 'android':
+    case 'ios':
+    default:
+      return 'light';
+  }
+}
+
+// We need this 'first' check because we don't want to start the app up any other time
+//   than the first time. And storage.fetch() will cause onready() to fire.
+let first = true;
+Storage.onready(async () => {
+  if (!first) {
+    return;
+  }
+  first = false;
+
+  // Update zoom
+  window.updateZoomFactor();
+
+  // Ensure accounts created prior to 1.0.0-beta8 do have their
+  // 'primaryDevicePubKey' defined.
+
+  if (Registration.isDone() && !Storage.get('primaryDevicePubKey')) {
+    await Storage.put('primaryDevicePubKey', getOurPubKeyStrFromCache());
+  }
+
+  // These make key operations available to IPC handlers created in preload.js
+  window.Events = {
+    getThemeSetting: () => Storage.get('theme-setting', 'light'),
+    setThemeSetting: async (value: any) => {
+      await Storage.put('theme-setting', value);
+    },
+    getHideMenuBar: () => Storage.get('hide-menu-bar'),
+    setHideMenuBar: async (value: boolean) => {
+      await Storage.put('hide-menu-bar', value);
+      window.setAutoHideMenuBar(false);
+      window.setMenuBarVisibility(!value);
+    },
+
+    getSpellCheck: () => Storage.get('spell-check', true),
+    setSpellCheck: async (value: boolean) => {
+      await Storage.put('spell-check', value);
+    },
+
+    shutdown: async () => {
+      // Stop background processing
+      AttachmentDownloads.stop();
+      // Stop processing incoming messages
+      // FIXME audric stop polling opengroupv2 and swarm nodes
+
+      // Shut down the data interface cleanly
+      await Data.shutdown();
+    },
+  };
+
+  const currentVersion = window.getVersion();
+  const lastVersion = Storage.get('version');
+  newVersion = !lastVersion || currentVersion !== lastVersion;
+  await Storage.put('version', currentVersion);
+
+  if (newVersion) {
+    window.log.info(`New version detected: ${currentVersion}; previous: ${lastVersion}`);
+
+    await Data.cleanupOrphanedAttachments();
+
+    await window.Signal.Logs.deleteAll();
+  }
+
+  const themeSetting = window.Events.getThemeSetting();
+  const newThemeSetting = mapOldThemeToNew(themeSetting);
+  window.Events.setThemeSetting(newThemeSetting);
+
+  try {
+    AttachmentDownloads.initAttachmentPaths();
+
+    await Promise.all([getConversationController().load(), BlockedNumberController.load()]);
+  } catch (error) {
+    window.log.error(
+      'main_start.js: ConversationController failed to load:',
+      error && error.stack ? error.stack : error
+    );
+  } finally {
+    void start();
+  }
+});
+
+function manageExpiringData() {
+  window.Signal.Data.cleanSeenMessages();
+  window.Signal.Data.cleanLastHashes();
+  setTimeout(manageExpiringData, 1000 * 60 * 60);
+}
+
+// tslint:disable-next-line: max-func-body-length
+async function start() {
+  manageExpiringData();
+  window.dispatchEvent(new Event('storage_ready'));
+
+  window.log.info('Cleanup: starting...');
+
+  const results = await Promise.all([window.Signal.Data.getOutgoingWithoutExpiresAt()]);
+
+  // Combine the models
+  const messagesForCleanup = results.reduce(
+    (array, current) => array.concat(current.toArray()),
+    []
+  );
+
+  window.log.info(`Cleanup: Found ${messagesForCleanup.length} messages for cleanup`);
+  await Promise.all(
+    messagesForCleanup.map(async (message: MessageModel) => {
+      const sentAt = message.get('sent_at');
+
+      if (message.hasErrors()) {
+        return;
+      }
+
+      window.log.info(`Cleanup: Deleting unsent message ${sentAt}`);
+      await window.Signal.Data.removeMessage(message.id);
+    })
+  );
+  window.log.info('Cleanup: complete');
+
+  window.log.info('listening for registration events');
+  WhisperEvents.on('registration_done', async () => {
+    window.log.info('handling registration event');
+
+    // Disable link previews as default per Kee
+    Storage.onready(async () => {
+      await Storage.put('link-preview-setting', false);
+    });
+
+    await connect();
+  });
+
+  const appView = new AppView({
+    el: $('body'),
+  });
+
+  ExpirationTimerOptions.initExpiringMessageListener();
+
+  if (Registration.isDone() && !isSignInByLinking()) {
+    await connect();
+    appView.openInbox();
+  } else {
+    appView.openStandalone();
+  }
+
+  window.addEventListener('focus', () => {
+    Notifications.clear();
+  });
+  window.addEventListener('unload', () => {
+    Notifications.fastClear();
+  });
+
+  // Set user's launch count.
+  const prevLaunchCount = window.getSettingValue('launch-count');
+  // tslint:disable-next-line: restrict-plus-operands
+  const launchCount = !prevLaunchCount ? 1 : prevLaunchCount + 1;
+  window.setSettingValue('launch-count', launchCount);
+
+  // On first launch
+  if (launchCount === 1) {
+    // Initialise default settings
+    window.setSettingValue('hide-menu-bar', true);
+    window.setSettingValue('link-preview-setting', false);
+  }
+
+  window.setTheme = newTheme => {
+    window.Events.setThemeSetting(newTheme);
+  };
+
+  window.toggleMenuBar = () => {
+    const current = window.getSettingValue('hide-menu-bar');
+    if (current === undefined) {
+      window.Events.setHideMenuBar(false);
+      return;
+    }
+
+    window.Events.setHideMenuBar(!current);
+  };
+
+  window.toggleSpellCheck = () => {
+    const currentValue = window.getSettingValue('spell-check');
+    // if undefined, it means 'default' so true. but we have to toggle it, so false
+    // if not undefined, we take the opposite
+    const newValue = currentValue !== undefined ? !currentValue : false;
+    window.Events.setSpellCheck(newValue);
+    window.libsession.Utils.ToastUtils.pushRestartNeeded();
+  };
+
+  window.toggleMediaPermissions = async () => {
+    const value = window.getMediaPermissions();
+
+    if (value === true) {
+      const valueCallPermissions = window.getCallMediaPermissions();
+      if (valueCallPermissions) {
+        window.log.info('toggleMediaPermissions : forcing callPermissions to false');
+
+        await window.toggleCallMediaPermissionsTo(false);
+      }
+    }
+
+    if (value === false && isMacOS()) {
+      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 (isMacOS()) {
+        window.askForMediaAccess();
+      }
+      window.log.info('toggleCallMediaPermissionsTo : forcing audio/video to true');
+      // turning ON "call permissions" forces turning on "audio/video permissions"
+      window.setMediaPermissions(true);
+    }
+    window.setCallMediaPermissions(enabled);
+  };
+
+  window.openFromNotification = async conversationKey => {
+    window.showWindow();
+    if (conversationKey) {
+      // do not put the messageId here so the conversation is loaded on the last unread instead
+      await window.openConversationWithMessages({ conversationKey, messageId: null });
+    } else {
+      appView.openInbox();
+    }
+  };
+
+  WhisperEvents.on('openInbox', () => {
+    appView.openInbox();
+  });
+}
+
+let disconnectTimer: number | null = null;
+function onOffline() {
+  window.log.info('offline');
+  window.globalOnlineStatus = false;
+
+  window.removeEventListener('offline', onOffline);
+  window.addEventListener('online', onOnline);
+
+  // We've received logs from Linux where we get an 'offline' event, then 30ms later
+  //   we get an online event. This waits a bit after getting an 'offline' event
+  //   before disconnecting the socket manually.
+  disconnectTimer = setTimeout(disconnect, 1000);
+}
+
+function onOnline() {
+  window.log.info('online');
+  window.globalOnlineStatus = true;
+
+  window.removeEventListener('online', onOnline);
+  window.addEventListener('offline', onOffline);
+
+  if (disconnectTimer) {
+    window.log.warn('Already online. Had a blip in online/offline status.');
+    clearTimeout(disconnectTimer);
+    disconnectTimer = null;
+    return;
+  }
+  if (disconnectTimer) {
+    clearTimeout(disconnectTimer);
+    disconnectTimer = null;
+  }
+
+  void connect();
+}
+
+function disconnect() {
+  window.log.info('disconnect');
+
+  // Clear timer, since we're only called when the timer is expired
+  disconnectTimer = null;
+  AttachmentDownloads.stop();
+}
+
+let connectCount = 0;
+async function connect() {
+  window.log.info('connect');
+
+  // Bootstrap our online/offline detection, only the first time we connect
+  if (connectCount === 0 && navigator.onLine) {
+    window.addEventListener('offline', onOffline);
+  }
+  if (connectCount === 0 && !navigator.onLine) {
+    window.log.warn('Starting up offline; will connect when we have network access');
+    window.addEventListener('online', onOnline);
+    onEmpty(); // this ensures that the loading screen is dismissed
+    return;
+  }
+
+  if (!Registration.everDone()) {
+    return;
+  }
+
+  connectCount += 1;
+  Notifications.disable(); // avoid notification flood until empty
+  setTimeout(() => {
+    Notifications.enable();
+  }, 10 * 1000); // 10 sec
+
+  await queueAllCached();
+  await AttachmentDownloads.start({
+    logger: window.log,
+  });
+
+  window.textsecure.messaging = true;
+}
+
+function onEmpty() {
+  window.readyForUpdates();
+
+  Notifications.enable();
+}
+
+class AppView extends Backbone.View {
+  private inboxView: any | null = null;
+  private standaloneView: any;
+
+  public initialize() {
+    this.inboxView = null;
+
+    const rtlLocales = ['fa', 'ar', 'he'];
+
+    const loc = (window.i18n as any).getLocale();
+    if (rtlLocales.includes(loc)) {
+      this.$el.addClass('rtl');
+    }
+    const hideMenuBar = Storage.get('hide-menu-bar', true) as boolean;
+    window.setAutoHideMenuBar(hideMenuBar);
+    window.setMenuBarVisibility(!hideMenuBar);
+  }
+  // events: {
+  //   openInbox: 'openInbox';
+  // };
+
+  public openView(view: any) {
+    // tslint:disable-next-line: no-inner-html
+    this.el.innerHTML = '';
+    this.el.append(view.el);
+    this.delegateEvents();
+  }
+
+  public openStandalone() {
+    this.resetViews();
+    this.standaloneView = SessionRegistrationView();
+    this.openView(this.standaloneView);
+  }
+
+  public closeStandalone() {
+    if (this.standaloneView) {
+      this.standaloneView.remove();
+      this.standaloneView = null;
+    }
+  }
+
+  public resetViews() {
+    this.closeStandalone();
+  }
+
+  public openInbox() {
+    if (!this.inboxView) {
+      this.inboxView = new SessionInboxView({
+        window,
+      });
+      return getConversationController()
+        .loadPromise()
+        ?.then(() => {
+          this.openView(this.inboxView);
+        });
+    }
+    if (!$.contains(this.el, this.inboxView.el)) {
+      this.openView(this.inboxView);
+    }
+    window.focus(); // FIXME
+    return Promise.resolve();
+  }
+}
+
+class TextScramble {
+  private frame: any;
+  private queue: any;
+  private readonly el: any;
+  private readonly chars: any;
+  private resolve: any;
+  private frameRequest: any;
+
+  constructor(el: any) {
+    this.el = el;
+    this.chars = '0123456789abcdef';
+    this.update = this.update.bind(this);
+  }
+  // tslint:disable: insecure-random
+
+  public async setText(newText: string) {
+    const oldText = this.el.value;
+    const length = Math.max(oldText.length, newText.length);
+    // eslint-disable-next-line no-return-assign
+    // tslint:disable-next-line: promise-must-complete
+    const promise = new Promise(resolve => (this.resolve = resolve));
+    this.queue = [];
+
+    for (let i = 0; i < length; i++) {
+      const from = oldText[i] || '';
+      const to = newText[i] || '';
+      const startNumber = Math.floor(Math.random() * 40);
+      const end = startNumber + Math.floor(Math.random() * 40);
+      this.queue.push({
+        from,
+        to,
+        start: startNumber,
+        end,
+      });
+    }
+
+    cancelAnimationFrame(this.frameRequest);
+    this.frame = 0;
+    this.update();
+    return promise;
+  }
+
+  public update() {
+    let output = '';
+    let complete = 0;
+
+    // tslint:disable-next-line: one-variable-per-declaration
+    for (let i = 0, n = this.queue.length; i < n; i++) {
+      const { from, to, start: startNumber, end } = this.queue[i];
+      let { char } = this.queue[i];
+
+      if (this.frame >= end) {
+        complete++;
+        output += to;
+      } else if (this.frame >= startNumber) {
+        if (!char || Math.random() < 0.28) {
+          char = this.randomChar();
+          this.queue[i].char = char;
+        }
+        output += char;
+      } else {
+        output += from;
+      }
+    }
+
+    this.el.value = output;
+
+    if (complete === this.queue.length) {
+      this.resolve();
+    } else {
+      this.frameRequest = requestAnimationFrame(this.update);
+      this.frame++;
+    }
+  }
+
+  public randomChar() {
+    return this.chars[Math.floor(Math.random() * this.chars.length)];
+  }
+}
+window.Session = window.Session || {};
+
+window.Session.setNewSessionID = (sessionID: string) => {
+  const el = document.querySelector('.session-id-editable-textarea');
+  const fx = new TextScramble(el);
+  if (el) {
+    (el as any).value = sessionID;
+  }
+  void fx.setText(sessionID);
+};
diff --git a/js/modules/signal.js b/js/modules/signal.js
index 01fe3f038..bebade759 100644
--- a/js/modules/signal.js
+++ b/js/modules/signal.js
@@ -5,23 +5,10 @@ const Data = require('../../ts/data/data');
 const OS = require('../../ts/OS');
 const Util = require('../../ts/util');
 
-// Components
-const {
-  SessionRegistrationView,
-} = require('../../ts/components/registration/SessionRegistrationView');
-
-const { SessionInboxView } = require('../../ts/components/SessionInboxView');
-
 exports.setup = () => {
   Data.init();
 
-  const Components = {
-    SessionInboxView,
-    SessionRegistrationView,
-  };
-
   return {
-    Components,
     Crypto,
     Data,
     OS,
diff --git a/js/views/app_view.js b/js/views/app_view.js
deleted file mode 100644
index f49efac2c..000000000
--- a/js/views/app_view.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/* global Backbone, Whisper, storage, _, $ */
-
-/* eslint-disable more/no-then */
-
-// eslint-disable-next-line func-names
-(function() {
-  'use strict';
-
-  window.Whisper = window.Whisper || {};
-
-  Whisper.AppView = Backbone.View.extend({
-    initialize() {
-      this.inboxView = null;
-
-      this.applyRtl();
-      this.applyHideMenu();
-    },
-    events: {
-      openInbox: 'openInbox',
-    },
-    applyRtl() {
-      const rtlLocales = ['fa', 'ar', 'he'];
-
-      const loc = window.i18n.getLocale();
-      if (rtlLocales.includes(loc)) {
-        this.$el.addClass('rtl');
-      }
-    },
-    applyHideMenu() {
-      const hideMenuBar = storage.get('hide-menu-bar', true);
-      window.setAutoHideMenuBar(hideMenuBar);
-      window.setMenuBarVisibility(!hideMenuBar);
-    },
-    openView(view) {
-      this.el.innerHTML = '';
-      this.el.append(view.el);
-      this.delegateEvents();
-    },
-    openDebugLog() {
-      this.closeDebugLog();
-      this.debugLogView = new Whisper.DebugLogView();
-      this.debugLogView.$el.appendTo(this.el);
-    },
-    closeDebugLog() {
-      if (this.debugLogView) {
-        this.debugLogView.remove();
-        this.debugLogView = null;
-      }
-    },
-    openStandalone() {
-      this.resetViews();
-      this.standaloneView = new Whisper.SessionRegistrationView();
-      this.openView(this.standaloneView);
-    },
-    closeStandalone() {
-      if (this.standaloneView) {
-        this.standaloneView.remove();
-        this.standaloneView = null;
-      }
-    },
-    resetViews() {
-      this.closeStandalone();
-    },
-    openInbox(options = {}) {
-      _.defaults(options, { initialLoadComplete: this.initialLoadComplete });
-
-      if (!this.inboxView) {
-        // We create the inbox immediately so we don't miss an update to
-        //   this.initialLoadComplete between the start of this method and the
-        //   creation of inboxView.
-        this.inboxView = new Whisper.InboxView({
-          window,
-          initialLoadComplete: options.initialLoadComplete,
-        });
-        return window
-          .getConversationController()
-          .loadPromise()
-          .then(() => {
-            this.openView(this.inboxView);
-          });
-      }
-      if (!$.contains(this.el, this.inboxView.el)) {
-        this.openView(this.inboxView);
-      }
-      window.focus(); // FIXME
-      return Promise.resolve();
-    },
-  });
-})();
diff --git a/js/views/debug_log_view.js b/js/views/debug_log_view.js
deleted file mode 100644
index 3b106dfa3..000000000
--- a/js/views/debug_log_view.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* global i18n: false */
-/* global Whisper: false */
-
-// eslint-disable-next-line func-names
-(function() {
-  'use strict';
-
-  window.Whisper = window.Whisper || {};
-
-  Whisper.DebugLogLinkView = Whisper.View.extend({
-    templateName: 'debug-log-link',
-    initialize(options) {
-      this.url = options.url;
-    },
-    render_attributes() {
-      return {
-        url: this.url,
-        reportIssue: i18n('reportIssue'),
-      };
-    },
-  });
-  Whisper.DebugLogView = Whisper.View.extend({
-    templateName: 'debug-log',
-    className: 'debug-log modal',
-    initialize() {
-      this.render();
-      this.$('textarea').val(i18n('loading'));
-
-      const operatingSystemInfo = `Operating System: ${window.getOSRelease()}`;
-
-      const commitHashInfo = window.getCommitHash() ? `Commit Hash: ${window.getCommitHash()}` : '';
-
-      // eslint-disable-next-line more/no-then
-      window.log.fetch().then(text => {
-        const debugLogWithSystemInfo = operatingSystemInfo + commitHashInfo + text;
-
-        this.$('textarea').val(debugLogWithSystemInfo);
-      });
-    },
-    events: {
-      'click .submit': 'saveLogToDesktop',
-      'click .close': 'close',
-    },
-    render_attributes: {
-      title: i18n('debugLog'),
-      cancel: i18n('cancel'),
-      saveLogToDesktop: i18n('saveLogToDesktop'),
-      debugLogExplanation: i18n('debugLogExplanation'),
-    },
-    close() {
-      window.closeDebugLog();
-    },
-    async saveLogToDesktop(e) {
-      e.preventDefault();
-      const text = this.$('textarea').val();
-      if (text.length === 0) {
-        return;
-      }
-      window.saveLog(text);
-      window.closeDebugLog();
-    },
-  });
-})();
diff --git a/js/views/session_inbox_view.js b/js/views/session_inbox_view.js
deleted file mode 100644
index 7d17748ce..000000000
--- a/js/views/session_inbox_view.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/* global Whisper */
-
-// eslint-disable-next-line func-names
-(function() {
-  'use strict';
-
-  window.Whisper = window.Whisper || {};
-
-  Whisper.InboxView = Whisper.View.extend({
-    initialize() {
-      this.render();
-    },
-
-    render() {
-      this.dialogView = new Whisper.ReactWrapperView({
-        className: 'inbox index',
-        Component: window.Signal.Components.SessionInboxView,
-      });
-
-      this.$el.append(this.dialogView.el);
-      return this;
-    },
-
-    close() {
-      this.remove();
-    },
-  });
-})();
diff --git a/js/views/session_registration_view.js b/js/views/session_registration_view.js
deleted file mode 100644
index 98b33852f..000000000
--- a/js/views/session_registration_view.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/* eslint-disable no-plusplus */
-/* global
- Whisper,
-*/
-
-/* eslint-disable more/no-then */
-
-// eslint-disable-next-line func-names
-(function() {
-  'use strict';
-
-  window.Whisper = window.Whisper || {};
-
-  Whisper.SessionRegistrationView = Whisper.View.extend({
-    className: 'session-fullscreen',
-    initialize() {
-      this.render();
-    },
-    render() {
-      this.session_registration_view = new Whisper.ReactWrapperView({
-        className: 'session-full-screen-flow session-fullscreen',
-        Component: window.Signal.Components.SessionRegistrationView,
-        props: {},
-      });
-
-      this.$el.append(this.session_registration_view.el);
-      return this;
-    },
-
-    log(s) {
-      window.log.info(s);
-      this.$('#status').text(s);
-    },
-  });
-
-  class TextScramble {
-    constructor(el) {
-      this.el = el;
-      this.chars = '0123456789abcdef';
-      this.update = this.update.bind(this);
-    }
-
-    setText(newText) {
-      const oldText = this.el.value;
-      const length = Math.max(oldText.length, newText.length);
-      // eslint-disable-next-line no-return-assign
-      const promise = new Promise(resolve => (this.resolve = resolve));
-      this.queue = [];
-
-      for (let i = 0; i < length; i++) {
-        const from = oldText[i] || '';
-        const to = newText[i] || '';
-        const start = Math.floor(Math.random() * 40);
-        const end = start + Math.floor(Math.random() * 40);
-        this.queue.push({
-          from,
-          to,
-          start,
-          end,
-        });
-      }
-
-      cancelAnimationFrame(this.frameRequest);
-      this.frame = 0;
-      this.update();
-      return promise;
-    }
-
-    update() {
-      let output = '';
-      let complete = 0;
-
-      for (let i = 0, n = this.queue.length; i < n; i++) {
-        const { from, to, start, end } = this.queue[i];
-        let { char } = this.queue[i];
-
-        if (this.frame >= end) {
-          complete++;
-          output += to;
-        } else if (this.frame >= start) {
-          if (!char || Math.random() < 0.28) {
-            char = this.randomChar();
-            this.queue[i].char = char;
-          }
-          output += char;
-        } else {
-          output += from;
-        }
-      }
-
-      this.el.value = output;
-
-      if (complete === this.queue.length) {
-        this.resolve();
-      } else {
-        this.frameRequest = requestAnimationFrame(this.update);
-        this.frame++;
-      }
-    }
-
-    randomChar() {
-      return this.chars[Math.floor(Math.random() * this.chars.length)];
-    }
-  }
-  window.Session = window.Session || {};
-
-  window.Session.setNewSessionID = sessionID => {
-    const el = document.querySelector('.session-id-editable-textarea');
-    const fx = new TextScramble(el);
-    el.value = sessionID;
-    fx.setText(sessionID);
-  };
-})();
diff --git a/js/views/whisper_view.js b/js/views/whisper_view.js
deleted file mode 100644
index 121277475..000000000
--- a/js/views/whisper_view.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/* global Whisper, Backbone, Mustache, _, $ */
-
-/*
- * Whisper.View
- *
- * This is the base for most of our views. The Backbone view is extended
- * with some conveniences:
- *
- * 1. Pre-parses all our mustache templates for performance.
- * https://github.com/janl/mustache.js#pre-parsing-and-caching-templates
- *
- * 2. Defines a default definition for render() which allows sub-classes
- * to simply specify a templateName and renderAttributes which are plugged
- * into Mustache.render
- *
- * 3. Makes all the templates available for rendering as partials.
- * https://github.com/janl/mustache.js#partials
- *
- * 4. Provides some common functionality, e.g. confirmation dialog
- *
- */
-
-// eslint-disable-next-line func-names
-(function() {
-  'use strict';
-
-  window.Whisper = window.Whisper || {};
-
-  Whisper.View = Backbone.View.extend(
-    {
-      constructor(...params) {
-        Backbone.View.call(this, ...params);
-        Mustache.parse(_.result(this, 'template'));
-      },
-      render_attributes() {
-        return _.result(this.model, 'attributes', {});
-      },
-      render_partials() {
-        return Whisper.View.Templates;
-      },
-      template() {
-        if (this.templateName) {
-          return Whisper.View.Templates[this.templateName];
-        }
-        return '';
-      },
-      render() {
-        const attrs = _.result(this, 'render_attributes', {});
-        const template = _.result(this, 'template', '');
-        const partials = _.result(this, 'render_partials', '');
-        this.$el.html(Mustache.render(template, attrs, partials));
-        return this;
-      },
-      confirm(message, okText) {
-        return new Promise((resolve, reject) => {
-          window.confirmationDialog({
-            title: message,
-            okText,
-            resolve,
-            reject,
-          });
-        });
-      },
-    },
-    {
-      // Class attributes
-      Templates: (() => {
-        const templates = {};
-        $('script[type="text/x-tmpl-mustache"]').each((i, el) => {
-          const $el = $(el);
-          const id = $el.attr('id');
-          templates[id] = $el.html();
-        });
-        return templates;
-      })(),
-    }
-  );
-})();
diff --git a/js/views/whisper_view.ts b/js/views/whisper_view.ts
new file mode 100644
index 000000000..9e2c2dad7
--- /dev/null
+++ b/js/views/whisper_view.ts
@@ -0,0 +1,68 @@
+/* global Whisper, Backbone, Mustache, _, $ */
+
+/*
+ * Whisper.View
+ *
+ * This is the base for most of our views. The Backbone view is extended
+ * with some conveniences:
+ *
+ * 1. Pre-parses all our mustache templates for performance.
+ * https://github.com/janl/mustache.js#pre-parsing-and-caching-templates
+ *
+ * 2. Defines a default definition for render() which allows sub-classes
+ * to simply specify a templateName and renderAttributes which are plugged
+ * into Mustache.render
+ *
+ * 3. Makes all the templates available for rendering as partials.
+ * https://github.com/janl/mustache.js#partials
+ *
+ * 4. Provides some common functionality, e.g. confirmation dialog
+ *
+ */
+
+// eslint-disable-next-line func-names
+(function() {
+  'use strict';
+
+  window.Whisper = window.Whisper || {};
+
+  // window.Whisper.View = Backbone.View.extend(
+  //   {
+  //     constructor(...params: any) {
+  //       Backbone.View.call(this, ...params);
+  //       Mustache.parse(_.result(this, 'template'));
+  //     },
+  //     render_attributes() {
+  //       return _.result(this.model, 'attributes', {});
+  //     },
+  //     render_partials() {
+  //       return window.Whisper.View.Templates;
+  //     },
+  //     template() {
+  //       if (this.templateName) {
+  //         return window.Whisper.View.Templates[this.templateName];
+  //       }
+  //       return '';
+  //     },
+  //     render() {
+  //       const attrs = _.result(this, 'render_attributes', {});
+  //       const template = _.result(this, 'template', '');
+  //       const partials = _.result(this, 'render_partials', '');
+  //       this.$el.html(Mustache.render(template, attrs, partials));
+  //       return this;
+  //     },
+  //   },
+  //   {
+  //     // Class attributes
+  //     Templates: (() => {
+  //       const templates = {};
+  //       $('script[type="text/x-tmpl-mustache"]').each((i, el) => {
+  //         const $el = $(el);
+  //         const id = $el.attr('id');
+  //         templates[id] = $el.html();
+  //       });
+  //       return templates;
+  //     })(),
+  //   }
+  // );
+})();
diff --git a/preload.js b/preload.js
index 3d5f6bbbf..9266d3311 100644
--- a/preload.js
+++ b/preload.js
@@ -60,7 +60,6 @@ window.setPassword = (passPhrase, oldPhrase) =>
       if (error) {
         return reject(error);
       }
-      Whisper.events.trigger('password-updated');
       return resolve();
     });
     ipc.send('set-password', passPhrase, oldPhrase);
diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss
index 3ec76b9d1..3ef10e928 100644
--- a/stylesheets/_modal.scss
+++ b/stylesheets/_modal.scss
@@ -178,14 +178,6 @@
   }
 }
 
-.permissions-popup,
-.debug-log-window {
-  .modal {
-    background-color: transparent;
-    padding: 0;
-  }
-}
-
 .loki-dialog {
   & ~ .index.inbox {
     // filter: blur(2px); // FIXME enable back once modals are moved to react
diff --git a/ts/components/SessionInboxView.tsx b/ts/components/SessionInboxView.tsx
index 5c7374e4d..7dbcef691 100644
--- a/ts/components/SessionInboxView.tsx
+++ b/ts/components/SessionInboxView.tsx
@@ -57,15 +57,17 @@ export class SessionInboxView extends React.Component<any, State> {
     window.persistStore = persistor;
 
     return (
-      <Provider store={this.store}>
-        <PersistGate loading={null} persistor={persistor}>
-          <div className="gutter">
-            <div className="network-status-container" />
-            {this.renderLeftPane()}
-          </div>
-          <SessionMainPanel />
-        </PersistGate>
-      </Provider>
+      <div className="inbox index">
+        <Provider store={this.store}>
+          <PersistGate loading={null} persistor={persistor}>
+            <div className="gutter">
+              <div className="network-status-container" />
+              {this.renderLeftPane()}
+            </div>
+            <SessionMainPanel />
+          </PersistGate>
+        </Provider>
+      </div>
     );
   }
 
diff --git a/ts/components/registration/SessionRegistrationView.tsx b/ts/components/registration/SessionRegistrationView.tsx
index 9e2b77faa..d8f913d04 100644
--- a/ts/components/registration/SessionRegistrationView.tsx
+++ b/ts/components/registration/SessionRegistrationView.tsx
@@ -13,22 +13,26 @@ export const SessionRegistrationView = () => {
     void setSignInByLinking(false);
   }, []);
   return (
-    <SessionTheme>
-      <Flex
-        className="session-content"
-        alignItems="center"
-        flexDirection="column"
-        container={true}
-        height="100%"
-      >
-        <Flex container={true} margin="auto" alignItems="center" flexDirection="column">
-          <SessionToastContainer />
-          <SessionIcon iconSize={150} iconType="brand" />
+    <div className="session-fullscreen">
+      <div className="session-full-screen-flow session-fullscreen">
+        <SessionTheme>
+          <Flex
+            className="session-content"
+            alignItems="center"
+            flexDirection="column"
+            container={true}
+            height="100%"
+          >
+            <Flex container={true} margin="auto" alignItems="center" flexDirection="column">
+              <SessionToastContainer />
+              <SessionIcon iconSize={150} iconType="brand" />
 
-          <AccentText />
-          <RegistrationStages />
-        </Flex>
-      </Flex>
-    </SessionTheme>
+              <AccentText />
+              <RegistrationStages />
+            </Flex>
+          </Flex>
+        </SessionTheme>
+      </div>
+    </div>
   );
 };
diff --git a/ts/views/DebugLogView.tsx b/ts/views/DebugLogView.tsx
new file mode 100644
index 000000000..b1f45be94
--- /dev/null
+++ b/ts/views/DebugLogView.tsx
@@ -0,0 +1,55 @@
+import React, { useEffect, useState } from 'react';
+
+export const DebugLogView = () => {
+  const [content, setContent] = useState(window.i18n('loading'));
+  useEffect(() => {
+    const operatingSystemInfo = `Operating System: ${(window as any).getOSRelease()}`;
+
+    const commitHashInfo = (window as any).getCommitHash()
+      ? `Commit Hash: ${(window as any).getCommitHash()}`
+      : '';
+
+    // eslint-disable-next-line more/no-then
+    window.log.fetch().then((text: string) => {
+      const debugLogWithSystemInfo = operatingSystemInfo + commitHashInfo + text;
+
+      setContent(debugLogWithSystemInfo);
+    });
+  }, []);
+
+  return (
+    <div className="content">
+      <div>
+        <button
+          className="x close"
+          aria-label="close debug log"
+          onClick={() => {
+            (window as any).closeDebugLog();
+          }}
+        />
+        <h1> {window.i18n('debugLog')} </h1>
+        <p> {window.i18n('debugLogExplanation')}</p>
+      </div>
+      <textarea spellCheck="false" rows={5}>
+        {content}
+      </textarea>
+      <div className="buttons">
+        <button
+          className="grey submit"
+          onClick={e => {
+            e.preventDefault();
+
+            if (content.length <= 20) {
+              // loading
+              return;
+            }
+            (window as any).saveLog(content);
+            (window as any).closeDebugLog();
+          }}
+        >
+          {window.i18n('saveLogToDesktop')}
+        </button>
+      </div>
+    </div>
+  );
+};
diff --git a/ts/webworker/master.ts b/ts/webworker/master.ts
index ca12a55ef..3c36d1518 100644
--- a/ts/webworker/master.ts
+++ b/ts/webworker/master.ts
@@ -1,4 +1,4 @@
-export async function yo() {
+export function yo() {
   const worker = new Worker('./ts/webworker/workers/auth.worker.js', { type: 'module' });
   worker.postMessage({
     question: 'The Answer to the Ultimate Question of Life, The Universe, and Everything.',
diff --git a/ts/window.d.ts b/ts/window.d.ts
index adc7bf3b4..8c6387576 100644
--- a/ts/window.d.ts
+++ b/ts/window.d.ts
@@ -68,6 +68,18 @@ declare global {
     getEnvironment: () => string;
     getNodeVersion: () => string;
 
+    showWindow: () => void;
+    setCallMediaPermissions: (val: boolean) => void;
+    setMediaPermissions: (val: boolean) => void;
+    askForMediaAccess: () => void;
+    getMediaPermissions: () => boolean;
+    nodeSetImmediate: any;
+    globalOnlineStatus: boolean;
+
+    getTitle: () => string;
+    getVersion: () => string;
+    setAutoHideMenuBar: (val: boolean) => void;
+    setMenuBarVisibility: (val: boolean) => void;
     contextMenuShown: boolean;
     inboxStore?: Store;
     openConversationWithMessages: (args: {
diff --git a/tsconfig.json b/tsconfig.json
index 720b9e59f..dfca4df84 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,56 +1,26 @@
 {
   "compilerOptions": {
     // Basic Options
-    "target": "es2020", // Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'.
+    "target": "es6", // Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'.
     "module": "commonjs", // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
-    // Specify library files to be included in the compilation.
     "lib": [
       "dom", // Required to access `window`
       "es2020"
     ],
-    // "allowJs": true,                       // Allow javascript files to be compiled.
-    // "checkJs": true,                       // Report errors in .js files.
     "jsx": "react", // Specify JSX code generation: 'preserve', 'react-native', or 'react'.
-    // "declaration": true,                   // Generates corresponding '.d.ts' file.
-    "sourceMap": true, // Generates corresponding '.map' file.
-    // "outFile": "./",                       // Concatenate and emit output to single file.
-    // "outDir": "./",                        // Redirect output structure to the directory.
+
     "rootDir": "./", // Specify the root directory of input files. Use to control the output directory structure with --outDir.
-    // "removeComments": true,                // Do not emit comments to output.
-    // "noEmit": true,                        // Do not emit outputs.
-    // "importHelpers": true,                 // Import emit helpers from 'tslib'.
-    // "downlevelIteration": true,            // Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.
-    // "isolatedModules": true,               // Transpile each file as a separate module (similar to 'ts.transpileModule').
 
-    // Strict Type-Checking Options
     "strict": true, // Enable all strict type-checking options.
     "skipLibCheck": true,
-    // Additional Checks
     "noUnusedLocals": true, // Report errors on unused locals.
     "noUnusedParameters": true, // Report errors on unused parameters.
     "noImplicitReturns": true, // Report error when not all code paths in function return a value.
     "noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch statement.
 
-    // Module Resolution Options
     "moduleResolution": "node", // Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).
-    // "baseUrl": "./",                       // Base directory to resolve non-absolute module names.
-    // "paths": {},                           // A series of entries which re-map imports to lookup locations relative to the 'baseUrl'.
-    // "rootDirs": [],                        // List of root folders whose combined content represents the structure of the project at runtime.
-    // "typeRoots": [],                       // List of folders to include type definitions from.
-    // "types": [],                           // Type declaration files to be included in compilation.
-    // "allowSyntheticDefaultImports": true,  // Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
     "useUnknownInCatchVariables": false,
-    "esModuleInterop": true // Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'.
-    // "preserveSymlinks": true,              // Do not resolve the real path of symlinks.
-
-    // Source Map Options
-    // "sourceRoot": "./",                    // Specify the location where debugge should locate TypeScript files instead of source locations.
-    // "mapRoot": "./",                       // Specify the location where debugge should locate map files instead of generated locations.
-    // "inlineSourceMap": true,               // Emit a single file with source maps instead of having a separate file.
-    // "inlineSources": true,                 // Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set.
-
-    // Experimental Options
-    // "experimentalDecorators": true,        // Enables experimental support for ES7 decorators.
-    // "emitDecoratorMetadata": true,         // Enables experimental support for emitting type metadata for decorators.
+    "esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'.
+    "inlineSourceMap": true // Emit a single file with source maps instead of having a separate file.
   }
 }