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 { loadRecentEmojis } from './util/loadRecentEmojis';
import { deleteAllLogs } from './util/deleteAllLogs';
import { ReactWrapperView } from './views/ReactWrapperView';
import { ToastCaptchaFailed } from './components/ToastCaptchaFailed';
import { ToastCaptchaSolved } from './components/ToastCaptchaSolved';
import { showToast } from './util/showToast';
@ -1267,30 +1266,6 @@ export async function startApp(): Promise<void> {
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 => {
const { ctrlKey, metaKey, shiftKey } = event;
@ -1309,7 +1284,7 @@ export async function startApp(): Promise<void> {
// Show keyboard shortcuts - handled by Electron-managed keyboard shortcuts
// However, on linux Ctrl+/ selects all text, so we prevent that
if (commandOrCtrl && key === '/') {
window.showKeyboardShortcuts();
window.Events.showKeyboardShortcuts();
event.stopPropagation();
event.preventDefault();
@ -1374,9 +1349,11 @@ export async function startApp(): Promise<void> {
}
// Cancel out of keyboard shortcut screen - has first precedence
if (shortcutGuideView && key === 'Escape') {
shortcutGuideView.remove();
shortcutGuideView = null;
const isShortcutGuideModalVisible = window.reduxStore
? window.reduxStore.getState().globalModals.isShortcutGuideModalVisible
: false;
if (isShortcutGuideModalVisible && key === 'Escape') {
window.reduxActions.globalModals.closeShortcutGuideModal();
event.preventDefault();
event.stopPropagation();
return;

View file

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

View file

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

View file

@ -19,9 +19,18 @@ import { WhatsNewModal } from './WhatsNewModal';
export type PropsType = {
i18n: LocalizerType;
theme: ThemeType;
// AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId?: string;
renderAddUserToAnotherGroup: () => JSX.Element;
// ContactModal
contactModalState?: ContactModalStateType;
renderContactModal: () => JSX.Element;
// ErrorModal
errorModalProps?: { description?: string; title?: string };
renderErrorModal: (opts: {
description?: string;
title?: string;
}) => JSX.Element;
// ForwardMessageModal
forwardMessageProps?: ForwardMessagePropsType;
renderForwardMessageModal: () => JSX.Element;
@ -31,9 +40,9 @@ export type PropsType = {
// SafetyNumberModal
safetyNumberModalContactId?: string;
renderSafetyNumber: () => JSX.Element;
// AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId?: string;
renderAddUserToAnotherGroup: () => JSX.Element;
// ShortcutGuideModal
isShortcutGuideModalVisible: boolean;
renderShortcutGuideModal: () => JSX.Element;
// SignalConnectionsModal
isSignalConnectionsVisible: boolean;
toggleSignalConnectionsModal: () => unknown;
@ -57,9 +66,15 @@ export type PropsType = {
export function GlobalModalContainer({
i18n,
// AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId,
renderAddUserToAnotherGroup,
// ContactModal
contactModalState,
renderContactModal,
// ErrorModal
errorModalProps,
renderErrorModal,
// ForwardMessageModal
forwardMessageProps,
renderForwardMessageModal,
@ -69,9 +84,9 @@ export function GlobalModalContainer({
// SafetyNumberModal
safetyNumberModalContactId,
renderSafetyNumber,
// AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId,
renderAddUserToAnotherGroup,
// ShortcutGuideModal
isShortcutGuideModalVisible,
renderShortcutGuideModal,
// SignalConnectionsModal
isSignalConnectionsVisible,
toggleSignalConnectionsModal,
@ -92,18 +107,66 @@ export function GlobalModalContainer({
hideWhatsNewModal,
isWhatsNewVisible,
}: PropsType): JSX.Element | null {
// We want the send anyway dialog to supersede most modals since this is an
// immediate action the user needs to take.
// We want the following dialogs to show in this order:
// 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) {
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) {
return renderSafetyNumber();
}
if (addUserToAnotherGroupModalContactId) {
return renderAddUserToAnotherGroup();
if (stickerPackPreviewId) {
return renderStickerPreviewModal();
}
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;
}

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
import * as React from 'react';
@ -9,14 +9,15 @@ import { ShortcutGuide } from './ShortcutGuide';
export type PropsType = {
hasInstalledStickers: boolean;
platform: string;
readonly close: () => unknown;
readonly closeShortcutGuideModal: () => unknown;
readonly i18n: LocalizerType;
};
export const ShortcutGuideModal = React.memo(function ShortcutGuideModalInner(
props: PropsType
) {
const { i18n, close, hasInstalledStickers, platform } = props;
const { i18n, closeShortcutGuideModal, hasInstalledStickers, platform } =
props;
const [root, setRoot] = React.useState<HTMLElement | null>(null);
React.useEffect(() => {
@ -34,10 +35,10 @@ export const ShortcutGuideModal = React.memo(function ShortcutGuideModalInner(
<div className="module-shortcut-guide-modal">
<div className="module-shortcut-guide-container">
<ShortcutGuide
close={closeShortcutGuideModal}
hasInstalledStickers={hasInstalledStickers}
platform={platform}
close={close}
i18n={i18n}
platform={platform}
/>
</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({});
Blocked.args = {
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) {
return <Toast onClose={hideToast}>{i18n('unblockToSend')}</Toast>;
}

View file

@ -1,7 +1,19 @@
// Copyright 2021-2022 Signal Messenger, LLC
// 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 {
applyNewAvatar,
decryptGroupDescription,
@ -12,25 +24,11 @@ import {
LINK_VERSION_ERROR,
parseGroupLink,
} from '../groups';
import * as Errors from '../types/errors';
import { UUIDKind } from '../types/UUID';
import * as Bytes from '../Bytes';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { isGroupV1 } from '../util/whatTypeOfConversation';
import { createGroupV2JoinModal } from '../state/roots/createGroupV2JoinModal';
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 { isGroupV1 } from '../util/whatTypeOfConversation';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { sleep } from '../util/sleep';
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}`);
if (error instanceof Error && error.name === LINK_VERSION_ERROR) {
showErrorDialog(
window.i18n('GroupV2--join--unknown-link-version'),
window.i18n('GroupV2--join--unknown-link-version--title')
);
window.reduxActions.globalModals.showErrorModal({
description: window.i18n('GroupV2--join--unknown-link-version'),
title: window.i18n('GroupV2--join--unknown-link-version--title'),
});
} else {
showErrorDialog(
window.i18n('GroupV2--join--invalid-link'),
window.i18n('GroupV2--join--invalid-link--title')
);
window.reduxActions.globalModals.showErrorModal({
description: window.i18n('GroupV2--join--invalid-link'),
title: window.i18n('GroupV2--join--invalid-link--title'),
});
}
return;
}
@ -74,7 +72,7 @@ export async function joinViaLink(hash: string): Promise<void> {
window.reduxActions.conversations.showConversation({
conversationId: existingConversation.id,
});
showToast(ToastAlreadyGroupMember);
window.reduxActions.toast.showToast(ToastType.AlreadyGroupMember);
return;
}
@ -99,20 +97,20 @@ export async function joinViaLink(hash: string): Promise<void> {
error instanceof HTTPError &&
error.responseHeaders['x-signal-forbidden-reason']
) {
showErrorDialog(
window.i18n('GroupV2--join--link-forbidden'),
window.i18n('GroupV2--join--link-forbidden--title')
);
window.reduxActions.globalModals.showErrorModal({
description: window.i18n('GroupV2--join--link-forbidden'),
title: window.i18n('GroupV2--join--link-forbidden--title'),
});
} else if (error instanceof HTTPError && error.code === 403) {
showErrorDialog(
window.i18n('GroupV2--join--link-revoked'),
window.i18n('GroupV2--join--link-revoked--title')
);
window.reduxActions.globalModals.showErrorModal({
description: window.i18n('GroupV2--join--link-revoked'),
title: window.i18n('GroupV2--join--link-revoked--title'),
});
} else {
showErrorDialog(
window.i18n('GroupV2--join--general-join-failure'),
window.i18n('GroupV2--join--general-join-failure--title')
);
window.reduxActions.globalModals.showErrorModal({
description: window.i18n('GroupV2--join--general-join-failure'),
title: window.i18n('GroupV2--join--general-join-failure--title'),
});
}
return;
}
@ -121,10 +119,10 @@ export async function joinViaLink(hash: string): Promise<void> {
log.error(
`joinViaLink/${logId}: addFromInviteLink value of ${result.addFromInviteLink} is invalid`
);
showErrorDialog(
window.i18n('GroupV2--join--link-revoked'),
window.i18n('GroupV2--join--link-revoked--title')
);
window.reduxActions.globalModals.showErrorModal({
description: window.i18n('GroupV2--join--link-revoked'),
title: window.i18n('GroupV2--join--link-revoked--title'),
});
return;
}
@ -168,7 +166,7 @@ export async function joinViaLink(hash: string): Promise<void> {
conversationId: existingConversation.id,
});
showToast(ToastAlreadyRequestedToJoin);
window.reduxActions.toast.showToast(ToastType.AlreadyRequestedToJoin);
return;
}
@ -202,9 +200,9 @@ export async function joinViaLink(hash: string): Promise<void> {
const closeDialog = async () => {
try {
if (groupV2InfoDialog) {
groupV2InfoDialog.remove();
groupV2InfoDialog = undefined;
if (groupV2InfoNode) {
unmountComponentAtNode(groupV2InfoNode);
groupV2InfoNode = undefined;
}
window.reduxActions.conversations.setPreJoinConversation(undefined);
@ -220,9 +218,9 @@ export async function joinViaLink(hash: string): Promise<void> {
const join = async () => {
try {
if (groupV2InfoDialog) {
groupV2InfoDialog.remove();
groupV2InfoDialog = undefined;
if (groupV2InfoNode) {
unmountComponentAtNode(groupV2InfoNode);
groupV2InfoNode = undefined;
}
window.reduxActions.conversations.setPreJoinConversation(undefined);
@ -375,13 +373,13 @@ export async function joinViaLink(hash: string): Promise<void> {
log.info(`joinViaLink/${logId}: Showing modal`);
let groupV2InfoDialog: Backbone.View | undefined = new ReactWrapperView({
className: 'group-v2-join-dialog-wrapper',
JSX: window.Signal.State.Roots.createGroupV2JoinModal(window.reduxStore, {
join,
onClose: closeDialog,
}),
});
let groupV2InfoNode: HTMLDivElement | undefined =
document.createElement('div');
render(
createGroupV2JoinModal(window.reduxStore, { join, onClose: closeDialog }),
groupV2InfoNode
);
// We declare a new function here so we can await but not block
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
if (!groupV2InfoDialog) {
if (!groupV2InfoNode) {
await window.Signal.Migrations.deleteAttachmentData(
attributes.avatar.path
);
@ -426,19 +424,3 @@ export async function joinViaLink(hash: string): Promise<void> {
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
import { createApp } from './state/roots/createApp';
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
import { createStore } from './state/createStore';
import * as appDuck from './state/ducks/app';
@ -393,9 +391,7 @@ export const setup = (options: {
const Roots = {
createApp,
createGroupV2JoinModal,
createSafetyNumberViewer,
createShortcutGuideModal,
};
const Ducks = {

View file

@ -41,10 +41,15 @@ type MigrateToGV2PropsType = {
export type GlobalModalsStateType = Readonly<{
addUserToAnotherGroupModalContactId?: string;
contactModalState?: ContactModalStateType;
errorModalProps?: {
description?: string;
title?: string;
};
forwardMessageProps?: ForwardMessagePropsType;
gv2MigrationProps?: MigrateToGV2PropsType;
isProfileEditorVisible: boolean;
isSignalConnectionsVisible: boolean;
isShortcutGuideModalVisible: boolean;
isStoriesSettingsVisible: boolean;
isWhatsNewVisible: 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 SHOW_STICKER_PACK_PREVIEW = 'globalModals/SHOW_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 = {
contactId: string;
@ -186,6 +195,26 @@ type CloseStickerPackPreviewActionType = {
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 =
| StartMigrationToGV2ActionType
| CloseGV2MigrationDialogActionType
@ -201,6 +230,10 @@ export type GlobalModalsActionType =
| ShowSendAnywayDialogActionType
| CloseStickerPackPreviewActionType
| ShowStickerPackPreviewActionType
| CloseErrorModalActionType
| ShowErrorModalActionType
| CloseShortcutGuideModalActionType
| ShowShortcutGuideModalActionType
| ToggleForwardMessageModalActionType
| ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType
@ -231,6 +264,10 @@ export const actions = {
closeGV2MigrationDialog,
showStickerPackPreview,
closeStickerPackPreview,
closeErrorModal,
showErrorModal,
closeShortcutGuideModal,
showShortcutGuideModal,
};
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
export function getEmptyState(): GlobalModalsStateType {
return {
isProfileEditorVisible: false,
isShortcutGuideModalVisible: false,
isSignalConnectionsVisible: false,
isStoriesSettingsVisible: 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;
}

View file

@ -1,6 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// TODO DESKTOP-4761
import React from 'react';
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
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import { ErrorModal } from '../../components/ErrorModal';
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
import { SmartContactModal } from './ContactModal';
import { SmartForwardMessageModal } from './ForwardMessageModal';
import { SmartProfileEditorModal } from './ProfileEditorModal';
import { SmartSafetyNumberModal } from './SafetyNumberModal';
import { SmartSendAnywayDialog } from './SendAnywayDialog';
import { SmartShortcutGuideModal } from './ShortcutGuideModal';
import { SmartStickerPreviewModal } from './StickerPreviewModal';
import { SmartStoriesSettingsModal } from './StoriesSettingsModal';
import { getConversationsStoppingSend } from '../selectors/conversations';
import { mapDispatchToProps } from '../actions';
import { getIntl, getTheme } from '../selectors/user';
import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
import { SmartStickerPreviewModal } from './StickerPreviewModal';
import { useGlobalModalActions } from '../ducks/globalModals';
function renderProfileEditor(): JSX.Element {
return <SmartProfileEditorModal />;
@ -38,42 +41,95 @@ function renderSendAnywayDialog(): JSX.Element {
return <SmartSendAnywayDialog />;
}
const mapStateToProps = (state: StateType) => {
const i18n = getIntl(state);
function renderShortcutGuideModal(): JSX.Element {
return <SmartShortcutGuideModal />;
}
const { stickerPackPreviewId } = state.globalModals;
export function SmartGlobalModalContainer(): JSX.Element {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
return {
...state.globalModals,
hasSafetyNumberChangeModal: getConversationsStoppingSend(state).length > 0,
i18n,
theme: getTheme(state),
renderContactModal,
renderForwardMessageModal,
renderProfileEditor,
renderStoriesSettings,
renderStickerPreviewModal: () =>
const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
const {
addUserToAnotherGroupModalContactId,
isProfileEditorVisible,
isSignalConnectionsVisible,
isShortcutGuideModalVisible,
isStoriesSettingsVisible,
isWhatsNewVisible,
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 ? (
<SmartStickerPreviewModal packId={stickerPackPreviewId} />
) : null,
renderSafetyNumber: () => (
<SmartSafetyNumberModal
contactID={String(state.globalModals.safetyNumberModalContactId)}
[stickerPackPreviewId]
);
const renderErrorModal = useCallback(
({ description, title }: { description?: string; title?: string }) => (
<ErrorModal
title={title}
description={description}
i18n={i18n}
onClose={closeErrorModal}
/>
),
renderAddUserToAnotherGroup: () => {
return (
<SmartAddUserToAnotherGroupModal
contactID={String(
state.globalModals.addUserToAnotherGroupModalContactId
)}
/>
);
},
renderSendAnywayDialog,
};
};
[closeErrorModal, i18n]
);
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGlobalModalContainer = smart(GlobalModalContainer);
return (
<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,
} from '../selectors/stickers';
export type ExternalProps = {
close: () => unknown;
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { close } = props;
const mapStateToProps = (state: StateType) => {
const blessedPacks = getBlessedStickerPacks(state);
const installedPacks = getInstalledStickerPacks(state);
const knownPacks = getKnownStickerPacks(state);
@ -38,7 +32,6 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const platform = getPlatform(state);
return {
close,
hasInstalledStickers,
platform,
i18n: getIntl(state),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,6 @@
import React from 'react';
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 { ToastCaptchaSolved } from '../components/ToastCaptchaSolved';
import type {
@ -24,8 +22,6 @@ import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPa
import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
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 ToastCaptchaSolved): void;
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
// 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 type { ConversationModel } from '../models/conversations';
import { getMessageById } from '../messages/getMessageById';
import { strictAssert } from '../util/assert';
import { isGroup } from '../util/whatTypeOfConversation';
import { ReactWrapperView } from './ReactWrapperView';
import * as log from '../logging/log';
import { createConversationView } from '../state/roots/createConversationView';
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;

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