From 51575d2f565892d64d9a0a21f6efc12b2ad13242 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 24 Mar 2022 11:15:21 +1100 Subject: [PATCH] moved views to TS but broken --- .gitignore | 2 +- background.html | 8 - debug_log.html | 35 +- debug_log_preload.js | 7 + js/debug_log_start.js | 8 +- js/main_start.js | 430 ------------- js/main_start.ts | 569 ++++++++++++++++++ js/modules/signal.js | 13 - js/views/app_view.js | 89 --- js/views/debug_log_view.js | 63 -- js/views/session_inbox_view.js | 28 - js/views/session_registration_view.js | 113 ---- js/views/whisper_view.js | 78 --- js/views/whisper_view.ts | 68 +++ preload.js | 1 - stylesheets/_modal.scss | 8 - ts/components/SessionInboxView.tsx | 20 +- .../registration/SessionRegistrationView.tsx | 36 +- ts/views/DebugLogView.tsx | 55 ++ ts/webworker/master.ts | 2 +- ts/window.d.ts | 12 + tsconfig.json | 40 +- 22 files changed, 754 insertions(+), 931 deletions(-) delete mode 100644 js/main_start.js create mode 100644 js/main_start.ts delete mode 100644 js/views/app_view.js delete mode 100644 js/views/debug_log_view.js delete mode 100644 js/views/session_inbox_view.js delete mode 100644 js/views/session_registration_view.js delete mode 100644 js/views/whisper_view.js create mode 100644 js/views/whisper_view.ts create mode 100644 ts/views/DebugLogView.tsx 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 @@ - - - - - - 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 @@ - - - - - - + +
+ + 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(, 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) { + // 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 { window.persistStore = persistor; return ( - - -
-
- {this.renderLeftPane()} -
- - - +
+ + +
+
+ {this.renderLeftPane()} +
+ + + +
); } 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 ( - - - - - +
+
+ + + + + - - - - - + + + + + +
+
); }; 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 ( +
+
+
+ +
+ +
+
+ ); +}; 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 + "rootDir": "./", // Specify the root directory of input files. Use to control the output directory structure with --outDir. + "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. } }