New top-level React root: <App />
This commit is contained in:
parent
9a1f722545
commit
173771d34b
22 changed files with 457 additions and 266 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -333,7 +333,7 @@
|
|||
<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/storage.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/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/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/app_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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
12
stylesheets/components/App.scss
Normal file
12
stylesheets/components/App.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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
42
ts/components/App.tsx
Normal 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>
|
||||
);
|
||||
};
|
35
ts/components/BackboneHost.tsx
Normal file
35
ts/components/BackboneHost.tsx
Normal 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
48
ts/components/Inbox.tsx
Normal 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
11
ts/components/Install.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
11
ts/components/StandaloneRegistration.tsx
Normal file
11
ts/components/StandaloneRegistration.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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
155
ts/state/ducks/app.ts
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
15
ts/state/roots/createApp.tsx
Normal file
15
ts/state/roots/createApp.tsx
Normal 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
21
ts/state/smart/App.ts
Normal 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);
|
|
@ -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;
|
||||
|
|
|
@ -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
10
ts/window.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue