Removes ReactWrapperView

This commit is contained in:
Josh Perez 2022-12-21 22:07:45 -05:00 committed by GitHub
parent dec23725e5
commit 0b83ab497d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 444 additions and 396 deletions

View file

@ -140,7 +140,6 @@ import { isValidUuid, UUIDKind, UUID } from './types/UUID';
import * as log from './logging/log'; import * as log from './logging/log';
import { loadRecentEmojis } from './util/loadRecentEmojis'; import { loadRecentEmojis } from './util/loadRecentEmojis';
import { deleteAllLogs } from './util/deleteAllLogs'; import { deleteAllLogs } from './util/deleteAllLogs';
import { ReactWrapperView } from './views/ReactWrapperView';
import { ToastCaptchaFailed } from './components/ToastCaptchaFailed'; import { ToastCaptchaFailed } from './components/ToastCaptchaFailed';
import { ToastCaptchaSolved } from './components/ToastCaptchaSolved'; import { ToastCaptchaSolved } from './components/ToastCaptchaSolved';
import { showToast } from './util/showToast'; import { showToast } from './util/showToast';
@ -1267,30 +1266,6 @@ export async function startApp(): Promise<void> {
window.reduxActions.user.userChanged({ menuOptions: options }); window.reduxActions.user.userChanged({ menuOptions: options });
}); });
let shortcutGuideView: ReactWrapperView | null = null;
window.showKeyboardShortcuts = () => {
if (!shortcutGuideView) {
shortcutGuideView = new ReactWrapperView({
className: 'shortcut-guide-wrapper',
JSX: window.Signal.State.Roots.createShortcutGuideModal(
window.reduxStore,
{
close: () => {
if (shortcutGuideView) {
shortcutGuideView.remove();
shortcutGuideView = null;
}
},
}
),
onClose: () => {
shortcutGuideView = null;
},
});
}
};
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {
const { ctrlKey, metaKey, shiftKey } = event; const { ctrlKey, metaKey, shiftKey } = event;
@ -1309,7 +1284,7 @@ export async function startApp(): Promise<void> {
// Show keyboard shortcuts - handled by Electron-managed keyboard shortcuts // Show keyboard shortcuts - handled by Electron-managed keyboard shortcuts
// However, on linux Ctrl+/ selects all text, so we prevent that // However, on linux Ctrl+/ selects all text, so we prevent that
if (commandOrCtrl && key === '/') { if (commandOrCtrl && key === '/') {
window.showKeyboardShortcuts(); window.Events.showKeyboardShortcuts();
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
@ -1374,9 +1349,11 @@ export async function startApp(): Promise<void> {
} }
// Cancel out of keyboard shortcut screen - has first precedence // Cancel out of keyboard shortcut screen - has first precedence
if (shortcutGuideView && key === 'Escape') { const isShortcutGuideModalVisible = window.reduxStore
shortcutGuideView.remove(); ? window.reduxStore.getState().globalModals.isShortcutGuideModalVisible
shortcutGuideView = null; : false;
if (isShortcutGuideModalVisible && key === 'Escape') {
window.reduxActions.globalModals.closeShortcutGuideModal();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return; return;

View file

@ -16,7 +16,6 @@ const i18n = setupI18n('en', enMessages);
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
title: text('title', overrideProps.title || ''), title: text('title', overrideProps.title || ''),
description: text('description', overrideProps.description || ''), description: text('description', overrideProps.description || ''),
buttonText: text('buttonText', overrideProps.buttonText || ''),
i18n, i18n,
onClose: action('onClick'), onClose: action('onClick'),
}); });
@ -35,7 +34,6 @@ export function CustomStrings(): JSX.Element {
{...createProps({ {...createProps({
title: 'Real bad!', title: 'Real bad!',
description: 'Just avoid that next time, kay?', description: 'Just avoid that next time, kay?',
buttonText: 'Fine',
})} })}
/> />
); );

View file

