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
22 changed files with 457 additions and 266 deletions

View File

@@ -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<void> {
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<void> {
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<void> {
platform: window.platform,
i18n: window.i18n,
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
// 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<void> {
}
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<void> {
});
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<void> {
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<void> {
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<void> {
}
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<void> {
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<void> {
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<void> {
`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);

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
// 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,

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 { 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,

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
// 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;

View File

@@ -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 <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-$(",
"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 { 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<MessageModel> | undefined;
startupProcessingQueue: StartupQueue | undefined;
baseAttachmentsPath: string;
@@ -229,7 +232,6 @@ declare global {
nodeSetImmediate: typeof setImmediate;
normalizeUuids: (obj: any, paths: Array<string>, context: string) => void;
onFullScreenChange: (fullScreen: boolean) => void;
owsDesktopApp: WhatIsThis;
platform: string;
preloadedImages: Array<WhatIsThis>;
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;