From 173771d34b61b5ec528bfb95b183fe0240819da7 Mon Sep 17 00:00:00 2001
From: Josh Perez <60019601+josh-signal@users.noreply.github.com>
Date: Mon, 14 Jun 2021 15:01:00 -0400
Subject: [PATCH] New top-level React root:
---
CONTRIBUTING.md | 2 +-
background.html | 3 +-
js/modules/signal.js | 4 +
js/views/app_view.js | 154 ----------------------
js/views/inbox_view.js | 46 +++----
stylesheets/_global.scss | 16 +--
stylesheets/components/App.scss | 12 ++
stylesheets/manifest.scss | 1 +
ts/background.ts | 79 ++++--------
ts/components/App.tsx | 42 ++++++
ts/components/BackboneHost.tsx | 35 +++++
ts/components/Inbox.tsx | 48 +++++++
ts/components/Install.tsx | 11 ++
ts/components/StandaloneRegistration.tsx | 11 ++
ts/state/actions.ts | 2 +
ts/state/ducks/app.ts | 155 +++++++++++++++++++++++
ts/state/reducer.ts | 2 +
ts/state/roots/createApp.tsx | 15 +++
ts/state/smart/App.ts | 21 +++
ts/state/types.ts | 2 +
ts/util/lint/exceptions.json | 52 ++++----
ts/window.d.ts | 10 +-
22 files changed, 457 insertions(+), 266 deletions(-)
delete mode 100644 js/views/app_view.js
create mode 100644 stylesheets/components/App.scss
create mode 100644 ts/components/App.tsx
create mode 100644 ts/components/BackboneHost.tsx
create mode 100644 ts/components/Inbox.tsx
create mode 100644 ts/components/Install.tsx
create mode 100644 ts/components/StandaloneRegistration.tsx
create mode 100644 ts/state/ducks/app.ts
create mode 100644 ts/state/roots/createApp.tsx
create mode 100644 ts/state/smart/App.ts
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4ef4f8a089b5..09b1d3fe9ab1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -258,7 +258,7 @@ and [iOS](https://github.com/signalapp/Signal-iOS/blob/master/BUILDING.md) proje
Then you can set up your development build of Signal Desktop as normal. If you've already
set up as a standalone install, you can switch by opening the DevTools (View -> Toggle
-Developer Tools) and entering this into the Console and pressing enter: `window.owsDesktopApp.appView.openInstaller();`
+Developer Tools) and entering this into the Console and pressing enter: `window.reduxActions.app.openInstaller();`
## Changing to production
diff --git a/background.html b/background.html
index 983c7b127b03..e5e3aad23256 100644
--- a/background.html
+++ b/background.html
@@ -333,7 +333,7 @@
-
+
@@ -368,7 +368,6 @@
-
diff --git a/js/modules/signal.js b/js/modules/signal.js
index 1cd9303209fd..d09a212f9713 100644
--- a/js/modules/signal.js
+++ b/js/modules/signal.js
@@ -80,6 +80,7 @@ const {
const {
createConversationHeader,
} = require('../../ts/state/roots/createConversationHeader');
+const { createApp } = require('../../ts/state/roots/createApp');
const { createCallManager } = require('../../ts/state/roots/createCallManager');
const {
createForwardMessageModal,
@@ -120,6 +121,7 @@ const {
} = require('../../ts/state/roots/createShortcutGuideModal');
const { createStore } = require('../../ts/state/createStore');
+const appDuck = require('../../ts/state/ducks/app');
const callingDuck = require('../../ts/state/ducks/calling');
const conversationsDuck = require('../../ts/state/ducks/conversations');
const emojisDuck = require('../../ts/state/ducks/emojis');
@@ -356,6 +358,7 @@ exports.setup = (options = {}) => {
};
const Roots = {
+ createApp,
createCallManager,
createChatColorPicker,
createCompositionArea,
@@ -379,6 +382,7 @@ exports.setup = (options = {}) => {
};
const Ducks = {
+ app: appDuck,
calling: callingDuck,
conversations: conversationsDuck,
emojis: emojisDuck,
diff --git a/js/views/app_view.js b/js/views/app_view.js
deleted file mode 100644
index ef812c6fc49c..000000000000
--- a/js/views/app_view.js
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright 2017-2021 Signal Messenger, LLC
-// SPDX-License-Identifier: AGPL-3.0-only
-
-/* global Backbone, Whisper, storage, _, ConversationController, $ */
-
-/* eslint-disable more/no-then */
-
-// eslint-disable-next-line func-names
-(function () {
- window.Whisper = window.Whisper || {};
-
- function resolveTheme() {
- const theme = storage.get('theme-setting') || 'system';
- if (theme === 'system') {
- return window.systemTheme;
- }
- return theme;
- }
-
- Whisper.AppView = Backbone.View.extend({
- initialize() {
- this.inboxView = null;
- this.installView = null;
-
- this.applyTheme();
- this.applyHideMenu();
-
- window.subscribeToSystemThemeChange(() => {
- this.applyTheme();
- });
- },
- events: {
- openInbox: 'openInbox',
- },
- applyTheme() {
- const theme = resolveTheme();
- this.$el
- .removeClass('light-theme')
- .removeClass('dark-theme')
- .addClass(`${theme}-theme`);
- },
- applyHideMenu() {
- const hideMenuBar = storage.get('hide-menu-bar', false);
- 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;
- }
- },
- openInstaller(options = {}) {
- window.addSetupMenuItems();
-
- this.resetViews();
- const installView = new Whisper.InstallView(options);
- this.installView = installView;
-
- this.openView(this.installView);
- },
- closeInstaller() {
- if (this.installView) {
- this.installView.remove();
- this.installView = null;
- }
- },
- openStandalone() {
- if (window.getEnvironment() !== 'production') {
- window.addSetupMenuItems();
- this.resetViews();
- this.standaloneView = new Whisper.StandaloneRegistrationView();
- this.openView(this.standaloneView);
- }
- },
- closeStandalone() {
- if (this.standaloneView) {
- this.standaloneView.remove();
- this.standaloneView = null;
- }
- },
- resetViews() {
- this.closeInstaller();
- this.closeStandalone();
- },
- openInbox(options = {}) {
- // The inbox can be created before the 'empty' event fires or afterwards. If
- // before, it's straightforward: the onEmpty() handler below updates the
- // view directly, and we're in good shape. If we create the inbox late, we
- // need to be sure that the current value of initialLoadComplete is provided
- // so its loading screen doesn't stick around forever.
-
- // Two primary techniques at play for this situation:
- // - background.js has two openInbox() calls, and passes initalLoadComplete
- // directly via the options parameter.
- // - in other situations openInbox() will be called with no options. So this
- // view keeps track of whether onEmpty() has ever been called with
- // this.initialLoadComplete. An example of this: on a phone-pairing setup.
- _.defaults(options, { initialLoadComplete: this.initialLoadComplete });
-
- window.log.info('open inbox');
- this.closeInstaller();
-
- 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 ConversationController.loadPromise().then(() => {
- this.openView(this.inboxView);
- });
- }
- if (!$.contains(this.el, this.inboxView.el)) {
- this.openView(this.inboxView);
- }
-
- return Promise.resolve();
- },
- onEmpty() {
- const view = this.inboxView;
-
- this.initialLoadComplete = true;
- if (view) {
- view.onEmpty();
- }
- },
- onProgress(count) {
- const view = this.inboxView;
- if (view) {
- view.onProgress(count);
- }
- },
- openConversation(id, messageId) {
- if (id) {
- this.openInbox().then(() => {
- this.inboxView.openConversation(id, messageId);
- });
- }
- },
- });
-})();
diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js
index ac652f598f9c..3d9bbef8b7fb 100644
--- a/js/views/inbox_view.js
+++ b/js/views/inbox_view.js
@@ -106,6 +106,30 @@
this.conversation_stack.unload();
});
+ window.Whisper.events.on('showConversation', async (id, messageId) => {
+ const conversation = await ConversationController.getOrCreateAndWait(
+ id,
+ 'private'
+ );
+
+ conversation.setMarkedUnread(false);
+
+ const { openConversationExternal } = window.reduxActions.conversations;
+ if (openConversationExternal) {
+ openConversationExternal(conversation.id, messageId);
+ }
+
+ this.conversation_stack.open(conversation, messageId);
+ this.focusConversation();
+ });
+
+ window.Whisper.events.on('loadingProgress', count => {
+ const view = this.appLoadingScreen;
+ if (view) {
+ view.updateProgress(count);
+ }
+ });
+
if (!options.initialLoadComplete) {
this.appLoadingScreen = new Whisper.AppLoadingScreen();
this.appLoadingScreen.render();
@@ -209,12 +233,6 @@
}
}
},
- onProgress(count) {
- const view = this.appLoadingScreen;
- if (view) {
- view.updateProgress(count);
- }
- },
focusConversation(e) {
if (e && this.$(e.target).closest('.placeholder').length) {
return;
@@ -231,22 +249,6 @@
reloadBackgroundPage() {
window.location.reload();
},
- async openConversation(id, messageId) {
- const conversation = await ConversationController.getOrCreateAndWait(
- id,
- 'private'
- );
-
- conversation.setMarkedUnread(false);
-
- const { openConversationExternal } = window.reduxActions.conversations;
- if (openConversationExternal) {
- openConversationExternal(conversation.id, messageId);
- }
-
- this.conversation_stack.open(conversation, messageId);
- this.focusConversation();
- },
closeRecording(e) {
if (e && this.$(e.target).closest('.capture-audio').length > 0) {
return;
diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss
index 107c5e12fb95..9fb0c183c89c 100644
--- a/stylesheets/_global.scss
+++ b/stylesheets/_global.scss
@@ -26,15 +26,15 @@ body {
--title-bar-drag-area-height: 28px;
--draggable-app-region: drag;
}
-}
-body.light-theme {
- background-color: $color-white;
- color: $color-gray-90;
-}
-body.dark-theme {
- background-color: $color-gray-95;
- color: $color-gray-05;
+ &.light-theme {
+ background-color: $color-white;
+ color: $color-gray-90;
+ }
+ &.dark-theme {
+ background-color: $color-gray-95;
+ color: $color-gray-05;
+ }
}
::-webkit-scrollbar {
diff --git a/stylesheets/components/App.scss b/stylesheets/components/App.scss
new file mode 100644
index 000000000000..7238b2a7660e
--- /dev/null
+++ b/stylesheets/components/App.scss
@@ -0,0 +1,12 @@
+.App {
+ height: 100%;
+
+ &.light-theme {
+ background-color: $color-white;
+ color: $color-gray-90;
+ }
+ &.dark-theme {
+ background-color: $color-gray-95;
+ color: $color-gray-05;
+ }
+}
diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss
index 69413fa1fa54..3544dbc6f3e9 100644
--- a/stylesheets/manifest.scss
+++ b/stylesheets/manifest.scss
@@ -28,6 +28,7 @@
// New style: components
@import './components/AddGroupMembersModal.scss';
+@import './components/App.scss';
@import './components/Avatar.scss';
@import './components/AvatarInput.scss';
@import './components/Button.scss';
diff --git a/ts/background.ts b/ts/background.ts
index f2b0ab0e7b5e..e5d9e5e3ad74 100644
--- a/ts/background.ts
+++ b/ts/background.ts
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash';
+import { render } from 'react-dom';
import {
DecryptionErrorMessage,
PlaintextContent,
@@ -300,10 +301,8 @@ export async function startApp(): Promise {
window.log.info('environment:', window.getEnvironment());
let idleDetector: WhatIsThis;
- let initialLoadComplete = false;
let newVersion = false;
- window.owsDesktopApp = {};
window.document.title = window.getTitle();
window.Whisper.KeyChangeListener.init(window.textsecure.storage.protocol);
@@ -905,6 +904,9 @@ export async function startApp(): Promise {
const ourUuid = window.textsecure.storage.user.getUuid();
const ourConversationId = window.ConversationController.getOurConversationId();
+ const themeSetting = window.Events.getThemeSetting();
+ const theme = themeSetting === 'system' ? window.systemTheme : themeSetting;
+
const initialState = {
conversations: {
conversationLookup: window.Signal.Util.makeLookup(conversations, 'id'),
@@ -943,7 +945,7 @@ export async function startApp(): Promise {
platform: window.platform,
i18n: window.i18n,
interactionMode: window.getInteractionMode(),
- theme: window.Events.getThemeSetting(),
+ theme,
},
};
@@ -955,6 +957,10 @@ export async function startApp(): Promise {
// Binding these actions to our redux store and exposing them allows us to update
// redux when things change in the backbone world.
+ actions.app = window.Signal.State.bindActionCreators(
+ window.Signal.State.Ducks.app.actions,
+ store.dispatch
+ );
actions.calling = window.Signal.State.bindActionCreators(
window.Signal.State.Ducks.calling.actions,
store.dispatch
@@ -1547,17 +1553,11 @@ export async function startApp(): Promise {
}
window.Whisper.events.on('setupAsNewDevice', () => {
- const { appView } = window.owsDesktopApp;
- if (appView) {
- appView.openInstaller();
- }
+ window.reduxActions.app.openInstaller();
});
window.Whisper.events.on('setupAsStandalone', () => {
- const { appView } = window.owsDesktopApp;
- if (appView) {
- appView.openStandalone();
- }
+ window.reduxActions.app.openStandalone();
});
window.Whisper.events.on('powerMonitorSuspend', () => {
@@ -1712,10 +1712,13 @@ export async function startApp(): Promise {
});
cancelInitializationMessage();
- const appView = new window.Whisper.AppView({
- el: $('body'),
- });
- window.owsDesktopApp.appView = appView;
+ render(
+ window.Signal.State.Roots.createApp(window.reduxStore),
+ document.body
+ );
+ const hideMenuBar = window.storage.get('hide-menu-bar', false);
+ window.setAutoHideMenuBar(hideMenuBar);
+ window.setMenuBarVisibility(!hideMenuBar);
window.Whisper.WallClockListener.init(window.Whisper.events);
window.Whisper.ExpiringMessagesListener.init(window.Whisper.events);
@@ -1723,22 +1726,14 @@ export async function startApp(): Promise {
if (window.Signal.Util.Registration.everDone()) {
connect();
- appView.openInbox({
- initialLoadComplete,
- });
+ window.reduxActions.app.openInbox();
} else {
- appView.openInstaller();
+ window.reduxActions.app.openInstaller();
}
- window.Whisper.events.on('showDebugLog', () => {
- appView.openDebugLog();
- });
- window.Whisper.events.on('unauthorized', () => {
- appView.inboxView.networkStatusView.update();
- });
window.Whisper.events.on('contactsync', () => {
- if (appView.installView) {
- appView.openInbox();
+ if (window.reduxStore.getState().app.isShowingInstaller) {
+ window.reduxActions.app.openInbox();
}
});
@@ -1747,20 +1742,12 @@ export async function startApp(): Promise {
window.Whisper.Notifications.fastClear()
);
- window.Whisper.events.on('showConversation', (id, messageId) => {
- if (appView) {
- appView.openConversation(id, messageId);
- }
- });
-
window.Whisper.Notifications.on('click', (id, messageId) => {
window.showWindow();
if (id) {
- appView.openConversation(id, messageId);
+ window.Whisper.events.trigger('showConversation', id, messageId);
} else {
- appView.openInbox({
- initialLoadComplete,
- });
+ window.reduxActions.app.openInbox();
}
});
@@ -2291,11 +2278,6 @@ export async function startApp(): Promise {
}
function onChangeTheme() {
- const view = window.owsDesktopApp.appView;
- if (view) {
- view.applyTheme();
- }
-
if (window.reduxActions && window.reduxActions.user) {
const theme = window.Events.getThemeSetting();
window.reduxActions.user.userChanged({
@@ -2352,7 +2334,6 @@ export async function startApp(): Promise {
window.flushAllWaitBatchers(),
]);
window.log.info('onEmpty: All outstanding database requests complete');
- initialLoadComplete = true;
window.readyForUpdates();
// Start listeners here, after we get through our queue.
@@ -2370,13 +2351,8 @@ export async function startApp(): Promise {
window.Whisper.Notifications.enable();
await onAppView;
- const view = window.owsDesktopApp.appView;
- if (!view) {
- throw new Error('Expected `appView` to be initialized');
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- view.onEmpty();
+ window.reduxActions.app.initialLoadComplete();
window.logAppLoadedEvent({
processedCount: messageReceiver && messageReceiver.getProcessedCount(),
@@ -2456,10 +2432,7 @@ export async function startApp(): Promise {
`incrementProgress: Message count is ${initialStartupCount}`
);
- const view = window.owsDesktopApp.appView;
- if (view) {
- view.onProgress(initialStartupCount);
- }
+ window.Whisper.events.trigger('loadingProgress', initialStartupCount);
}
window.Whisper.events.on('manualConnect', manualConnect);
diff --git a/ts/components/App.tsx b/ts/components/App.tsx
new file mode 100644
index 000000000000..119a168eb3a2
--- /dev/null
+++ b/ts/components/App.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { AppViewType } from '../state/ducks/app';
+import { Inbox } from './Inbox';
+import { Install } from './Install';
+import { StandaloneRegistration } from './StandaloneRegistration';
+import { ThemeType } from '../types/Util';
+
+export type PropsType = {
+ appView: AppViewType;
+ hasInitialLoadCompleted: boolean;
+ theme: ThemeType;
+};
+
+export const App = ({
+ appView,
+ hasInitialLoadCompleted,
+ theme,
+}: PropsType): JSX.Element => {
+ let contents;
+
+ if (appView === AppViewType.Installer) {
+ contents = ;
+ } else if (appView === AppViewType.Standalone) {
+ contents = ;
+ } else if (appView === AppViewType.Inbox) {
+ contents = ;
+ }
+
+ return (
+
+ {contents}
+
+ );
+};
diff --git a/ts/components/BackboneHost.tsx b/ts/components/BackboneHost.tsx
new file mode 100644
index 000000000000..9d78b8fa0ef4
--- /dev/null
+++ b/ts/components/BackboneHost.tsx
@@ -0,0 +1,35 @@
+import React, { useEffect, useRef } from 'react';
+import * as Backbone from 'backbone';
+
+type PropsType = {
+ View: typeof Backbone.View;
+ className?: string;
+};
+
+export const BackboneHost = ({ View, className }: PropsType): JSX.Element => {
+ const hostRef = useRef(null);
+ const viewRef = useRef(undefined);
+
+ useEffect(() => {
+ const view = new View({
+ el: hostRef.current,
+ });
+
+ viewRef.current = view;
+
+ return () => {
+ if (!viewRef || !viewRef.current) {
+ return;
+ }
+
+ viewRef.current.remove();
+ viewRef.current = undefined;
+ };
+ }, [View]);
+
+ return (
+
+ );
+};
diff --git a/ts/components/Inbox.tsx b/ts/components/Inbox.tsx
new file mode 100644
index 000000000000..8619c54133ab
--- /dev/null
+++ b/ts/components/Inbox.tsx
@@ -0,0 +1,48 @@
+import React, { useEffect, useRef } from 'react';
+import * as Backbone from 'backbone';
+
+type InboxViewType = Backbone.View & {
+ onEmpty?: () => void;
+};
+
+type InboxViewOptionsType = Backbone.ViewOptions & {
+ initialLoadComplete: boolean;
+ window: typeof window;
+};
+
+export type PropsType = {
+ hasInitialLoadCompleted: boolean;
+};
+
+export const Inbox = ({ hasInitialLoadCompleted }: PropsType): JSX.Element => {
+ const hostRef = useRef(null);
+ const viewRef = useRef(undefined);
+
+ useEffect(() => {
+ const viewOptions: InboxViewOptionsType = {
+ el: hostRef.current,
+ initialLoadComplete: false,
+ window,
+ };
+ const view = new window.Whisper.InboxView(viewOptions);
+
+ viewRef.current = view;
+
+ return () => {
+ if (!viewRef || !viewRef.current) {
+ return;
+ }
+
+ viewRef.current.remove();
+ viewRef.current = undefined;
+ };
+ }, []);
+
+ useEffect(() => {
+ if (hasInitialLoadCompleted && viewRef.current && viewRef.current.onEmpty) {
+ viewRef.current.onEmpty();
+ }
+ }, [hasInitialLoadCompleted, viewRef]);
+
+ return ;
+};
diff --git a/ts/components/Install.tsx b/ts/components/Install.tsx
new file mode 100644
index 000000000000..13b9b684ffcc
--- /dev/null
+++ b/ts/components/Install.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { BackboneHost } from './BackboneHost';
+
+export const Install = (): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/ts/components/StandaloneRegistration.tsx b/ts/components/StandaloneRegistration.tsx
new file mode 100644
index 000000000000..c52386c39c34
--- /dev/null
+++ b/ts/components/StandaloneRegistration.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { BackboneHost } from './BackboneHost';
+
+export const StandaloneRegistration = (): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/ts/state/actions.ts b/ts/state/actions.ts
index bc64a773145b..6aa89b5f9374 100644
--- a/ts/state/actions.ts
+++ b/ts/state/actions.ts
@@ -1,6 +1,7 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
+import { actions as app } from './ducks/app';
import { actions as audioPlayer } from './ducks/audioPlayer';
import { actions as calling } from './ducks/calling';
import { actions as conversations } from './ducks/conversations';
@@ -17,6 +18,7 @@ import { actions as updates } from './ducks/updates';
import { actions as user } from './ducks/user';
export const mapDispatchToProps = {
+ ...app,
...audioPlayer,
...calling,
...conversations,
diff --git a/ts/state/ducks/app.ts b/ts/state/ducks/app.ts
new file mode 100644
index 000000000000..c626885a6bcf
--- /dev/null
+++ b/ts/state/ducks/app.ts
@@ -0,0 +1,155 @@
+// Copyright 2021 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { ThunkAction } from 'redux-thunk';
+import { StateType as RootStateType } from '../reducer';
+
+// State
+
+export enum AppViewType {
+ Blank = 'Blank',
+ Inbox = 'Inbox',
+ Installer = 'Installer',
+ Standalone = 'Standalone',
+}
+
+export type AppStateType = {
+ appView: AppViewType;
+ hasInitialLoadCompleted: boolean;
+};
+
+// Actions
+
+const INITIAL_LOAD_COMPLETE = 'app/INITIAL_LOAD_COMPLETE';
+const OPEN_INBOX = 'app/OPEN_INBOX';
+const OPEN_INSTALLER = 'app/OPEN_INSTALLER';
+const OPEN_STANDALONE = 'app/OPEN_STANDALONE';
+
+type InitialLoadCompleteActionType = {
+ type: typeof INITIAL_LOAD_COMPLETE;
+};
+
+type OpenInboxActionType = {
+ type: typeof OPEN_INBOX;
+};
+
+type OpenInstallerActionType = {
+ type: typeof OPEN_INSTALLER;
+};
+
+type OpenStandaloneActionType = {
+ type: typeof OPEN_STANDALONE;
+};
+
+export type AppActionType =
+ | InitialLoadCompleteActionType
+ | OpenInboxActionType
+ | OpenInstallerActionType
+ | OpenStandaloneActionType;
+
+export const actions = {
+ initialLoadComplete,
+ openInbox,
+ openInstaller,
+ openStandalone,
+};
+
+function initialLoadComplete(): InitialLoadCompleteActionType {
+ return {
+ type: INITIAL_LOAD_COMPLETE,
+ };
+}
+
+function openInbox(): ThunkAction<
+ void,
+ RootStateType,
+ unknown,
+ OpenInboxActionType
+> {
+ return async dispatch => {
+ window.log.info('open inbox');
+
+ await window.ConversationController.loadPromise();
+
+ dispatch({
+ type: OPEN_INBOX,
+ });
+ };
+}
+
+function openInstaller(): ThunkAction<
+ void,
+ RootStateType,
+ unknown,
+ OpenInstallerActionType
+> {
+ return dispatch => {
+ window.addSetupMenuItems();
+
+ dispatch({
+ type: OPEN_INSTALLER,
+ });
+ };
+}
+
+function openStandalone(): ThunkAction<
+ void,
+ RootStateType,
+ unknown,
+ OpenStandaloneActionType
+> {
+ return dispatch => {
+ if (window.getEnvironment() === 'production') {
+ return;
+ }
+
+ window.addSetupMenuItems();
+ dispatch({
+ type: OPEN_STANDALONE,
+ });
+ };
+}
+
+// Reducer
+
+export function getEmptyState(): AppStateType {
+ return {
+ appView: AppViewType.Blank,
+ hasInitialLoadCompleted: false,
+ };
+}
+
+export function reducer(
+ state: Readonly = getEmptyState(),
+ action: Readonly
+): AppStateType {
+ if (action.type === OPEN_INBOX) {
+ return {
+ ...state,
+ appView: AppViewType.Inbox,
+ };
+ }
+
+ if (action.type === INITIAL_LOAD_COMPLETE) {
+ return {
+ ...state,
+ hasInitialLoadCompleted: true,
+ };
+ }
+
+ if (action.type === OPEN_INSTALLER) {
+ return {
+ ...state,
+ appView: AppViewType.Installer,
+ };
+ }
+
+ if (action.type === OPEN_STANDALONE) {
+ return {
+ ...state,
+ appView: AppViewType.Standalone,
+ };
+ }
+
+ return state;
+}
diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts
index 429e71343bca..4d3afb657c47 100644
--- a/ts/state/reducer.ts
+++ b/ts/state/reducer.ts
@@ -3,6 +3,7 @@
import { combineReducers } from 'redux';
+import { reducer as app } from './ducks/app';
import { reducer as audioPlayer } from './ducks/audioPlayer';
import { reducer as calling } from './ducks/calling';
import { reducer as conversations } from './ducks/conversations';
@@ -19,6 +20,7 @@ import { reducer as updates } from './ducks/updates';
import { reducer as user } from './ducks/user';
export const reducer = combineReducers({
+ app,
audioPlayer,
calling,
conversations,
diff --git a/ts/state/roots/createApp.tsx b/ts/state/roots/createApp.tsx
new file mode 100644
index 000000000000..78c6b6c81f15
--- /dev/null
+++ b/ts/state/roots/createApp.tsx
@@ -0,0 +1,15 @@
+// Copyright 2021 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import React, { ReactElement } from 'react';
+import { Provider } from 'react-redux';
+
+import { Store } from 'redux';
+
+import { SmartApp } from '../smart/App';
+
+export const createApp = (store: Store): ReactElement => (
+
+
+
+);
diff --git a/ts/state/smart/App.ts b/ts/state/smart/App.ts
new file mode 100644
index 000000000000..fa667a957b28
--- /dev/null
+++ b/ts/state/smart/App.ts
@@ -0,0 +1,21 @@
+// Copyright 2021 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { connect } from 'react-redux';
+
+import { App } from '../../components/App';
+import { StateType } from '../reducer';
+import { getIntl, getTheme } from '../selectors/user';
+import { mapDispatchToProps } from '../actions';
+
+const mapStateToProps = (state: StateType) => {
+ return {
+ ...state.app,
+ i18n: getIntl(state),
+ theme: getTheme(state),
+ };
+};
+
+const smart = connect(mapStateToProps, mapDispatchToProps);
+
+export const SmartApp = smart(App);
diff --git a/ts/state/types.ts b/ts/state/types.ts
index 001d3b2d9006..bc371518a2fe 100644
--- a/ts/state/types.ts
+++ b/ts/state/types.ts
@@ -1,6 +1,7 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
+import { actions as app } from './ducks/app';
import { actions as audioPlayer } from './ducks/audioPlayer';
import { actions as calling } from './ducks/calling';
import { actions as conversations } from './ducks/conversations';
@@ -17,6 +18,7 @@ import { actions as updates } from './ducks/updates';
import { actions as user } from './ducks/user';
export type ReduxActions = {
+ app: typeof app;
audioPlayer: typeof audioPlayer;
calling: typeof calling;
conversations: typeof conversations;
diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json
index 93dbe89e8234..7e0b7c442e5a 100644
--- a/ts/util/lint/exceptions.json
+++ b/ts/util/lint/exceptions.json
@@ -252,30 +252,6 @@
"updated": "2020-08-21T11:29:29.636Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
- {
- "rule": "DOM-innerHTML",
- "path": "js/views/app_view.js",
- "line": " this.el.innerHTML = '';",
- "reasonCategory": "usageTrusted",
- "updated": "2018-09-15T00:38:04.183Z",
- "reasonDetail": "Hard-coded string"
- },
- {
- "rule": "jQuery-append(",
- "path": "js/views/app_view.js",
- "line": " this.el.append(view.el);",
- "reasonCategory": "usageTrusted",
- "updated": "2018-09-19T18:13:29.628Z",
- "reasonDetail": "Interacting with already-existing DOM nodes"
- },
- {
- "rule": "jQuery-appendTo(",
- "path": "js/views/app_view.js",
- "line": " this.debugLogView.$el.appendTo(this.el);",
- "reasonCategory": "usageTrusted",
- "updated": "2018-09-19T18:13:29.628Z",
- "reasonDetail": "Interacting with already-existing DOM nodes"
- },
{
"rule": "jQuery-$(",
"path": "js/views/banner_view.js",
@@ -13510,6 +13486,20 @@
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Only used to focus the element."
},
+ {
+ "rule": "React-useRef",
+ "path": "ts/components/BackboneHost.js",
+ "line": " const hostRef = react_1.useRef(null);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2021-06-09T04:02:08.305Z"
+ },
+ {
+ "rule": "React-useRef",
+ "path": "ts/components/BackboneHost.js",
+ "line": " const viewRef = react_1.useRef(undefined);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2021-06-09T04:02:08.305Z"
+ },
{
"rule": "React-useRef",
"path": "ts/components/CallNeedPermissionScreen.js",
@@ -13812,6 +13802,20 @@
"updated": "2021-03-05T16:51:54.214Z",
"reasonDetail": "Used to handle an element. Only updates the value and selection state."
},
+ {
+ "rule": "React-useRef",
+ "path": "ts/components/Inbox.js",
+ "line": " const hostRef = react_1.useRef(null);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2021-06-08T02:49:25.154Z"
+ },
+ {
+ "rule": "React-useRef",
+ "path": "ts/components/Inbox.js",
+ "line": " const viewRef = react_1.useRef(undefined);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2021-06-08T02:49:25.154Z"
+ },
{
"rule": "jQuery-$(",
"path": "ts/components/Intl.js",
diff --git a/ts/window.d.ts b/ts/window.d.ts
index 4a73de684e47..6e67245403a9 100644
--- a/ts/window.d.ts
+++ b/ts/window.d.ts
@@ -42,6 +42,7 @@ import * as Errors from '../js/modules/types/errors';
import { ConversationController } from './ConversationController';
import { ReduxActions } from './state/types';
import { createStore } from './state/createStore';
+import { createApp } from './state/roots/createApp';
import { createCallManager } from './state/roots/createCallManager';
import { createChatColorPicker } from './state/roots/createChatColorPicker';
import { createCompositionArea } from './state/roots/createCompositionArea';
@@ -62,6 +63,7 @@ import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal
import { createStickerManager } from './state/roots/createStickerManager';
import { createStickerPreviewModal } from './state/roots/createStickerPreviewModal';
import { createTimeline } from './state/roots/createTimeline';
+import * as appDuck from './state/ducks/app';
import * as callingDuck from './state/ducks/calling';
import * as conversationsDuck from './state/ducks/conversations';
import * as emojisDuck from './state/ducks/emojis';
@@ -163,6 +165,7 @@ declare global {
) => void
) => void;
+ addSetupMenuItems: () => void;
attachmentDownloadQueue: Array | undefined;
startupProcessingQueue: StartupQueue | undefined;
baseAttachmentsPath: string;
@@ -229,7 +232,6 @@ declare global {
nodeSetImmediate: typeof setImmediate;
normalizeUuids: (obj: any, paths: Array, context: string) => void;
onFullScreenChange: (fullScreen: boolean) => void;
- owsDesktopApp: WhatIsThis;
platform: string;
preloadedImages: Array;
reduxActions: ReduxActions;
@@ -507,6 +509,7 @@ declare global {
bindActionCreators: typeof bindActionCreators;
createStore: typeof createStore;
Roots: {
+ createApp: typeof createApp;
createCallManager: typeof createCallManager;
createChatColorPicker: typeof createChatColorPicker;
createCompositionArea: typeof createCompositionArea;
@@ -529,6 +532,7 @@ declare global {
createTimeline: typeof createTimeline;
};
Ducks: {
+ app: typeof appDuck;
calling: typeof callingDuck;
conversations: typeof conversationsDuck;
emojis: typeof emojisDuck;
@@ -695,13 +699,15 @@ export type WhisperType = {
ConversationArchivedToast: WhatIsThis;
ConversationUnarchivedToast: WhatIsThis;
ConversationMarkedUnreadToast: WhatIsThis;
- AppView: WhatIsThis;
WallClockListener: WhatIsThis;
MessageRequests: WhatIsThis;
BannerView: any;
RecorderView: any;
GroupMemberList: any;
GroupLinkCopiedToast: typeof Backbone.View;
+ InboxView: typeof window.Whisper.View;
+ InstallView: typeof window.Whisper.View;
+ StandaloneRegistrationView: typeof window.Whisper.View;
KeyVerificationPanelView: any;
SafetyNumberChangeDialogView: any;
BodyRangesType: BodyRangesType;