@ -8,7 +8,6 @@ import { Modal } from './Modal';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
export type PropsType = { export type PropsType = {
buttonText?: string;
description?: string; description?: string;
title?: string; title?: string;
@ -23,11 +22,11 @@ function focusRef(el: HTMLElement | null) {
} }
export function ErrorModal(props: PropsType): JSX.Element { export function ErrorModal(props: PropsType): JSX.Element {
const { buttonText, description, i18n, onClose, title } = props; const { description, i18n, onClose, title } = props;
const footer = ( const footer = (
<Button onClick={onClose} ref={focusRef} variant={ButtonVariant.Secondary}> <Button onClick={onClose} ref={focusRef} variant={ButtonVariant.Secondary}>
{buttonText || i18n('Confirmation--confirm')} {i18n('Confirmation--confirm')}
</Button> </Button>
); );

View file

@ -19,9 +19,18 @@ import { WhatsNewModal } from './WhatsNewModal';
export type PropsType = { export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
theme: ThemeType; theme: ThemeType;
// AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId?: string;
renderAddUserToAnotherGroup: () => JSX.Element;
// ContactModal // ContactModal
contactModalState?: ContactModalStateType; contactModalState?: ContactModalStateType;
renderContactModal: () => JSX.Element; renderContactModal: () => JSX.Element;
// ErrorModal
errorModalProps?: { description?: string; title?: string };
renderErrorModal: (opts: {
description?: string;
title?: string;
}) => JSX.Element;
// ForwardMessageModal // ForwardMessageModal
forwardMessageProps?: ForwardMessagePropsType; forwardMessageProps?: ForwardMessagePropsType;
renderForwardMessageModal: () => JSX.Element; renderForwardMessageModal: () => JSX.Element;
@ -31,9 +40,9 @@ export type PropsType = {
// SafetyNumberModal // SafetyNumberModal
safetyNumberModalContactId?: string; safetyNumberModalContactId?: string;
renderSafetyNumber: () => JSX.Element; renderSafetyNumber: () => JSX.Element;
// AddUserToAnotherGroupModal // ShortcutGuideModal
addUserToAnotherGroupModalContactId?: string; isShortcutGuideModalVisible: boolean;
renderAddUserToAnotherGroup: () => JSX.Element; renderShortcutGuideModal: () => JSX.Element;
// SignalConnectionsModal // SignalConnectionsModal
isSignalConnectionsVisible: boolean; isSignalConnectionsVisible: boolean;
toggleSignalConnectionsModal: () => unknown; toggleSignalConnectionsModal: () => unknown;
@ -57,9 +66,15 @@ export type PropsType = {
export function GlobalModalContainer({ export function GlobalModalContainer({
i18n, i18n,
// AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId,
renderAddUserToAnotherGroup,
// ContactModal // ContactModal
contactModalState, contactModalState,
renderContactModal, renderContactModal,
// ErrorModal
errorModalProps,
renderErrorModal,
// ForwardMessageModal // ForwardMessageModal
forwardMessageProps, forwardMessageProps,
renderForwardMessageModal, renderForwardMessageModal,
@ -69,9 +84,9 @@ export function GlobalModalContainer({
// SafetyNumberModal // SafetyNumberModal
safetyNumberModalContactId, safetyNumberModalContactId,
renderSafetyNumber, renderSafetyNumber,
// AddUserToAnotherGroupModal // ShortcutGuideModal
addUserToAnotherGroupModalContactId, isShortcutGuideModalVisible,
renderAddUserToAnotherGroup, renderShortcutGuideModal,
// SignalConnectionsModal // SignalConnectionsModal
isSignalConnectionsVisible, isSignalConnectionsVisible,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
@ -92,18 +107,66 @@ export function GlobalModalContainer({
hideWhatsNewModal, hideWhatsNewModal,
isWhatsNewVisible, isWhatsNewVisible,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
// We want the send anyway dialog to supersede most modals since this is an // We want the following dialogs to show in this order:
// immediate action the user needs to take. // 1. Errors
// 2. Safety Number Changes
// 3. The Rest (in no particular order, but they're ordered alphabetically)
// Errors
if (errorModalProps) {
return renderErrorModal(errorModalProps);
}
// Safety Number
if (hasSafetyNumberChangeModal || safetyNumberChangedBlockingData) { if (hasSafetyNumberChangeModal || safetyNumberChangedBlockingData) {
return renderSendAnywayDialog(); return renderSendAnywayDialog();
} }
// The Rest
if (addUserToAnotherGroupModalContactId) {
return renderAddUserToAnotherGroup();
}
if (contactModalState) {
return renderContactModal();
}
if (forwardMessageProps) {
return renderForwardMessageModal();
}
if (isProfileEditorVisible) {
return renderProfileEditor();
}
if (isShortcutGuideModalVisible) {
return renderShortcutGuideModal();
}
if (isSignalConnectionsVisible) {
return (
<SignalConnectionsModal
i18n={i18n}
onClose={toggleSignalConnectionsModal}
/>
);
}
if (isStoriesSettingsVisible) {
return renderStoriesSettings();
}
if (isWhatsNewVisible) {
return <WhatsNewModal hideWhatsNewModal={hideWhatsNewModal} i18n={i18n} />;
}
if (safetyNumberModalContactId) { if (safetyNumberModalContactId) {
return renderSafetyNumber(); return renderSafetyNumber();
} }
if (addUserToAnotherGroupModalContactId) { if (stickerPackPreviewId) {
return renderAddUserToAnotherGroup(); return renderStickerPreviewModal();
} }
if (userNotFoundModalState) { if (userNotFoundModalState) {
@ -135,38 +198,5 @@ export function GlobalModalContainer({
); );
} }
if (contactModalState) {
return renderContactModal();
}
if (isProfileEditorVisible) {
return renderProfileEditor();
}
if (isWhatsNewVisible) {
return <WhatsNewModal hideWhatsNewModal={hideWhatsNewModal} i18n={i18n} />;
}
if (forwardMessageProps) {
return renderForwardMessageModal();
}
if (isSignalConnectionsVisible) {
return (
<SignalConnectionsModal
i18n={i18n}
onClose={toggleSignalConnectionsModal}
/>
);
}
if (isStoriesSettingsVisible) {
return renderStoriesSettings();
}
if (stickerPackPreviewId) {
return renderStickerPreviewModal();
}
return null; return null;
} }

View file

@ -1,4 +1,4 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import * as React from 'react';
@ -9,14 +9,15 @@ import { ShortcutGuide } from './ShortcutGuide';
export type PropsType = { export type PropsType = {
hasInstalledStickers: boolean; hasInstalledStickers: boolean;
platform: string; platform: string;
readonly close: () => unknown; readonly closeShortcutGuideModal: () => unknown;
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
}; };
export const ShortcutGuideModal = React.memo(function ShortcutGuideModalInner( export const ShortcutGuideModal = React.memo(function ShortcutGuideModalInner(
props: PropsType props: PropsType
) { ) {
const { i18n, close, hasInstalledStickers, platform } = props; const { i18n, closeShortcutGuideModal, hasInstalledStickers, platform } =
props;
const [root, setRoot] = React.useState<HTMLElement | null>(null); const [root, setRoot] = React.useState<HTMLElement | null>(null);
React.useEffect(() => { React.useEffect(() => {
@ -34,10 +35,10 @@ export const ShortcutGuideModal = React.memo(function ShortcutGuideModalInner(
<div className="module-shortcut-guide-modal"> <div className="module-shortcut-guide-modal">
<div className="module-shortcut-guide-container"> <div className="module-shortcut-guide-container">
<ShortcutGuide <ShortcutGuide
close={closeShortcutGuideModal}
hasInstalledStickers={hasInstalledStickers} hasInstalledStickers={hasInstalledStickers}
platform={platform}
close={close}
i18n={i18n} i18n={i18n}
platform={platform}
/> />
</div> </div>
</div>, </div>,

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastAlreadyGroupMember } from './ToastAlreadyGroupMember';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastAlreadyGroupMember',
};
export const _ToastAlreadyGroupMember = (): JSX.Element => (
<ToastAlreadyGroupMember {...defaultProps} />
);
_ToastAlreadyGroupMember.story = {
name: 'ToastAlreadyGroupMember',
};

View file

@ -1,20 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastAlreadyGroupMember({
i18n,
onClose,
}: PropsType): JSX.Element {
return (
<Toast onClose={onClose}>{i18n('GroupV2--join--already-in-group')}</Toast>
);
}

View file

@ -51,6 +51,20 @@ AddingUserToGroup.args = {
}, },
}; };
export const AlreadyGroupMember = Template.bind({});
AlreadyGroupMember.args = {
toast: {
toastType: ToastType.AlreadyGroupMember,
},
};
export const AlreadyRequestedToJoin = Template.bind({});
AlreadyRequestedToJoin.args = {
toast: {
toastType: ToastType.AlreadyRequestedToJoin,
},
};
export const Blocked = Template.bind({}); export const Blocked = Template.bind({});
Blocked.args = { Blocked.args = {
toast: { toast: {

View file

@ -45,6 +45,22 @@ export function ToastManager({
); );
} }
if (toastType === ToastType.AlreadyGroupMember) {
return (
<Toast onClose={hideToast}>
{i18n('GroupV2--join--already-in-group')}
</Toast>
);
}
if (toastType === ToastType.AlreadyRequestedToJoin) {
return (
<Toast onClose={hideToast}>
{i18n('GroupV2--join--already-awaiting-approval')}
</Toast>
);
}
if (toastType === ToastType.Blocked) { if (toastType === ToastType.Blocked) {
return <Toast onClose={hideToast}>{i18n('unblockToSend')}</Toast>; return <Toast onClose={hideToast}>{i18n('unblockToSend')}</Toast>;
} }

View file

@ -1,7 +1,19 @@
// Copyright 2021-2022 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import { render, unmountComponentAtNode } from 'react-dom';
import type { ConversationAttributesType } from '../model-types.d';
import type { ConversationModel } from '../models/conversations';
import type { PreJoinConversationType } from '../state/ducks/conversations';
import * as Bytes from '../Bytes';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { HTTPError } from '../textsecure/Errors';
import { SignalService as Proto } from '../protobuf';
import { ToastType } from '../types/Toast';
import { UUIDKind } from '../types/UUID';
import { import {
applyNewAvatar, applyNewAvatar,
decryptGroupDescription, decryptGroupDescription,
@ -12,25 +24,11 @@ import {
LINK_VERSION_ERROR, LINK_VERSION_ERROR,
parseGroupLink, parseGroupLink,
} from '../groups'; } from '../groups';
import * as Errors from '../types/errors'; import { createGroupV2JoinModal } from '../state/roots/createGroupV2JoinModal';
import { UUIDKind } from '../types/UUID';
import * as Bytes from '../Bytes';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { isGroupV1 } from '../util/whatTypeOfConversation';
import { explodePromise } from '../util/explodePromise'; import { explodePromise } from '../util/explodePromise';
import type { ConversationAttributesType } from '../model-types.d';
import type { ConversationModel } from '../models/conversations';
import type { PreJoinConversationType } from '../state/ducks/conversations';
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
import { showToast } from '../util/showToast';
import { ReactWrapperView } from '../views/ReactWrapperView';
import { ErrorModal } from '../components/ErrorModal';
import { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember';
import { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
import { HTTPError } from '../textsecure/Errors';
import { isAccessControlEnabled } from './util'; import { isAccessControlEnabled } from './util';
import { isGroupV1 } from '../util/whatTypeOfConversation';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { sleep } from '../util/sleep'; import { sleep } from '../util/sleep';
export async function joinViaLink(hash: string): Promise<void> { export async function joinViaLink(hash: string): Promise<void> {
@ -43,15 +41,15 @@ export async function joinViaLink(hash: string): Promise<void> {
log.error(`joinViaLink: Failed to parse group link ${errorString}`); log.error(`joinViaLink: Failed to parse group link ${errorString}`);
if (error instanceof Error && error.name === LINK_VERSION_ERROR) { if (error instanceof Error && error.name === LINK_VERSION_ERROR) {
showErrorDialog( window.reduxActions.globalModals.showErrorModal({
window.i18n('GroupV2--join--unknown-link-version'), description: window.i18n('GroupV2--join--unknown-link-version'),
window.i18n('GroupV2--join--unknown-link-version--title') title: window.i18n('GroupV2--join--unknown-link-version--title'),
); });
} else { } else {
showErrorDialog( window.reduxActions.globalModals.showErrorModal({
window.i18n('GroupV2--join--invalid-link'), description: window.i18n('GroupV2--join--invalid-link'),
window.i18n('GroupV2--join--invalid-link--title') title: window.i18n('GroupV2--join--invalid-link--title'),
); });
} }
return; return;
} }
@ -74,7 +72,7 @@ export async function joinViaLink(hash: string): Promise<void> {
window.reduxActions.conversations.showConversation({ window.reduxActions.conversations.showConversation({
conversationId: existingConversation.id, conversationId: existingConversation.id,
}); });
showToast(ToastAlreadyGroupMember); window.reduxActions.toast.showToast(ToastType.AlreadyGroupMember);
return; return;
} }
@ -99,20 +97,20 @@ export async function joinViaLink(hash: string): Promise<void> {
error instanceof HTTPError && error instanceof HTTPError &&
error.responseHeaders['x-signal-forbidden-reason'] error.responseHeaders['x-signal-forbidden-reason']
) { ) {
showErrorDialog( window.reduxActions.globalModals.showErrorModal({
window.i18n('GroupV2--join--link-forbidden'), description: window.i18n('GroupV2--join--link-forbidden'),
window.i18n('GroupV2--join--link-forbidden--title') title: window.i18n('GroupV2--join--link-forbidden--title'),
); });
} else if (error instanceof HTTPError && error.code === 403) { } else if (error instanceof HTTPError && error.code === 403) {
showErrorDialog( window.reduxActions.globalModals.showErrorModal({
window.i18n('GroupV2--join--link-revoked'), description: window.i18n('GroupV2--join--link-revoked'),
window.i18n('GroupV2--join--link-revoked--title') title: window.i18n('GroupV2--join--link-revoked--title'),
); });
} else { } else {
showErrorDialog( window.reduxActions.globalModals.showErrorModal({
window.i18n('GroupV2--join--general-join-failure'), description: window.i18n('GroupV2--join--general-join-failure'),
window.i18n('GroupV2--join--general-join-failure--title') title: window.i18n('GroupV2--join--general-join-failure--title'),
); });
} }
return; return;
} }
@ -121,10 +119,10 @@ export async function joinViaLink(hash: string): Promise<void> {
log.error( log.error(
`joinViaLink/${logId}: addFromInviteLink value of ${result.addFromInviteLink} is invalid` `joinViaLink/${logId}: addFromInviteLink value of ${result.addFromInviteLink} is invalid`
); );
showErrorDialog( window.reduxActions.globalModals.showErrorModal({
window.i18n('GroupV2--join--link-revoked'), description: window.i18n('GroupV2--join--link-revoked'),
window.i18n('GroupV2--join--link-revoked--title') title: window.i18n('GroupV2--join--link-revoked--title'),
); });
return; return;
} }
@ -168,7 +166,7 @@ export async function joinViaLink(hash: string): Promise<void> {
conversationId: existingConversation.id, conversationId: existingConversation.id,
}); });
showToast(ToastAlreadyRequestedToJoin); window.reduxActions.toast.showToast(ToastType.AlreadyRequestedToJoin);
return; return;
} }
@ -202,9 +200,9 @@ export async function joinViaLink(hash: string): Promise<void> {
const closeDialog = async () => { const closeDialog = async () => {
try { try {
if (groupV2InfoDialog) { if (groupV2InfoNode) {
groupV2InfoDialog.remove(); unmountComponentAtNode(groupV2InfoNode);
groupV2InfoDialog = undefined; groupV2InfoNode = undefined;
} }
window.reduxActions.conversations.setPreJoinConversation(undefined); window.reduxActions.conversations.setPreJoinConversation(undefined);
@ -220,9 +218,9 @@ export async function joinViaLink(hash: string): Promise<void> {
const join = async () => { const join = async () => {
try { try {
if (groupV2InfoDialog) { if (groupV2InfoNode) {
groupV2InfoDialog.remove(); unmountComponentAtNode(groupV2InfoNode);
groupV2InfoDialog = undefined; groupV2InfoNode = undefined;
} }
window.reduxActions.conversations.setPreJoinConversation(undefined); window.reduxActions.conversations.setPreJoinConversation(undefined);
@ -375,13 +373,13 @@ export async function joinViaLink(hash: string): Promise<void> {
log.info(`joinViaLink/${logId}: Showing modal`); log.info(`joinViaLink/${logId}: Showing modal`);
let groupV2InfoDialog: Backbone.View | undefined = new ReactWrapperView({ let groupV2InfoNode: HTMLDivElement | undefined =
className: 'group-v2-join-dialog-wrapper', document.createElement('div');
JSX: window.Signal.State.Roots.createGroupV2JoinModal(window.reduxStore, {
join, render(
onClose: closeDialog, createGroupV2JoinModal(window.reduxStore, { join, onClose: closeDialog }),
}), groupV2InfoNode
}); );
// We declare a new function here so we can await but not block // We declare a new function here so we can await but not block
const fetchAvatar = async () => { const fetchAvatar = async () => {
@ -405,7 +403,7 @@ export async function joinViaLink(hash: string): Promise<void> {
}; };
// Dialog has been dismissed; we'll delete the unneeeded avatar // Dialog has been dismissed; we'll delete the unneeeded avatar
if (!groupV2InfoDialog) { if (!groupV2InfoNode) {
await window.Signal.Migrations.deleteAttachmentData( await window.Signal.Migrations.deleteAttachmentData(
attributes.avatar.path attributes.avatar.path
); );
@ -426,19 +424,3 @@ export async function joinViaLink(hash: string): Promise<void> {
await promise; await promise;
} }
function showErrorDialog(description: string, title: string) {
const errorView = new ReactWrapperView({
className: 'error-modal-wrapper',
JSX: (
<ErrorModal
i18n={window.i18n}
title={title}
description={description}
onClose={() => {
errorView.remove();
}}
/>
),
});
}

View file

@ -25,9 +25,7 @@ import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
// State // State
import { createApp } from './state/roots/createApp'; import { createApp } from './state/roots/createApp';
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer'; import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
import { createStore } from './state/createStore'; import { createStore } from './state/createStore';
import * as appDuck from './state/ducks/app'; import * as appDuck from './state/ducks/app';
@ -393,9 +391,7 @@ export const setup = (options: {
const Roots = { const Roots = {
createApp, createApp,
createGroupV2JoinModal,
createSafetyNumberViewer, createSafetyNumberViewer,
createShortcutGuideModal,
}; };
const Ducks = { const Ducks = {

View file

@ -41,10 +41,15 @@ type MigrateToGV2PropsType = {
export type GlobalModalsStateType = Readonly<{ export type GlobalModalsStateType = Readonly<{
addUserToAnotherGroupModalContactId?: string; addUserToAnotherGroupModalContactId?: string;
contactModalState?: ContactModalStateType; contactModalState?: ContactModalStateType;
errorModalProps?: {
description?: string;
title?: string;
};
forwardMessageProps?: ForwardMessagePropsType; forwardMessageProps?: ForwardMessagePropsType;
gv2MigrationProps?: MigrateToGV2PropsType; gv2MigrationProps?: MigrateToGV2PropsType;
isProfileEditorVisible: boolean; isProfileEditorVisible: boolean;
isSignalConnectionsVisible: boolean; isSignalConnectionsVisible: boolean;
isShortcutGuideModalVisible: boolean;
isStoriesSettingsVisible: boolean; isStoriesSettingsVisible: boolean;
isWhatsNewVisible: boolean; isWhatsNewVisible: boolean;
profileEditorHasError: boolean; profileEditorHasError: boolean;
@ -80,6 +85,10 @@ const SHOW_GV2_MIGRATION_DIALOG = 'globalModals/SHOW_GV2_MIGRATION_DIALOG';
const CLOSE_GV2_MIGRATION_DIALOG = 'globalModals/CLOSE_GV2_MIGRATION_DIALOG'; const CLOSE_GV2_MIGRATION_DIALOG = 'globalModals/CLOSE_GV2_MIGRATION_DIALOG';
const SHOW_STICKER_PACK_PREVIEW = 'globalModals/SHOW_STICKER_PACK_PREVIEW'; const SHOW_STICKER_PACK_PREVIEW = 'globalModals/SHOW_STICKER_PACK_PREVIEW';
const CLOSE_STICKER_PACK_PREVIEW = 'globalModals/CLOSE_STICKER_PACK_PREVIEW'; const CLOSE_STICKER_PACK_PREVIEW = 'globalModals/CLOSE_STICKER_PACK_PREVIEW';
const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL';
const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL';
const CLOSE_SHORTCUT_GUIDE_MODAL = 'globalModals/CLOSE_SHORTCUT_GUIDE_MODAL';
const SHOW_SHORTCUT_GUIDE_MODAL = 'globalModals/SHOW_SHORTCUT_GUIDE_MODAL';
export type ContactModalStateType = { export type ContactModalStateType = {
contactId: string; contactId: string;
@ -186,6 +195,26 @@ type CloseStickerPackPreviewActionType = {
type: typeof CLOSE_STICKER_PACK_PREVIEW; type: typeof CLOSE_STICKER_PACK_PREVIEW;
}; };
type CloseErrorModalActionType = {
type: typeof CLOSE_ERROR_MODAL;
};
type ShowErrorModalActionType = {
type: typeof SHOW_ERROR_MODAL;
payload: {
description?: string;
title?: string;
};
};
type CloseShortcutGuideModalActionType = {
type: typeof CLOSE_SHORTCUT_GUIDE_MODAL;
};
type ShowShortcutGuideModalActionType = {
type: typeof SHOW_SHORTCUT_GUIDE_MODAL;
};
export type GlobalModalsActionType = export type GlobalModalsActionType =
| StartMigrationToGV2ActionType | StartMigrationToGV2ActionType
| CloseGV2MigrationDialogActionType | CloseGV2MigrationDialogActionType
@ -201,6 +230,10 @@ export type GlobalModalsActionType =
| ShowSendAnywayDialogActionType | ShowSendAnywayDialogActionType
| CloseStickerPackPreviewActionType | CloseStickerPackPreviewActionType
| ShowStickerPackPreviewActionType | ShowStickerPackPreviewActionType
| CloseErrorModalActionType
| ShowErrorModalActionType
| CloseShortcutGuideModalActionType
| ShowShortcutGuideModalActionType
| ToggleForwardMessageModalActionType | ToggleForwardMessageModalActionType
| ToggleProfileEditorActionType | ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType | ToggleProfileEditorErrorActionType
@ -231,6 +264,10 @@ export const actions = {
closeGV2MigrationDialog, closeGV2MigrationDialog,
showStickerPackPreview, showStickerPackPreview,
closeStickerPackPreview, closeStickerPackPreview,
closeErrorModal,
showErrorModal,
closeShortcutGuideModal,
showShortcutGuideModal,
}; };
export const useGlobalModalActions = (): BoundActionCreatorsMapObject< export const useGlobalModalActions = (): BoundActionCreatorsMapObject<
@ -467,11 +504,46 @@ export function showStickerPackPreview(
}; };
} }
function closeErrorModal(): CloseErrorModalActionType {
return {
type: CLOSE_ERROR_MODAL,
};
}
function showErrorModal({
description,
title,
}: {
title?: string;
description?: string;
}): ShowErrorModalActionType {
return {
type: SHOW_ERROR_MODAL,
payload: {
description,
title,
},
};
}
function closeShortcutGuideModal(): CloseShortcutGuideModalActionType {
return {
type: CLOSE_SHORTCUT_GUIDE_MODAL,
};
}
function showShortcutGuideModal(): ShowShortcutGuideModalActionType {
return {
type: SHOW_SHORTCUT_GUIDE_MODAL,
};
}
// Reducer // Reducer
export function getEmptyState(): GlobalModalsStateType { export function getEmptyState(): GlobalModalsStateType {
return { return {
isProfileEditorVisible: false, isProfileEditorVisible: false,
isShortcutGuideModalVisible: false,
isSignalConnectionsVisible: false, isSignalConnectionsVisible: false,
isStoriesSettingsVisible: false, isStoriesSettingsVisible: false,
isWhatsNewVisible: false, isWhatsNewVisible: false,
@ -616,5 +688,33 @@ export function reducer(
}; };
} }
if (action.type === CLOSE_ERROR_MODAL) {
return {
...state,
errorModalProps: undefined,
};
}
if (action.type === SHOW_ERROR_MODAL) {
return {
...state,
errorModalProps: action.payload,
};
}
if (action.type === CLOSE_SHORTCUT_GUIDE_MODAL) {
return {
...state,
isShortcutGuideModalVisible: false,
};
}
if (action.type === SHOW_SHORTCUT_GUIDE_MODAL) {
return {
...state,
isShortcutGuideModalVisible: true,
};
}
return state; return state;
} }

View file

@ -1,6 +1,8 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// TODO DESKTOP-4761
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';

View file

@ -1,19 +0,0 @@
// Copyright 2019-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Provider } from 'react-redux';
import type { Store } from 'redux';
import type { ExternalProps } from '../smart/ShortcutGuideModal';
import { SmartShortcutGuideModal } from '../smart/ShortcutGuideModal';
export const createShortcutGuideModal = (
store: Store,
props: ExternalProps
): React.ReactElement => (
<Provider store={store}>
<SmartShortcutGuideModal {...props} />
</Provider>
);

View file

@ -1,22 +1,25 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { useCallback } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { ErrorModal } from '../../components/ErrorModal';
import { GlobalModalContainer } from '../../components/GlobalModalContainer'; import { GlobalModalContainer } from '../../components/GlobalModalContainer';
import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
import { SmartContactModal } from './ContactModal'; import { SmartContactModal } from './ContactModal';
import { SmartForwardMessageModal } from './ForwardMessageModal'; import { SmartForwardMessageModal } from './ForwardMessageModal';
import { SmartProfileEditorModal } from './ProfileEditorModal'; import { SmartProfileEditorModal } from './ProfileEditorModal';
import { SmartSafetyNumberModal } from './SafetyNumberModal'; import { SmartSafetyNumberModal } from './SafetyNumberModal';
import { SmartSendAnywayDialog } from './SendAnywayDialog'; import { SmartSendAnywayDialog } from './SendAnywayDialog';
import { SmartShortcutGuideModal } from './ShortcutGuideModal';
import { SmartStickerPreviewModal } from './StickerPreviewModal';
import { SmartStoriesSettingsModal } from './StoriesSettingsModal'; import { SmartStoriesSettingsModal } from './StoriesSettingsModal';
import { getConversationsStoppingSend } from '../selectors/conversations'; import { getConversationsStoppingSend } from '../selectors/conversations';
import { mapDispatchToProps } from '../actions';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl, getTheme } from '../selectors/user';
import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal'; import { useGlobalModalActions } from '../ducks/globalModals';
import { SmartStickerPreviewModal } from './StickerPreviewModal';
function renderProfileEditor(): JSX.Element { function renderProfileEditor(): JSX.Element {
return <SmartProfileEditorModal />; return <SmartProfileEditorModal />;
@ -38,42 +41,95 @@ function renderSendAnywayDialog(): JSX.Element {
return <SmartSendAnywayDialog />; return <SmartSendAnywayDialog />;
} }
const mapStateToProps = (state: StateType) => { function renderShortcutGuideModal(): JSX.Element {
const i18n = getIntl(state); return <SmartShortcutGuideModal />;
}
const { stickerPackPreviewId } = state.globalModals; export function SmartGlobalModalContainer(): JSX.Element {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
return { const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
...state.globalModals,
hasSafetyNumberChangeModal: getConversationsStoppingSend(state).length > 0, const {
i18n, addUserToAnotherGroupModalContactId,
theme: getTheme(state), isProfileEditorVisible,
renderContactModal, isSignalConnectionsVisible,
renderForwardMessageModal, isShortcutGuideModalVisible,
renderProfileEditor, isStoriesSettingsVisible,
renderStoriesSettings, isWhatsNewVisible,
renderStickerPreviewModal: () => safetyNumberModalContactId,
stickerPackPreviewId,
} = useSelector<StateType, GlobalModalsStateType>(
state => state.globalModals
);
const {
closeErrorModal,
hideWhatsNewModal,
hideUserNotFoundModal,
toggleSignalConnectionsModal,
} = useGlobalModalActions();
const renderAddUserToAnotherGroup = useCallback(() => {
return (
<SmartAddUserToAnotherGroupModal
contactID={String(addUserToAnotherGroupModalContactId)}
/>
);
}, [addUserToAnotherGroupModalContactId]);
const renderSafetyNumber = useCallback(
() => (
<SmartSafetyNumberModal contactID={String(safetyNumberModalContactId)} />
),
[safetyNumberModalContactId]
);
const renderStickerPreviewModal = useCallback(
() =>
stickerPackPreviewId ? ( stickerPackPreviewId ? (
<SmartStickerPreviewModal packId={stickerPackPreviewId} /> <SmartStickerPreviewModal packId={stickerPackPreviewId} />
) : null, ) : null,
renderSafetyNumber: () => ( [stickerPackPreviewId]
<SmartSafetyNumberModal );
contactID={String(state.globalModals.safetyNumberModalContactId)}
const renderErrorModal = useCallback(
({ description, title }: { description?: string; title?: string }) => (
<ErrorModal
title={title}
description={description}
i18n={i18n}
onClose={closeErrorModal}
/> />
), ),
renderAddUserToAnotherGroup: () => { [closeErrorModal, i18n]
return ( );
<SmartAddUserToAnotherGroupModal
contactID={String(
state.globalModals.addUserToAnotherGroupModalContactId
)}
/>
);
},
renderSendAnywayDialog,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps); return (
<GlobalModalContainer
export const SmartGlobalModalContainer = smart(GlobalModalContainer); hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
hideUserNotFoundModal={hideUserNotFoundModal}
hideWhatsNewModal={hideWhatsNewModal}
i18n={i18n}
isProfileEditorVisible={isProfileEditorVisible}
isShortcutGuideModalVisible={isShortcutGuideModalVisible}
isSignalConnectionsVisible={isSignalConnectionsVisible}
isStoriesSettingsVisible={isStoriesSettingsVisible}
isWhatsNewVisible={isWhatsNewVisible}
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
renderContactModal={renderContactModal}
renderErrorModal={renderErrorModal}
renderForwardMessageModal={renderForwardMessageModal}
renderProfileEditor={renderProfileEditor}
renderSafetyNumber={renderSafetyNumber}
renderSendAnywayDialog={renderSendAnywayDialog}
renderShortcutGuideModal={renderShortcutGuideModal}
renderStickerPreviewModal={renderStickerPreviewModal}
renderStoriesSettings={renderStoriesSettings}
theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>
);
}

View file

@ -15,13 +15,7 @@ import {
getReceivedStickerPacks, getReceivedStickerPacks,
} from '../selectors/stickers'; } from '../selectors/stickers';
export type ExternalProps = { const mapStateToProps = (state: StateType) => {
close: () => unknown;
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { close } = props;
const blessedPacks = getBlessedStickerPacks(state); const blessedPacks = getBlessedStickerPacks(state);
const installedPacks = getInstalledStickerPacks(state); const installedPacks = getInstalledStickerPacks(state);
const knownPacks = getKnownStickerPacks(state); const knownPacks = getKnownStickerPacks(state);
@ -38,7 +32,6 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const platform = getPlatform(state); const platform = getPlatform(state);
return { return {
close,
hasInstalledStickers, hasInstalledStickers,
platform, platform,
i18n: getIntl(state), i18n: getIntl(state),

View file

@ -18,7 +18,7 @@ import type { LoggerType } from '../../types/Logging';
import { JobQueue } from '../../jobs/JobQueue'; import { JobQueue } from '../../jobs/JobQueue';
import type { ParsedJob, StoredJob, JobQueueStore } from '../../jobs/types'; import type { ParsedJob, StoredJob, JobQueueStore } from '../../jobs/types';
import { sleep } from '../../util'; import { sleep } from '../../util/sleep';
describe('JobQueue', () => { describe('JobQueue', () => {
describe('end-to-end tests', () => { describe('end-to-end tests', () => {

View file

@ -3,6 +3,8 @@
export enum ToastType { export enum ToastType {
AddingUserToGroup = 'AddingUserToGroup', AddingUserToGroup = 'AddingUserToGroup',
AlreadyGroupMember = 'AlreadyGroupMember',
AlreadyRequestedToJoin = 'AlreadyRequestedToJoin',
Blocked = 'Blocked', Blocked = 'Blocked',
BlockedGroup = 'BlockedGroup', BlockedGroup = 'BlockedGroup',
CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments', CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments',

View file

@ -3,7 +3,6 @@
import { webFrame } from 'electron'; import { webFrame } from 'electron';
import type { AudioDevice } from 'ringrtc'; import type { AudioDevice } from 'ringrtc';
import * as React from 'react';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { getStoriesAvailable } from './stories'; import { getStoriesAvailable } from './stories';
@ -19,9 +18,6 @@ import * as Stickers from '../types/Stickers';
import type { SystemTraySetting } from '../types/SystemTraySetting'; import type { SystemTraySetting } from '../types/SystemTraySetting';
import { parseSystemTraySetting } from '../types/SystemTraySetting'; import { parseSystemTraySetting } from '../types/SystemTraySetting';
import { ReactWrapperView } from '../views/ReactWrapperView';
import { ErrorModal } from '../components/ErrorModal';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import { calling } from '../services/calling'; import { calling } from '../services/calling';
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations'; import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
@ -422,7 +418,8 @@ export function createIPCEvents(
elem.remove(); elem.remove();
} }
}, },
showKeyboardShortcuts: () => window.showKeyboardShortcuts(), showKeyboardShortcuts: () =>
window.reduxActions.globalModals.showShortcutGuideModal(),
deleteAllData: async () => { deleteAllData: async () => {
await window.Signal.Data.goBackToMainProcess(); await window.Signal.Data.goBackToMainProcess();
@ -455,18 +452,9 @@ export function createIPCEvents(
'showGroupViaLink: Ran into an error!', 'showGroupViaLink: Ran into an error!',
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
const errorView = new ReactWrapperView({ window.reduxActions.globalModals.showErrorModal({
className: 'error-modal-wrapper', title: window.i18n('GroupV2--join--general-join-failure--title'),
JSX: ( description: window.i18n('GroupV2--join--general-join-failure'),
<ErrorModal
i18n={window.i18n}
title={window.i18n('GroupV2--join--general-join-failure--title')}
description={window.i18n('GroupV2--join--general-join-failure')}
onClose={() => {
errorView.remove();
}}
/>
),
}); });
} }
}, },
@ -549,16 +537,7 @@ export function createIPCEvents(
} }
function showUnknownSgnlLinkModal(): void { function showUnknownSgnlLinkModal(): void {
const errorView = new ReactWrapperView({ window.reduxActions.globalModals.showErrorModal({
className: 'error-modal-wrapper', description: window.i18n('unknown-sgnl-link'),
JSX: (
<ErrorModal
i18n={window.i18n}
description={window.i18n('unknown-sgnl-link')}
onClose={() => {
errorView.remove();
}}
/>
),
}); });
} }

View file

@ -9397,7 +9397,7 @@
}, },
{ {
"rule": "jQuery-prepend(", "rule": "jQuery-prepend(",
"path": "ts/util/createIPCEvents.tsx", "path": "ts/util/createIPCEvents.ts",
"line": " document.body.prepend(newOverlay);", "line": " document.body.prepend(newOverlay);",
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2022-11-30T00:14:31.394Z" "updated": "2022-11-30T00:14:31.394Z"

View file

@ -1,12 +1,12 @@
// Copyright 2021-2022 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import React from 'react';
import { ReactWrapperView } from '../views/ReactWrapperView'; import { render, unmountComponentAtNode } from 'react-dom';
import { ErrorModal } from '../components/ErrorModal';
import { ProgressModal } from '../components/ProgressModal';
import * as log from '../logging/log';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { ProgressModal } from '../components/ProgressModal';
import { clearTimeoutIfNecessary } from './clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from './clearTimeoutIfNecessary';
export async function longRunningTaskWrapper<T>({ export async function longRunningTaskWrapper<T>({
@ -24,17 +24,13 @@ export async function longRunningTaskWrapper<T>({
const ONE_SECOND = 1000; const ONE_SECOND = 1000;
const TWO_SECONDS = 2000; const TWO_SECONDS = 2000;
let progressView: Backbone.View | undefined; let progressNode: HTMLDivElement | undefined;
let spinnerStart; let spinnerStart;
let progressTimeout: NodeJS.Timeout | undefined = setTimeout(() => { let progressTimeout: NodeJS.Timeout | undefined = setTimeout(() => {
log.info(`longRunningTaskWrapper/${idLog}: Creating spinner`); progressNode = document.createElement('div');
// Note: this component uses a portal to render itself into the top-level DOM. No log.info(`longRunningTaskWrapper/${idLog}: Creating spinner`);
// need to attach it to the DOM here. render(<ProgressModal i18n={window.i18n} />, progressNode);
progressView = new ReactWrapperView({
className: 'progress-modal-wrapper',
JSX: <ProgressModal i18n={window.i18n} />,
});
spinnerStart = Date.now(); spinnerStart = Date.now();
}, TWO_SECONDS); }, TWO_SECONDS);
@ -47,7 +43,7 @@ export async function longRunningTaskWrapper<T>({
clearTimeoutIfNecessary(progressTimeout); clearTimeoutIfNecessary(progressTimeout);
progressTimeout = undefined; progressTimeout = undefined;
if (progressView) { if (progressNode) {
const now = Date.now(); const now = Date.now();
if (spinnerStart && now - spinnerStart < ONE_SECOND) { if (spinnerStart && now - spinnerStart < ONE_SECOND) {
log.info( log.info(
@ -55,8 +51,8 @@ export async function longRunningTaskWrapper<T>({
); );
await window.Signal.Util.sleep(ONE_SECOND); await window.Signal.Util.sleep(ONE_SECOND);
} }
progressView.remove(); unmountComponentAtNode(progressNode);
progressView = undefined; progressNode = undefined;
} }
return result; return result;
@ -68,27 +64,14 @@ export async function longRunningTaskWrapper<T>({
clearTimeoutIfNecessary(progressTimeout); clearTimeoutIfNecessary(progressTimeout);
progressTimeout = undefined; progressTimeout = undefined;
if (progressView) { if (progressNode) {
progressView.remove(); unmountComponentAtNode(progressNode);
progressView = undefined; progressNode = undefined;
} }
if (!suppressErrorDialog) { if (!suppressErrorDialog) {
log.info(`longRunningTaskWrapper/${idLog}: Showing error dialog`); log.info(`longRunningTaskWrapper/${idLog}: Showing error dialog`);
window.reduxActions.globalModals.showErrorModal({});
// Note: this component uses a portal to render itself into the top-level DOM. No
// need to attach it to the DOM here.
const errorView: Backbone.View = new ReactWrapperView({
className: 'error-modal-wrapper',
JSX: (
<ErrorModal
i18n={window.i18n}
onClose={() => {
errorView.remove();
}}
/>
),
});
} }
throw error; throw error;

View file

@ -4,8 +4,6 @@
import React from 'react'; import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom'; import { render, unmountComponentAtNode } from 'react-dom';
import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember';
import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed'; import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed';
import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved'; import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved';
import type { import type {
@ -24,8 +22,6 @@ import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPa
import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit'; import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment'; import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment';
export function showToast(Toast: typeof ToastAlreadyGroupMember): void;
export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): void;
export function showToast(Toast: typeof ToastCaptchaFailed): void; export function showToast(Toast: typeof ToastCaptchaFailed): void;
export function showToast(Toast: typeof ToastCaptchaSolved): void; export function showToast(Toast: typeof ToastCaptchaSolved): void;
export function showToast( export function showToast(

View file

@ -1,49 +0,0 @@
// Copyright 2018-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react';
import * as ReactDOM from 'react-dom';
import * as Backbone from 'backbone';
export class ReactWrapperView extends Backbone.View {
private readonly onClose?: () => unknown;
private JSX: ReactElement;
constructor({
className,
onClose,
JSX,
}: Readonly<{
className?: string;
onClose?: () => unknown;
JSX: ReactElement;
}>) {
super();
this.className = className ?? 'react-wrapper';
this.JSX = JSX;
this.onClose = onClose;
this.render();
}
update(JSX: ReactElement): void {
this.JSX = JSX;
this.render();
}
override render(): this {
this.el.className = this.className;
ReactDOM.render(this.JSX, this.el);
return this;
}
override remove(): this {
if (this.onClose) {
this.onClose();
}
ReactDOM.unmountComponentAtNode(this.el);
Backbone.View.prototype.remove.call(this);
return this;
}
}

View file

@ -1,16 +1,17 @@
// Copyright 2020-2022 Signal Messenger, LLC // Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable camelcase */ /* eslint-disable camelcase, max-classes-per-file */
import type * as Backbone from 'backbone'; import type { ReactElement } from 'react';
import * as Backbone from 'backbone';
import * as ReactDOM from 'react-dom';
import { render } from 'mustache'; import { render } from 'mustache';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import { getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { isGroup } from '../util/whatTypeOfConversation'; import { isGroup } from '../util/whatTypeOfConversation';
import { ReactWrapperView } from './ReactWrapperView';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { createConversationView } from '../state/roots/createConversationView'; import { createConversationView } from '../state/roots/createConversationView';
import { import {
@ -197,4 +198,47 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} }
} }
class ReactWrapperView extends Backbone.View {
private readonly onClose?: () => unknown;
private JSX: ReactElement;
constructor({
className,
onClose,
JSX,
}: Readonly<{
className?: string;
onClose?: () => unknown;
JSX: ReactElement;
}>) {
super();
this.className = className ?? 'react-wrapper';
this.JSX = JSX;
this.onClose = onClose;
this.render();
}
update(JSX: ReactElement): void {
this.JSX = JSX;
this.render();
}
override render(): this {
this.el.className = this.className;
ReactDOM.render(this.JSX, this.el);
return this;
}
override remove(): this {
if (this.onClose) {
this.onClose();
}
ReactDOM.unmountComponentAtNode(this.el);
Backbone.View.prototype.remove.call(this);
return this;
}
}
window.Whisper.ConversationView = ConversationView; window.Whisper.ConversationView = ConversationView;

4
ts/window.d.ts vendored
View file

@ -37,9 +37,7 @@ import type { ConversationController } from './ConversationController';
import type { ReduxActions } from './state/types'; import type { ReduxActions } from './state/types';
import type { createStore } from './state/createStore'; import type { createStore } from './state/createStore';
import type { createApp } from './state/roots/createApp'; import type { createApp } from './state/roots/createApp';
import type { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import type { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer'; import type { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
import type { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
import type * as appDuck from './state/ducks/app'; import type * as appDuck from './state/ducks/app';
import type * as callingDuck from './state/ducks/calling'; import type * as callingDuck from './state/ducks/calling';
import type * as conversationsDuck from './state/ducks/conversations'; import type * as conversationsDuck from './state/ducks/conversations';
@ -159,9 +157,7 @@ export type SignalCoreType = {
createStore: typeof createStore; createStore: typeof createStore;
Roots: { Roots: {
createApp: typeof createApp; createApp: typeof createApp;
createGroupV2JoinModal: typeof createGroupV2JoinModal;
createSafetyNumberViewer: typeof createSafetyNumberViewer; createSafetyNumberViewer: typeof createSafetyNumberViewer;
createShortcutGuideModal: typeof createShortcutGuideModal;
}; };
Ducks: { Ducks: {
app: typeof appDuck; app: typeof appDuck;