diff --git a/ts/background.ts b/ts/background.ts index 57347380c..58180a8e3 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -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 { 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 { // 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 { } // 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; diff --git a/ts/components/ErrorModal.stories.tsx b/ts/components/ErrorModal.stories.tsx index 42722fd5c..51a3d40b7 100644 --- a/ts/components/ErrorModal.stories.tsx +++ b/ts/components/ErrorModal.stories.tsx @@ -16,7 +16,6 @@ const i18n = setupI18n('en', enMessages); const createProps = (overrideProps: Partial = {}): 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', })} /> ); diff --git a/ts/components/ErrorModal.tsx b/ts/components/ErrorModal.tsx index 5d6dcb1aa..1e9b8b5bf 100644 --- a/ts/components/ErrorModal.tsx +++ b/ts/components/ErrorModal.tsx @@ -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 = ( ); diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index 3d75f5bce..e68171f6b 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -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 ( + + ); + } + + if (isStoriesSettingsVisible) { + return renderStoriesSettings(); + } + + if (isWhatsNewVisible) { + return ; + } + 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 ; - } - - if (forwardMessageProps) { - return renderForwardMessageModal(); - } - - if (isSignalConnectionsVisible) { - return ( - - ); - } - - if (isStoriesSettingsVisible) { - return renderStoriesSettings(); - } - - if (stickerPackPreviewId) { - return renderStickerPreviewModal(); - } - return null; } diff --git a/ts/components/ShortcutGuideModal.tsx b/ts/components/ShortcutGuideModal.tsx index ea22ba930..7f2fcd81c 100644 --- a/ts/components/ShortcutGuideModal.tsx +++ b/ts/components/ShortcutGuideModal.tsx @@ -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(null); React.useEffect(() => { @@ -34,10 +35,10 @@ export const ShortcutGuideModal = React.memo(function ShortcutGuideModalInner(
, diff --git a/ts/components/ToastAlreadyGroupMember.stories.tsx b/ts/components/ToastAlreadyGroupMember.stories.tsx deleted file mode 100644 index d48bb227d..000000000 --- a/ts/components/ToastAlreadyGroupMember.stories.tsx +++ /dev/null @@ -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.story = { - name: 'ToastAlreadyGroupMember', -}; diff --git a/ts/components/ToastAlreadyGroupMember.tsx b/ts/components/ToastAlreadyGroupMember.tsx deleted file mode 100644 index 88c6a1eee..000000000 --- a/ts/components/ToastAlreadyGroupMember.tsx +++ /dev/null @@ -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 ( - {i18n('GroupV2--join--already-in-group')} - ); -} diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 0a5bdc4b9..fae313e0d 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -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: { diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index d810a77ba..7be3a5ec0 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -45,6 +45,22 @@ export function ToastManager({ ); } + if (toastType === ToastType.AlreadyGroupMember) { + return ( + + {i18n('GroupV2--join--already-in-group')} + + ); + } + + if (toastType === ToastType.AlreadyRequestedToJoin) { + return ( + + {i18n('GroupV2--join--already-awaiting-approval')} + + ); + } + if (toastType === ToastType.Blocked) { return {i18n('unblockToSend')}; } diff --git a/ts/groups/joinViaLink.tsx b/ts/groups/joinViaLink.ts similarity index 84% rename from ts/groups/joinViaLink.tsx rename to ts/groups/joinViaLink.ts index b23555ef5..53f2ec992 100644 --- a/ts/groups/joinViaLink.tsx +++ b/ts/groups/joinViaLink.ts @@ -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 { @@ -43,15 +41,15 @@ export async function joinViaLink(hash: string): Promise { 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 { 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 { 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 { 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 { conversationId: existingConversation.id, }); - showToast(ToastAlreadyRequestedToJoin); + window.reduxActions.toast.showToast(ToastType.AlreadyRequestedToJoin); return; } @@ -202,9 +200,9 @@ export async function joinViaLink(hash: string): Promise { 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 { 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 { 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 { }; // 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 { await promise; } - -function showErrorDialog(description: string, title: string) { - const errorView = new ReactWrapperView({ - className: 'error-modal-wrapper', - JSX: ( - { - errorView.remove(); - }} - /> - ), - }); -} diff --git a/ts/signal.ts b/ts/signal.ts index a45bdd31f..1efce74be 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -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 = { diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index bd37a6855..9f404b5ef 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -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; } diff --git a/ts/state/roots/createGroupV2JoinModal.tsx b/ts/state/roots/createGroupV2JoinModal.tsx index e97c4c425..52494aa7e 100644 --- a/ts/state/roots/createGroupV2JoinModal.tsx +++ b/ts/state/roots/createGroupV2JoinModal.tsx @@ -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'; diff --git a/ts/state/roots/createShortcutGuideModal.tsx b/ts/state/roots/createShortcutGuideModal.tsx deleted file mode 100644 index cbb005467..000000000 --- a/ts/state/roots/createShortcutGuideModal.tsx +++ /dev/null @@ -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 => ( - - - -); diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx index 468da67f1..f7aa6ea49 100644 --- a/ts/state/smart/GlobalModalContainer.tsx +++ b/ts/state/smart/GlobalModalContainer.tsx @@ -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 ; @@ -38,42 +41,95 @@ function renderSendAnywayDialog(): JSX.Element { return ; } -const mapStateToProps = (state: StateType) => { - const i18n = getIntl(state); +function renderShortcutGuideModal(): JSX.Element { + return ; +} - 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( + state => state.globalModals + ); + + const { + closeErrorModal, + hideWhatsNewModal, + hideUserNotFoundModal, + toggleSignalConnectionsModal, + } = useGlobalModalActions(); + + const renderAddUserToAnotherGroup = useCallback(() => { + return ( + + ); + }, [addUserToAnotherGroupModalContactId]); + + const renderSafetyNumber = useCallback( + () => ( + + ), + [safetyNumberModalContactId] + ); + + const renderStickerPreviewModal = useCallback( + () => stickerPackPreviewId ? ( ) : null, - renderSafetyNumber: () => ( - ( + ), - renderAddUserToAnotherGroup: () => { - return ( - - ); - }, - renderSendAnywayDialog, - }; -}; + [closeErrorModal, i18n] + ); -const smart = connect(mapStateToProps, mapDispatchToProps); - -export const SmartGlobalModalContainer = smart(GlobalModalContainer); + return ( + + ); +} diff --git a/ts/state/smart/ShortcutGuideModal.tsx b/ts/state/smart/ShortcutGuideModal.tsx index 9269b46fb..a054ad8ff 100644 --- a/ts/state/smart/ShortcutGuideModal.tsx +++ b/ts/state/smart/ShortcutGuideModal.tsx @@ -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), diff --git a/ts/test-node/jobs/JobQueue_test.ts b/ts/test-node/jobs/JobQueue_test.ts index a0e0a1ded..b35113a01 100644 --- a/ts/test-node/jobs/JobQueue_test.ts +++ b/ts/test-node/jobs/JobQueue_test.ts @@ -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', () => { diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx index e94e27cf4..f9ff33a53 100644 --- a/ts/types/Toast.tsx +++ b/ts/types/Toast.tsx @@ -3,6 +3,8 @@ export enum ToastType { AddingUserToGroup = 'AddingUserToGroup', + AlreadyGroupMember = 'AlreadyGroupMember', + AlreadyRequestedToJoin = 'AlreadyRequestedToJoin', Blocked = 'Blocked', BlockedGroup = 'BlockedGroup', CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments', diff --git a/ts/util/createIPCEvents.tsx b/ts/util/createIPCEvents.ts similarity index 95% rename from ts/util/createIPCEvents.tsx rename to ts/util/createIPCEvents.ts index bc3267d8e..8708cb90f 100644 --- a/ts/util/createIPCEvents.tsx +++ b/ts/util/createIPCEvents.ts @@ -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: ( - { - 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: ( - { - errorView.remove(); - }} - /> - ), + window.reduxActions.globalModals.showErrorModal({ + description: window.i18n('unknown-sgnl-link'), }); } diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 490cd7c29..70c395a85 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -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" diff --git a/ts/util/longRunningTaskWrapper.tsx b/ts/util/longRunningTaskWrapper.tsx index 2006ced72..553fa866d 100644 --- a/ts/util/longRunningTaskWrapper.tsx +++ b/ts/util/longRunningTaskWrapper.tsx @@ -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({ @@ -24,17 +24,13 @@ export async function longRunningTaskWrapper({ 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: , - }); + log.info(`longRunningTaskWrapper/${idLog}: Creating spinner`); + render(, progressNode); spinnerStart = Date.now(); }, TWO_SECONDS); @@ -47,7 +43,7 @@ export async function longRunningTaskWrapper({ 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({ ); 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({ 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: ( - { - errorView.remove(); - }} - /> - ), - }); + window.reduxActions.globalModals.showErrorModal({}); } throw error; diff --git a/ts/util/showToast.tsx b/ts/util/showToast.tsx index 018198d7b..001f7f292 100644 --- a/ts/util/showToast.tsx +++ b/ts/util/showToast.tsx @@ -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( diff --git a/ts/views/ReactWrapperView.ts b/ts/views/ReactWrapperView.ts deleted file mode 100644 index 8f43daf8e..000000000 --- a/ts/views/ReactWrapperView.ts +++ /dev/null @@ -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; - } -} diff --git a/ts/views/conversation_view.tsx b/ts/views/conversation_view.tsx index f031767a1..a8985668a 100644 --- a/ts/views/conversation_view.tsx +++ b/ts/views/conversation_view.tsx @@ -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 { } } +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; diff --git a/ts/window.d.ts b/ts/window.d.ts index 726450f8a..aa5e8b13f 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -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;