New top-level React root: <App />

This commit is contained in:
Josh Perez 2021-06-14 15:01:00 -04:00 committed by GitHub
parent 9a1f722545
commit 173771d34b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 457 additions and 266 deletions

View file

@ -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 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 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 ## Changing to production

View file

@ -333,7 +333,7 @@
<script type='text/javascript' src='js/reliable_trigger.js'></script> <script type='text/javascript' src='js/reliable_trigger.js'></script>
<script type='text/javascript' src='js/database.js'></script> <script type='text/javascript' src='js/database.js'></script>
<script type='text/javascript' src='js/storage.js'></script> <script type='text/javascript' src='js/storage.js'></script>
<script type='text/javascript' src='libtextsecure/protocol_wrapper.js'></script> <script type='text/javascript' src='libtextsecure/protocol_wrapper.js'></script>
<script type='text/javascript' src='libtextsecure/storage/user.js'></script> <script type='text/javascript' src='libtextsecure/storage/user.js'></script>
<script type='text/javascript' src='libtextsecure/storage/unprocessed.js'></script> <script type='text/javascript' src='libtextsecure/storage/unprocessed.js'></script>
@ -368,7 +368,6 @@
<script type="text/javascript" src="js/views/phone-input-view.js"></script> <script type="text/javascript" src="js/views/phone-input-view.js"></script>
<script type='text/javascript' src='js/views/safety_number_change_dialog_view.js'></script> <script type='text/javascript' src='js/views/safety_number_change_dialog_view.js'></script>
<script type='text/javascript' src='js/views/standalone_registration_view.js'></script> <script type='text/javascript' src='js/views/standalone_registration_view.js'></script>
<script type='text/javascript' src='js/views/app_view.js'></script>
<script type='text/javascript' src='js/views/clear_data_view.js'></script> <script type='text/javascript' src='js/views/clear_data_view.js'></script>
<script type='text/javascript' src='js/wall_clock_listener.js'></script> <script type='text/javascript' src='js/wall_clock_listener.js'></script>

View file

@ -80,6 +80,7 @@ const {
const { const {
createConversationHeader, createConversationHeader,
} = require('../../ts/state/roots/createConversationHeader'); } = require('../../ts/state/roots/createConversationHeader');
const { createApp } = require('../../ts/state/roots/createApp');
const { createCallManager } = require('../../ts/state/roots/createCallManager'); const { createCallManager } = require('../../ts/state/roots/createCallManager');
const { const {
createForwardMessageModal, createForwardMessageModal,
@ -120,6 +121,7 @@ const {
} = require('../../ts/state/roots/createShortcutGuideModal'); } = require('../../ts/state/roots/createShortcutGuideModal');
const { createStore } = require('../../ts/state/createStore'); const { createStore } = require('../../ts/state/createStore');
const appDuck = require('../../ts/state/ducks/app');
const callingDuck = require('../../ts/state/ducks/calling'); const callingDuck = require('../../ts/state/ducks/calling');
const conversationsDuck = require('../../ts/state/ducks/conversations'); const conversationsDuck = require('../../ts/state/ducks/conversations');
const emojisDuck = require('../../ts/state/ducks/emojis'); const emojisDuck = require('../../ts/state/ducks/emojis');
@ -356,6 +358,7 @@ exports.setup = (options = {}) => {
}; };
const Roots = { const Roots = {
createApp,
createCallManager, createCallManager,
createChatColorPicker, createChatColorPicker,
createCompositionArea, createCompositionArea,
@ -379,6 +382,7 @@ exports.setup = (options = {}) => {
}; };
const Ducks = { const Ducks = {
app: appDuck,
calling: callingDuck, calling: callingDuck,
conversations: conversationsDuck, conversations: conversationsDuck,
emojis: emojisDuck, emojis: emojisDuck,

View file

@ -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);
});
}
},
});
})();

View file

