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;