@ -106,6 +106,30 @@
this.conversation_stack.unload(); 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) { if (!options.initialLoadComplete) {
this.appLoadingScreen = new Whisper.AppLoadingScreen(); this.appLoadingScreen = new Whisper.AppLoadingScreen();
this.appLoadingScreen.render(); this.appLoadingScreen.render();
@ -209,12 +233,6 @@
} }
} }
}, },
onProgress(count) {
const view = this.appLoadingScreen;
if (view) {
view.updateProgress(count);
}
},
focusConversation(e) { focusConversation(e) {
if (e && this.$(e.target).closest('.placeholder').length) { if (e && this.$(e.target).closest('.placeholder').length) {
return; return;
@ -231,22 +249,6 @@
reloadBackgroundPage() { reloadBackgroundPage() {
window.location.reload(); 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) { closeRecording(e) {
if (e && this.$(e.target).closest('.capture-audio').length > 0) { if (e && this.$(e.target).closest('.capture-audio').length > 0) {
return; return;

View file

@ -26,15 +26,15 @@ body {
--title-bar-drag-area-height: 28px; --title-bar-drag-area-height: 28px;
--draggable-app-region: drag; --draggable-app-region: drag;
} }
}
body.light-theme { &.light-theme {
background-color: $color-white; background-color: $color-white;
color: $color-gray-90; color: $color-gray-90;
} }
body.dark-theme { &.dark-theme {
background-color: $color-gray-95; background-color: $color-gray-95;
color: $color-gray-05; color: $color-gray-05;
}
} }
::-webkit-scrollbar { ::-webkit-scrollbar {

View file

@ -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;
}
}

View file

@ -28,6 +28,7 @@
// New style: components // New style: components
@import './components/AddGroupMembersModal.scss'; @import './components/AddGroupMembersModal.scss';
@import './components/App.scss';
@import './components/Avatar.scss'; @import './components/Avatar.scss';
@import './components/AvatarInput.scss'; @import './components/AvatarInput.scss';
@import './components/Button.scss'; @import './components/Button.scss';

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { render } from 'react-dom';
import { import {
DecryptionErrorMessage, DecryptionErrorMessage,
PlaintextContent, PlaintextContent,
@ -300,10 +301,8 @@ export async function startApp(): Promise<void> {
window.log.info('environment:', window.getEnvironment()); window.log.info('environment:', window.getEnvironment());
let idleDetector: WhatIsThis; let idleDetector: WhatIsThis;
let initialLoadComplete = false;
let newVersion = false; let newVersion = false;
window.owsDesktopApp = {};
window.document.title = window.getTitle(); window.document.title = window.getTitle();
window.Whisper.KeyChangeListener.init(window.textsecure.storage.protocol); window.Whisper.KeyChangeListener.init(window.textsecure.storage.protocol);
@ -905,6 +904,9 @@ export async function startApp(): Promise<void> {
const ourUuid = window.textsecure.storage.user.getUuid(); const ourUuid = window.textsecure.storage.user.getUuid();
const ourConversationId = window.ConversationController.getOurConversationId(); const ourConversationId = window.ConversationController.getOurConversationId();
const themeSetting = window.Events.getThemeSetting();
const theme = themeSetting === 'system' ? window.systemTheme : themeSetting;
const initialState = { const initialState = {
conversations: { conversations: {
conversationLookup: window.Signal.Util.makeLookup(conversations, 'id'), conversationLookup: window.Signal.Util.makeLookup(conversations, 'id'),
@ -943,7 +945,7 @@ export async function startApp(): Promise<void> {
platform: window.platform, platform: window.platform,
i18n: window.i18n, i18n: window.i18n,
interactionMode: window.getInteractionMode(), interactionMode: window.getInteractionMode(),
theme: window.Events.getThemeSetting(), theme,
}, },
}; };
@ -955,6 +957,10 @@ export async function startApp(): Promise<void> {
// Binding these actions to our redux store and exposing them allows us to update // Binding these actions to our redux store and exposing them allows us to update
// redux when things change in the backbone world. // 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( actions.calling = window.Signal.State.bindActionCreators(
window.Signal.State.Ducks.calling.actions, window.Signal.State.Ducks.calling.actions,
store.dispatch store.dispatch
@ -1547,17 +1553,11 @@ export async function startApp(): Promise<void> {
} }
window.Whisper.events.on('setupAsNewDevice', () => { window.Whisper.events.on('setupAsNewDevice', () => {
const { appView } = window.owsDesktopApp; window.reduxActions.app.openInstaller();
if (appView) {
appView.openInstaller();
}
}); });
window.Whisper.events.on('setupAsStandalone', () => { window.Whisper.events.on('setupAsStandalone', () => {
const { appView } = window.owsDesktopApp; window.reduxActions.app.openStandalone();
if (appView) {
appView.openStandalone();
}
}); });
window.Whisper.events.on('powerMonitorSuspend', () => { window.Whisper.events.on('powerMonitorSuspend', () => {
@ -1712,10 +1712,13 @@ export async function startApp(): Promise<void> {
}); });
cancelInitializationMessage(); cancelInitializationMessage();
const appView = new window.Whisper.AppView({ render(
el: $('body'), window.Signal.State.Roots.createApp(window.reduxStore),
}); document.body
window.owsDesktopApp.appView = appView; );
const hideMenuBar = window.storage.get('hide-menu-bar', false);
window.setAutoHideMenuBar(hideMenuBar);
window.setMenuBarVisibility(!hideMenuBar);
window.Whisper.WallClockListener.init(window.Whisper.events); window.Whisper.WallClockListener.init(window.Whisper.events);
window.Whisper.ExpiringMessagesListener.init(window.Whisper.events); window.Whisper.ExpiringMessagesListener.init(window.Whisper.events);
@ -1723,22 +1726,14 @@ export async function startApp(): Promise<void> {
if (window.Signal.Util.Registration.everDone()) { if (window.Signal.Util.Registration.everDone()) {
connect(); connect();
appView.openInbox({ window.reduxActions.app.openInbox();
initialLoadComplete,
});
} else { } 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', () => { window.Whisper.events.on('contactsync', () => {
if (appView.installView) { if (window.reduxStore.getState().app.isShowingInstaller) {
appView.openInbox(); window.reduxActions.app.openInbox();
} }
}); });
@ -1747,20 +1742,12 @@ export async function startApp(): Promise<void> {
window.Whisper.Notifications.fastClear() 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.Whisper.Notifications.on('click', (id, messageId) => {
window.showWindow(); window.showWindow();
if (id) { if (id) {
appView.openConversation(id, messageId); window.Whisper.events.trigger('showConversation', id, messageId);
} else { } else {
appView.openInbox({ window.reduxActions.app.openInbox();
initialLoadComplete,
});
} }
}); });
@ -2291,11 +2278,6 @@ export async function startApp(): Promise<void> {
} }
function onChangeTheme() { function onChangeTheme() {
const view = window.owsDesktopApp.appView;
if (view) {
view.applyTheme();
}
if (window.reduxActions && window.reduxActions.user) { if (window.reduxActions && window.reduxActions.user) {
const theme = window.Events.getThemeSetting(); const theme = window.Events.getThemeSetting();
window.reduxActions.user.userChanged({ window.reduxActions.user.userChanged({
@ -2352,7 +2334,6 @@ export async function startApp(): Promise<void> {
window.flushAllWaitBatchers(), window.flushAllWaitBatchers(),
]); ]);
window.log.info('onEmpty: All outstanding database requests complete'); window.log.info('onEmpty: All outstanding database requests complete');
initialLoadComplete = true;
window.readyForUpdates(); window.readyForUpdates();
// Start listeners here, after we get through our queue. // Start listeners here, after we get through our queue.
@ -2370,13 +2351,8 @@ export async function startApp(): Promise<void> {
window.Whisper.Notifications.enable(); window.Whisper.Notifications.enable();
await onAppView; 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 window.reduxActions.app.initialLoadComplete();
view.onEmpty();
window.logAppLoadedEvent({ window.logAppLoadedEvent({
processedCount: messageReceiver && messageReceiver.getProcessedCount(), processedCount: messageReceiver && messageReceiver.getProcessedCount(),
@ -2456,10 +2432,7 @@ export async function startApp(): Promise<void> {
`incrementProgress: Message count is ${initialStartupCount}` `incrementProgress: Message count is ${initialStartupCount}`
); );
const view = window.owsDesktopApp.appView; window.Whisper.events.trigger('loadingProgress', initialStartupCount);
if (view) {
view.onProgress(initialStartupCount);
}
} }
window.Whisper.events.on('manualConnect', manualConnect); window.Whisper.events.on('manualConnect', manualConnect);

42
ts/components/App.tsx Normal file
View file

@ -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 = <Install />;
} else if (appView === AppViewType.Standalone) {
contents = <StandaloneRegistration />;
} else if (appView === AppViewType.Inbox) {
contents = <Inbox hasInitialLoadCompleted={hasInitialLoadCompleted} />;
}
return (
<div
className={classNames({
App: true,
'light-theme': theme === ThemeType.light,
'dark-theme': theme === ThemeType.dark,
})}
>
{contents}
</div>
);
};

View file

@ -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<HTMLDivElement | null>(null);
const viewRef = useRef<Backbone.View | undefined>(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 (
<div>
<div className={className} ref={hostRef} />
</div>
);
};

48
ts/components/Inbox.tsx Normal file
View file

@ -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<HTMLDivElement | null>(null);
const viewRef = useRef<InboxViewType | undefined>(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 <div className="inbox index" ref={hostRef} />;
};

11
ts/components/Install.tsx Normal file
View file

@ -0,0 +1,11 @@
import React from 'react';
import { BackboneHost } from './BackboneHost';
export const Install = (): JSX.Element => {
return (
<BackboneHost
className="full-screen-flow"
View={window.Whisper.InstallView}
/>
);
};

View file

@ -0,0 +1,11 @@
import React from 'react';
import { BackboneHost } from './BackboneHost';
export const StandaloneRegistration = (): JSX.Element => {
return (
<BackboneHost
className="full-screen-flow"
View={window.Whisper.StandaloneRegistrationView}
/>
);
};

View file

@ -1,6 +1,7 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { actions as app } from './ducks/app';
import { actions as audioPlayer } from './ducks/audioPlayer'; import { actions as audioPlayer } from './ducks/audioPlayer';
import { actions as calling } from './ducks/calling'; import { actions as calling } from './ducks/calling';
import { actions as conversations } from './ducks/conversations'; 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'; import { actions as user } from './ducks/user';
export const mapDispatchToProps = { export const mapDispatchToProps = {
...app,
...audioPlayer, ...audioPlayer,
...calling, ...calling,
...conversations, ...conversations,

155
ts/state/ducks/app.ts Normal file
View file

@ -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<AppStateType> = getEmptyState(),
action: Readonly<AppActionType>
): 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;
}

View file

@ -3,6 +3,7 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { reducer as app } from './ducks/app';
import { reducer as audioPlayer } from './ducks/audioPlayer'; import { reducer as audioPlayer } from './ducks/audioPlayer';
import { reducer as calling } from './ducks/calling'; import { reducer as calling } from './ducks/calling';
import { reducer as conversations } from './ducks/conversations'; 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'; import { reducer as user } from './ducks/user';
export const reducer = combineReducers({ export const reducer = combineReducers({
app,
audioPlayer, audioPlayer,
calling, calling,
conversations, conversations,

View file

@ -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 => (
<Provider store={store}>
<SmartApp />
</Provider>
);

21
ts/state/smart/App.ts Normal file
View file

@ -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);

View file

@ -1,6 +1,7 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { actions as app } from './ducks/app';
import { actions as audioPlayer } from './ducks/audioPlayer'; import { actions as audioPlayer } from './ducks/audioPlayer';
import { actions as calling } from './ducks/calling'; import { actions as calling } from './ducks/calling';
import { actions as conversations } from './ducks/conversations'; 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'; import { actions as user } from './ducks/user';
export type ReduxActions = { export type ReduxActions = {
app: typeof app;
audioPlayer: typeof audioPlayer; audioPlayer: typeof audioPlayer;
calling: typeof calling; calling: typeof calling;
conversations: typeof conversations; conversations: typeof conversations;

View file

@ -252,30 +252,6 @@
"updated": "2020-08-21T11:29:29.636Z", "updated": "2020-08-21T11:29:29.636Z",
"reasonDetail": "Interacting with already-existing DOM nodes" "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-$(", "rule": "jQuery-$(",
"path": "js/views/banner_view.js", "path": "js/views/banner_view.js",
@ -13510,6 +13486,20 @@
"updated": "2020-10-26T19:12:24.410Z", "updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Only used to focus the element." "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", "rule": "React-useRef",
"path": "ts/components/CallNeedPermissionScreen.js", "path": "ts/components/CallNeedPermissionScreen.js",
@ -13812,6 +13802,20 @@
"updated": "2021-03-05T16:51:54.214Z", "updated": "2021-03-05T16:51:54.214Z",
"reasonDetail": "Used to handle an <input> element. Only updates the value and selection state." "reasonDetail": "Used to handle an <input> 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-$(", "rule": "jQuery-$(",
"path": "ts/components/Intl.js", "path": "ts/components/Intl.js",

10
ts/window.d.ts vendored
View file

@ -42,6 +42,7 @@ import * as Errors from '../js/modules/types/errors';
import { ConversationController } from './ConversationController'; import { ConversationController } from './ConversationController';
import { ReduxActions } from './state/types'; import { ReduxActions } from './state/types';
import { createStore } from './state/createStore'; import { createStore } from './state/createStore';
import { createApp } from './state/roots/createApp';
import { createCallManager } from './state/roots/createCallManager'; import { createCallManager } from './state/roots/createCallManager';
import { createChatColorPicker } from './state/roots/createChatColorPicker'; import { createChatColorPicker } from './state/roots/createChatColorPicker';
import { createCompositionArea } from './state/roots/createCompositionArea'; import { createCompositionArea } from './state/roots/createCompositionArea';
@ -62,6 +63,7 @@ import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal
import { createStickerManager } from './state/roots/createStickerManager'; import { createStickerManager } from './state/roots/createStickerManager';
import { createStickerPreviewModal } from './state/roots/createStickerPreviewModal'; import { createStickerPreviewModal } from './state/roots/createStickerPreviewModal';
import { createTimeline } from './state/roots/createTimeline'; import { createTimeline } from './state/roots/createTimeline';
import * as appDuck from './state/ducks/app';
import * as callingDuck from './state/ducks/calling'; import * as callingDuck from './state/ducks/calling';
import * as conversationsDuck from './state/ducks/conversations'; import * as conversationsDuck from './state/ducks/conversations';
import * as emojisDuck from './state/ducks/emojis'; import * as emojisDuck from './state/ducks/emojis';
@ -163,6 +165,7 @@ declare global {
) => void ) => void
) => void; ) => void;
addSetupMenuItems: () => void;
attachmentDownloadQueue: Array<MessageModel> | undefined; attachmentDownloadQueue: Array<MessageModel> | undefined;
startupProcessingQueue: StartupQueue | undefined; startupProcessingQueue: StartupQueue | undefined;
baseAttachmentsPath: string; baseAttachmentsPath: string;
@ -229,7 +232,6 @@ declare global {
nodeSetImmediate: typeof setImmediate; nodeSetImmediate: typeof setImmediate;
normalizeUuids: (obj: any, paths: Array<string>, context: string) => void; normalizeUuids: (obj: any, paths: Array<string>, context: string) => void;
onFullScreenChange: (fullScreen: boolean) => void; onFullScreenChange: (fullScreen: boolean) => void;
owsDesktopApp: WhatIsThis;
platform: string; platform: string;
preloadedImages: Array<WhatIsThis>; preloadedImages: Array<WhatIsThis>;
reduxActions: ReduxActions; reduxActions: ReduxActions;
@ -507,6 +509,7 @@ declare global {
bindActionCreators: typeof bindActionCreators; bindActionCreators: typeof bindActionCreators;
createStore: typeof createStore; createStore: typeof createStore;
Roots: { Roots: {
createApp: typeof createApp;
createCallManager: typeof createCallManager; createCallManager: typeof createCallManager;
createChatColorPicker: typeof createChatColorPicker; createChatColorPicker: typeof createChatColorPicker;
createCompositionArea: typeof createCompositionArea; createCompositionArea: typeof createCompositionArea;
@ -529,6 +532,7 @@ declare global {
createTimeline: typeof createTimeline; createTimeline: typeof createTimeline;
}; };
Ducks: { Ducks: {
app: typeof appDuck;
calling: typeof callingDuck; calling: typeof callingDuck;
conversations: typeof conversationsDuck; conversations: typeof conversationsDuck;
emojis: typeof emojisDuck; emojis: typeof emojisDuck;
@ -695,13 +699,15 @@ export type WhisperType = {
ConversationArchivedToast: WhatIsThis; ConversationArchivedToast: WhatIsThis;
ConversationUnarchivedToast: WhatIsThis; ConversationUnarchivedToast: WhatIsThis;
ConversationMarkedUnreadToast: WhatIsThis; ConversationMarkedUnreadToast: WhatIsThis;
AppView: WhatIsThis;
WallClockListener: WhatIsThis; WallClockListener: WhatIsThis;
MessageRequests: WhatIsThis; MessageRequests: WhatIsThis;
BannerView: any; BannerView: any;
RecorderView: any; RecorderView: any;
GroupMemberList: any; GroupMemberList: any;
GroupLinkCopiedToast: typeof Backbone.View; GroupLinkCopiedToast: typeof Backbone.View;
InboxView: typeof window.Whisper.View;
InstallView: typeof window.Whisper.View;
StandaloneRegistrationView: typeof window.Whisper.View;
KeyVerificationPanelView: any; KeyVerificationPanelView: any;
SafetyNumberChangeDialogView: any; SafetyNumberChangeDialogView: any;
BodyRangesType: BodyRangesType; BodyRangesType: BodyRangesType;