Move getUntrustedContacts out of conversation_view

This commit is contained in:
Josh Perez 2022-08-16 19:59:11 -04:00 committed by GitHub
parent 96c4cc4bcf
commit 936ce91b2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 410 additions and 414 deletions

View file

@ -52,11 +52,8 @@ type PropsType = {
export const App = ({
appView,
cancelConversationVerification,
conversationsStoppingSend,
executeMenuAction,
executeMenuRole,
getPreferredBadge,
hasInitialLoadCompleted,
hasSelectedStoryData,
hideMenuBar,
@ -75,7 +72,6 @@ export const App = ({
renderCustomizingPreferredReactionsModal,
renderGlobalModalContainer,
renderLeftPane,
renderSafetyNumber,
renderStories,
renderStoryViewer,
requestVerification,
@ -86,7 +82,6 @@ export const App = ({
theme,
titleBarDoubleClick,
toastType,
verifyConversationsStoppingSend,
}: PropsType): JSX.Element => {
let contents;
@ -107,23 +102,17 @@ export const App = ({
} else if (appView === AppViewType.Inbox) {
contents = (
<Inbox
cancelConversationVerification={cancelConversationVerification}
conversationsStoppingSend={conversationsStoppingSend}
hasInitialLoadCompleted={hasInitialLoadCompleted}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
isCustomizingPreferredReactions={isCustomizingPreferredReactions}
renderCustomizingPreferredReactionsModal={
renderCustomizingPreferredReactionsModal
}
renderLeftPane={renderLeftPane}
renderSafetyNumber={renderSafetyNumber}
selectedConversationId={selectedConversationId}
selectedMessage={selectedMessage}
showConversation={showConversation}
showWhatsNewModal={showWhatsNewModal}
theme={theme}
verifyConversationsStoppingSend={verifyConversationsStoppingSend}
/>
);
}

View file

@ -25,6 +25,7 @@ export type OwnProps = Readonly<{
hasXButton?: boolean;
i18n: LocalizerType;
moduleClassName?: string;
noMouseClose?: boolean;
onCancel?: () => unknown;
onClose: () => unknown;
onTopOfEverything?: boolean;
@ -56,18 +57,19 @@ function getButtonVariant(
export const ConfirmationDialog = React.memo(
({
moduleClassName,
actions = [],
cancelButtonVariant,
cancelText,
children,
hasXButton,
i18n,
moduleClassName,
noMouseClose,
onCancel,
onClose,
onTopOfEverything,
theme,
title,
hasXButton,
cancelButtonVariant,
onTopOfEverything,
}: Props) => {
const { close, overlayStyles, modalStyles } = useAnimated(onClose, {
getFrom: () => ({ opacity: 0, transform: 'scale(0.25)' }),
@ -94,10 +96,11 @@ export const ConfirmationDialog = React.memo(
return (
<ModalHost
onTopOfEverything={onTopOfEverything}
noMouseClose={noMouseClose}
onClose={close}
theme={theme}
onTopOfEverything={onTopOfEverything}
overlayStyles={overlayStyles}
theme={theme}
>
<animated.div style={modalStyles}>
<ModalWindow

View file

@ -6,6 +6,7 @@ import type {
ContactModalStateType,
ForwardMessagePropsType,
UserNotFoundModalStateType,
SafetyNumberChangedBlockingDataType,
} from '../state/ducks/globalModals';
import type { LocalizerType } from '../types/Util';
import { missingCaseError } from '../util/missingCaseError';
@ -35,6 +36,10 @@ type PropsType = {
// StoriesSettings
isStoriesSettingsVisible: boolean;
renderStoriesSettings: () => JSX.Element;
// SendAnywayDialog
hasSafetyNumberChangeModal: boolean;
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
renderSendAnywayDialog: () => JSX.Element;
// UserNotFoundModal
hideUserNotFoundModal: () => unknown;
userNotFoundModalState?: UserNotFoundModalStateType;
@ -63,6 +68,10 @@ export const GlobalModalContainer = ({
// StoriesSettings
isStoriesSettingsVisible,
renderStoriesSettings,
// SendAnywayDialog
hasSafetyNumberChangeModal,
safetyNumberChangedBlockingData,
renderSendAnywayDialog,
// UserNotFoundModal
hideUserNotFoundModal,
userNotFoundModalState,
@ -70,6 +79,12 @@ export const 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.
if (hasSafetyNumberChangeModal || safetyNumberChangedBlockingData) {
return renderSendAnywayDialog();
}
if (safetyNumberModalContactId) {
return renderSafetyNumber();
}

View file

@ -5,57 +5,39 @@ import type { ReactNode } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import type { ConversationModel } from '../models/conversations';
import type {
ConversationType,
ShowConversationType,
} from '../state/ducks/conversations';
import type { ShowConversationType } from '../state/ducks/conversations';
import type { ConversationView } from '../views/conversation_view';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { SafetyNumberProps } from './SafetyNumberChangeDialog';
import type { LocalizerType } from '../types/Util';
import * as log from '../logging/log';
import { SECOND } from '../util/durations';
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
import { ToastStickerPackInstallFailed } from './ToastStickerPackInstallFailed';
import { WhatsNewLink } from './WhatsNewLink';
import { showToast } from '../util/showToast';
import { strictAssert } from '../util/assert';
export type PropsType = {
cancelConversationVerification: () => void;
conversationsStoppingSend: Array<ConversationType>;
hasInitialLoadCompleted: boolean;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
isCustomizingPreferredReactions: boolean;
renderCustomizingPreferredReactionsModal: () => JSX.Element;
renderLeftPane: () => JSX.Element;
renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element;
selectedConversationId?: string;
selectedMessage?: string;
showConversation: ShowConversationType;
showWhatsNewModal: () => unknown;
theme: ThemeType;
verifyConversationsStoppingSend: () => void;
};
export const Inbox = ({
cancelConversationVerification,
conversationsStoppingSend,
hasInitialLoadCompleted,
getPreferredBadge,
i18n,
isCustomizingPreferredReactions,
renderCustomizingPreferredReactionsModal,
renderLeftPane,
renderSafetyNumber,
selectedConversationId,
selectedMessage,
showConversation,
showWhatsNewModal,
theme,
verifyConversationsStoppingSend,
}: PropsType): JSX.Element => {
const [loadingMessageCount, setLoadingMessageCount] = useState(0);
const [internalHasInitialLoadCompleted, setInternalHasInitialLoadCompleted] =
@ -226,21 +208,7 @@ export const Inbox = ({
}
let activeModal: ReactNode;
if (conversationsStoppingSend.length) {
activeModal = (
<SafetyNumberChangeDialog
confirmText={i18n('safetyNumberChangeDialog__pending-messages')}
contacts={conversationsStoppingSend}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
onCancel={cancelConversationVerification}
onConfirm={verifyConversationsStoppingSend}
renderSafetyNumber={renderSafetyNumber}
theme={theme}
/>
);
}
if (!activeModal && isCustomizingPreferredReactions) {
if (isCustomizingPreferredReactions) {
activeModal = renderCustomizingPreferredReactionsModal();
}

View file

@ -14,6 +14,11 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { LocalizerType, ThemeType } from '../types/Util';
import { isInSystemContacts } from '../util/isInSystemContacts';
export enum SafetyNumberChangeSource {
Calling = 'Calling',
MessageSend = 'MessageSend',
}
export type SafetyNumberProps = {
contactID: string;
onClose: () => void;
@ -75,6 +80,7 @@ export const SafetyNumberChangeDialog = ({
},
]}
i18n={i18n}
noMouseClose
onCancel={onClose}
onClose={noop}
title={i18n('safetyNumberChanges')}

View file

@ -0,0 +1,44 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ExplodePromiseResultType } from '../util/explodePromise';
import type { UUIDStringType } from '../types/UUID';
import { UUID } from '../types/UUID';
// This module provides single serve promises in a pub/sub manner.
// One example usage is if you're calling a redux action creator but need to
// await some result within it, you may pass in this promise and access it in
// other parts of the app via its referencing UUID.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const promises = new Map<UUIDStringType, ExplodePromiseResultType<any>>();
export function set<T>(
explodedPromise: ExplodePromiseResultType<T>
): UUIDStringType {
let uuid = UUID.generate().toString();
while (promises.has(uuid)) {
uuid = UUID.generate().toString();
}
promises.set(uuid, {
promise: explodedPromise.promise,
resolve: value => {
promises.delete(uuid);
explodedPromise.resolve(value);
},
reject: err => {
promises.delete(uuid);
explodedPromise.reject(err);
},
});
return uuid;
}
export function get<T>(
uuid: UUIDStringType
): ExplodePromiseResultType<T> | undefined {
return promises.get(uuid);
}

View file

@ -1,75 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// This file is here temporarily while we're switching off of Backbone into
// React. In the future, and in React-land, please just import and use
// the component directly. This is the thin API layer to bridge the gap
// while we convert things over. Please delete this file once all usages are
// ported over.
import React from 'react';
import { unmountComponentAtNode, render } from 'react-dom';
import type { ConversationModel } from '../models/conversations';
import { SafetyNumberChangeDialog } from '../components/SafetyNumberChangeDialog';
import { getPreferredBadgeSelector } from '../state/selectors/badges';
import { getTheme } from '../state/selectors/user';
export type SafetyNumberChangeViewProps = {
confirmText?: string;
contacts: Array<ConversationModel>;
reject: () => void;
resolve: () => void;
};
let dialogContainerNode: HTMLElement | undefined;
function removeDialog() {
if (!dialogContainerNode) {
return;
}
unmountComponentAtNode(dialogContainerNode);
document.body.removeChild(dialogContainerNode);
dialogContainerNode = undefined;
}
export function showSafetyNumberChangeDialog(
options: SafetyNumberChangeViewProps
): void {
if (dialogContainerNode) {
removeDialog();
}
dialogContainerNode = document.createElement('div');
document.body.appendChild(dialogContainerNode);
const reduxState = window.reduxStore.getState();
const getPreferredBadge = getPreferredBadgeSelector(reduxState);
const theme = getTheme(reduxState);
render(
<SafetyNumberChangeDialog
confirmText={options.confirmText}
contacts={options.contacts.map(contact => contact.format())}
getPreferredBadge={getPreferredBadge}
i18n={window.i18n}
onCancel={() => {
options.reject();
removeDialog();
}}
onConfirm={() => {
options.resolve();
removeDialog();
}}
renderSafetyNumber={props => {
return window.Signal.State.Roots.createSafetyNumberViewer(
window.reduxStore,
props
);
}}
theme={theme}
/>,
dialogContainerNode
);
}

View file

@ -21,8 +21,14 @@ import { calling } from '../../services/calling';
import { getOwn } from '../../util/getOwn';
import { assert, strictAssert } from '../../util/assert';
import * as universalExpireTimer from '../../util/universalExpireTimer';
import type { ToggleProfileEditorErrorActionType } from './globalModals';
import { TOGGLE_PROFILE_EDITOR_ERROR } from './globalModals';
import type {
ShowSendAnywayDialogActiontype,
ToggleProfileEditorErrorActionType,
} from './globalModals';
import {
SHOW_SEND_ANYWAY_DIALOG,
TOGGLE_PROFILE_EDITOR_ERROR,
} from './globalModals';
import { isRecord } from '../../util/isRecord';
import type {
UUIDFetchStateKeyType,
@ -782,6 +788,7 @@ export type ConversationActionType =
| ShowArchivedConversationsActionType
| ShowChooseGroupMembersActionType
| ShowInboxActionType
| ShowSendAnywayDialogActiontype
| StartComposingActionType
| StartSettingGroupMetadataActionType
| ToggleConversationInChooseMembersActionType
@ -2162,6 +2169,45 @@ function closeComposerModal(
};
}
function getVerificationDataForConversation(
state: Readonly<ConversationsStateType>,
conversationId: string,
untrustedUuids: ReadonlyArray<string>
): Record<string, ConversationVerificationData> {
const { verificationDataByConversation } = state;
const existingPendingState = getOwn(
verificationDataByConversation,
conversationId
);
if (
!existingPendingState ||
existingPendingState.type ===
ConversationVerificationState.VerificationCancelled
) {
return {
[conversationId]: {
type: ConversationVerificationState.PendingVerification as const,
uuidsNeedingVerification: untrustedUuids,
},
};
}
const uuidsNeedingVerification: ReadonlyArray<string> = Array.from(
new Set([
...existingPendingState.uuidsNeedingVerification,
...untrustedUuids,
])
);
return {
[conversationId]: {
type: ConversationVerificationState.PendingVerification as const,
uuidsNeedingVerification,
},
};
}
export function reducer(
state: Readonly<ConversationsStateType> = getEmptyState(),
action: Readonly<ConversationActionType>
@ -2510,47 +2556,41 @@ export function reducer(
if (action.type === CONVERSATION_STOPPED_BY_MISSING_VERIFICATION) {
const { conversationId, untrustedUuids } = action.payload;
const { verificationDataByConversation } = state;
const existingPendingState = getOwn(
verificationDataByConversation,
conversationId
);
if (
!existingPendingState ||
existingPendingState.type ===
ConversationVerificationState.VerificationCancelled
) {
return {
...state,
verificationDataByConversation: {
...verificationDataByConversation,
[conversationId]: {
type: ConversationVerificationState.PendingVerification as const,
uuidsNeedingVerification: untrustedUuids,
},
},
};
}
const uuidsNeedingVerification: ReadonlyArray<string> = Array.from(
new Set([
...existingPendingState.uuidsNeedingVerification,
...untrustedUuids,
])
const nextVerificationData = getVerificationDataForConversation(
state,
conversationId,
untrustedUuids
);
return {
...state,
verificationDataByConversation: {
...verificationDataByConversation,
[conversationId]: {
type: ConversationVerificationState.PendingVerification as const,
uuidsNeedingVerification,
},
...state.verificationDataByConversation,
...nextVerificationData,
},
};
}
if (action.type === SHOW_SEND_ANYWAY_DIALOG) {
const verificationDataByConversation = {
...state.verificationDataByConversation,
};
action.payload.conversationsToPause.forEach(
(untrustedUuids, conversationId) => {
const nextVerificationData = getVerificationDataForConversation(
state,
conversationId,
Array.from(untrustedUuids)
);
Object.assign(verificationDataByConversation, nextVerificationData);
}
);
return {
...state,
verificationDataByConversation,
};
}
if (action.type === 'MESSAGE_CHANGED') {
const { id, conversationId, data } = action.payload;
const existingConversation = state.messagesByConversation[conversationId];

View file

@ -2,8 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction } from 'redux-thunk';
import type { StateType as RootStateType } from '../reducer';
import type { ExplodePromiseResultType } from '../../util/explodePromise';
import type { PropsForMessage } from '../selectors/message';
import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import type { StateType as RootStateType } from '../reducer';
import type { UUIDStringType } from '../../types/UUID';
import * as SingleServePromise from '../../services/singleServePromise';
import { getMessageById } from '../../messages/getMessageById';
import { getMessagePropsSelector } from '../selectors/message';
import { useBoundActions } from '../../hooks/useBoundActions';
@ -11,15 +15,20 @@ import { useBoundActions } from '../../hooks/useBoundActions';
// State
export type ForwardMessagePropsType = Omit<PropsForMessage, 'renderingContext'>;
export type SafetyNumberChangedBlockingDataType = {
readonly promiseUuid: UUIDStringType;
readonly source?: SafetyNumberChangeSource;
};
export type GlobalModalsStateType = {
readonly contactModalState?: ContactModalStateType;
readonly forwardMessageProps?: ForwardMessagePropsType;
readonly isProfileEditorVisible: boolean;
readonly isStoriesSettingsVisible: boolean;
readonly isSignalConnectionsVisible: boolean;
readonly isStoriesSettingsVisible: boolean;
readonly isWhatsNewVisible: boolean;
readonly profileEditorHasError: boolean;
readonly safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
readonly safetyNumberModalContactId?: string;
readonly userNotFoundModalState?: UserNotFoundModalStateType;
};
@ -42,6 +51,8 @@ export const TOGGLE_PROFILE_EDITOR_ERROR =
const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL';
const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL';
export const SHOW_SEND_ANYWAY_DIALOG = 'globalModals/SHOW_SEND_ANYWAY_DIALOG';
const HIDE_SEND_ANYWAY_DIALOG = 'globalModals/HIDE_SEND_ANYWAY_DIALOG';
export type ContactModalStateType = {
contactId: string;
@ -114,6 +125,17 @@ type HideStoriesSettingsActionType = {
type: typeof HIDE_STORIES_SETTINGS;
};
export type ShowSendAnywayDialogActiontype = {
type: typeof SHOW_SEND_ANYWAY_DIALOG;
payload: SafetyNumberChangedBlockingDataType & {
conversationsToPause: Map<string, Set<string>>;
};
};
type HideSendAnywayDialogActiontype = {
type: typeof HIDE_SEND_ANYWAY_DIALOG;
};
export type GlobalModalsActionType =
| HideContactModalActionType
| ShowContactModalActionType
@ -123,6 +145,8 @@ export type GlobalModalsActionType =
| ShowUserNotFoundModalActionType
| HideStoriesSettingsActionType
| ShowStoriesSettingsActionType
| HideSendAnywayDialogActiontype
| ShowSendAnywayDialogActiontype
| ToggleForwardMessageModalActionType
| ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType
@ -140,6 +164,8 @@ export const actions = {
showUserNotFoundModal,
hideStoriesSettings,
showStoriesSettings,
hideBlockingSafetyNumberChangeDialog,
showBlockingSafetyNumberChangeDialog,
toggleForwardMessageModal,
toggleProfileEditor,
toggleProfileEditorHasError,
@ -262,6 +288,31 @@ function toggleSignalConnectionsModal(): ToggleSignalConnectionsModalActionType
};
}
function showBlockingSafetyNumberChangeDialog(
conversationsToPause: Map<string, Set<string>>,
explodedPromise: ExplodePromiseResultType<boolean>,
source?: SafetyNumberChangeSource
): ThunkAction<void, RootStateType, unknown, ShowSendAnywayDialogActiontype> {
const promiseUuid = SingleServePromise.set<boolean>(explodedPromise);
return dispatch => {
dispatch({
type: SHOW_SEND_ANYWAY_DIALOG,
payload: {
conversationsToPause,
promiseUuid,
source,
},
});
};
}
function hideBlockingSafetyNumberChangeDialog(): HideSendAnywayDialogActiontype {
return {
type: HIDE_SEND_ANYWAY_DIALOG,
};
}
// Reducer
export function getEmptyState(): GlobalModalsStateType {
@ -371,5 +422,24 @@ export function reducer(
};
}
if (action.type === SHOW_SEND_ANYWAY_DIALOG) {
const { promiseUuid, source } = action.payload;
return {
...state,
safetyNumberChangedBlockingData: {
promiseUuid,
source,
},
};
}
if (action.type === HIDE_SEND_ANYWAY_DIALOG) {
return {
...state,
safetyNumberChangedBlockingData: undefined,
};
}
return state;
}

View file

@ -24,7 +24,6 @@ import {
ConversationVerificationState,
} from '../ducks/conversationsEnums';
import { getOwn } from '../../util/getOwn';
import { isNotNil } from '../../util/isNotNil';
import type { UUIDFetchStateType } from '../../util/uuidFetchState';
import { deconstructLookup } from '../../util/deconstructLookup';
import type { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
@ -1022,20 +1021,6 @@ export const getConversationIdsStoppedForVerification = createSelector(
Object.keys(verificationDataByConversation)
);
export const getConversationsStoppedForVerification = createSelector(
getConversationByIdSelector,
getConversationIdsStoppedForVerification,
(
conversationSelector: (id: string) => undefined | ConversationType,
conversationIds: ReadonlyArray<string>
): Array<ConversationType> => {
const conversations = conversationIds
.map(conversationId => conversationSelector(conversationId))
.filter(isNotNil);
return sortByTitle(conversations);
}
);
export const getConversationUuidsStoppingSend = createSelector(
getConversationVerificationData,
(pendingData): Array<string> => {

View file

@ -11,11 +11,9 @@ import { SmartCallManager } from './CallManager';
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
import { SmartGlobalModalContainer } from './GlobalModalContainer';
import { SmartLeftPane } from './LeftPane';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { SmartStories } from './Stories';
import { SmartStoryViewer } from './StoryViewer';
import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getIntl,
getLocaleMessages,
@ -29,10 +27,8 @@ import {
shouldShowStoriesView,
} from '../selectors/stories';
import { getHideMenuBar } from '../selectors/items';
import { getConversationsStoppingSend } from '../selectors/conversations';
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
import { mapDispatchToProps } from '../actions';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { ErrorBoundary } from '../../components/ErrorBoundary';
const mapStateToProps = (state: StateType) => {
@ -40,8 +36,6 @@ const mapStateToProps = (state: StateType) => {
return {
...state.app,
conversationsStoppingSend: getConversationsStoppingSend(state),
getPreferredBadge: getPreferredBadgeSelector(state),
i18n,
localeMessages: getLocaleMessages(state),
isCustomizingPreferredReactions: getIsCustomizingPreferredReactions(state),
@ -56,9 +50,6 @@ const mapStateToProps = (state: StateType) => {
),
renderGlobalModalContainer: () => <SmartGlobalModalContainer />,
renderLeftPane: () => <SmartLeftPane />,
renderSafetyNumber: (props: SafetyNumberProps) => (
<SmartSafetyNumberViewer {...props} />
),
isShowingStoriesView: shouldShowStoriesView(state),
renderStories: () => (
<ErrorBoundary>

View file

@ -3,14 +3,16 @@
import React from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
import type { StateType } from '../reducer';
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
import { SmartContactModal } from './ContactModal';
import { SmartForwardMessageModal } from './ForwardMessageModal';
import { SmartProfileEditorModal } from './ProfileEditorModal';
import { SmartSafetyNumberModal } from './SafetyNumberModal';
import { SmartSendAnywayDialog } from './SendAnywayDialog';
import { SmartStoriesSettingsModal } from './StoriesSettingsModal';
import { getConversationsStoppingSend } from '../selectors/conversations';
import { mapDispatchToProps } from '../actions';
import { getIntl } from '../selectors/user';
@ -30,11 +32,16 @@ function renderStoriesSettings(): JSX.Element {
return <SmartStoriesSettingsModal />;
}
function renderSendAnywayDialog(): JSX.Element {
return <SmartSendAnywayDialog />;
}
const mapStateToProps = (state: StateType) => {
const i18n = getIntl(state);
return {
...state.globalModals,
hasSafetyNumberChangeModal: getConversationsStoppingSend(state).length > 0,
i18n,
renderContactModal,
renderForwardMessageModal,
@ -45,6 +52,7 @@ const mapStateToProps = (state: StateType) => {
contactID={String(state.globalModals.safetyNumberModalContactId)}
/>
),
renderSendAnywayDialog,
};
};

View file

@ -0,0 +1,75 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { useSelector } from 'react-redux';
import type { LocalizerType } from '../../types/Util';
import type { SafetyNumberChangedBlockingDataType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import * as SingleServePromise from '../../services/singleServePromise';
import {
SafetyNumberChangeDialog,
SafetyNumberChangeSource,
} from '../../components/SafetyNumberChangeDialog';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { getConversationsStoppingSend } from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
export function SmartSendAnywayDialog(): JSX.Element {
const { hideBlockingSafetyNumberChangeDialog } = useGlobalModalActions();
const { cancelConversationVerification, verifyConversationsStoppingSend } =
useConversationsActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const theme = useSelector(getTheme);
const contacts = useSelector(getConversationsStoppingSend);
const safetyNumberChangedBlockingData = useSelector<
StateType,
SafetyNumberChangedBlockingDataType | undefined
>(state => state.globalModals.safetyNumberChangedBlockingData);
const explodedPromise = safetyNumberChangedBlockingData
? SingleServePromise.get<boolean>(
safetyNumberChangedBlockingData.promiseUuid
)
: undefined;
let confirmText: string | undefined = i18n(
'safetyNumberChangeDialog__pending-messages'
);
if (safetyNumberChangedBlockingData?.source) {
confirmText =
safetyNumberChangedBlockingData?.source ===
SafetyNumberChangeSource.Calling
? i18n('callAnyway')
: undefined;
}
return (
<SafetyNumberChangeDialog
confirmText={confirmText}
contacts={contacts}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
onCancel={() => {
cancelConversationVerification();
explodedPromise?.resolve(false);
hideBlockingSafetyNumberChangeDialog();
}}
onConfirm={() => {
verifyConversationsStoppingSend();
explodedPromise?.resolve(true);
hideBlockingSafetyNumberChangeDialog();
}}
renderSafetyNumber={({ contactID, onClose }) => (
<SmartSafetyNumberViewer contactID={contactID} onClose={onClose} />
)}
theme={theme}
/>
);
}

View file

@ -28,11 +28,9 @@ import {
getContactNameColorSelector,
getConversationByIdSelector,
getConversationUuidsStoppingSend,
getConversationIdsStoppedForVerification,
getConversationsByTitleSelector,
getConversationSelector,
getConversationsStoppingSend,
getConversationsStoppedForVerification,
getFilteredCandidateContactsForNewGroup,
getFilteredComposeContacts,
getFilteredComposeGroups,
@ -333,49 +331,6 @@ describe('both/state/selectors/conversations', () => {
});
});
describe('#getConversationStoppedForVerification', () => {
it('returns an empty array if there are no conversations stopping send', () => {
const state = getEmptyRootState();
assert.isEmpty(getConversationsStoppingSend(state));
});
it('returns all conversations stopping send', () => {
const convoA = makeConversation('convo a');
const convoB = makeConversation('convo b');
const state: StateType = {
...getEmptyRootState(),
conversations: {
...getEmptyState(),
conversationLookup: {
'convo a': convoA,
'convo b': convoB,
},
verificationDataByConversation: {
'convo a': {
type: ConversationVerificationState.PendingVerification as const,
uuidsNeedingVerification: ['abc'],
},
'convo b': {
type: ConversationVerificationState.PendingVerification as const,
uuidsNeedingVerification: ['def', 'abc'],
},
},
},
};
assert.sameDeepMembers(getConversationIdsStoppedForVerification(state), [
'convo a',
'convo b',
]);
assert.sameDeepMembers(getConversationsStoppedForVerification(state), [
convoA,
convoB,
]);
});
});
describe('#getInvitedContactsForNewlyCreatedGroup', () => {
it('returns an empty array if there are no invited contacts', () => {
const state = getEmptyRootState();

View file

@ -0,0 +1,67 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationModel } from '../models/conversations';
import type { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
import * as log from '../logging/log';
import { explodePromise } from './explodePromise';
import { getConversationIdForLogging } from './idForLogging';
export async function blockSendUntilConversationsAreVerified(
conversations: Array<ConversationModel>,
source?: SafetyNumberChangeSource
): Promise<boolean> {
const conversationsToPause = new Map<string, Set<string>>();
await Promise.all(
conversations.map(async conversation => {
if (!conversation) {
return;
}
const uuidsStoppingSend = new Set<string>();
await conversation.updateVerified();
const unverifieds = conversation.getUnverified();
if (unverifieds.length) {
unverifieds.forEach(unverifiedConversation => {
const uuid = unverifiedConversation.get('uuid');
if (uuid) {
uuidsStoppingSend.add(uuid);
}
});
}
const untrusted = conversation.getUntrusted();
if (untrusted.length) {
untrusted.forEach(untrustedConversation => {
const uuid = untrustedConversation.get('uuid');
if (uuid) {
uuidsStoppingSend.add(uuid);
}
});
}
if (uuidsStoppingSend.size) {
log.info('blockSendUntilConversationsAreVerified: blocking send', {
id: getConversationIdForLogging(conversation.attributes),
untrustedCount: uuidsStoppingSend.size,
});
conversationsToPause.set(conversation.id, uuidsStoppingSend);
}
})
);
if (conversationsToPause.size) {
const explodedPromise = explodePromise<boolean>();
window.reduxActions.globalModals.showBlockingSafetyNumberChangeDialog(
conversationsToPause,
explodedPromise,
source
);
return explodedPromise.promise;
}
return true;
}

View file

@ -1,10 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationModel } from '../models/conversations';
export async function markAllAsApproved(
untrusted: ReadonlyArray<ConversationModel>
): Promise<void> {
await Promise.all(untrusted.map(contact => contact.setApproved()));
}

View file

@ -1,18 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationModel } from '../models/conversations';
export async function markAllAsVerifiedDefault(
unverified: ReadonlyArray<ConversationModel>
): Promise<void> {
await Promise.all(
unverified.map(contact => {
if (contact.isUnverified()) {
return contact.setVerifiedDefault();
}
return null;
})
);
}

View file

@ -2,15 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentType } from '../types/Attachment';
import type { ConversationModel } from '../models/conversations';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import type { MessageAttributesType } from '../model-types.d';
import * as log from '../logging/log';
import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversationsAreVerified';
import { getMessageIdForLogging } from './idForLogging';
import { markAllAsApproved } from './markAllAsApproved';
import { markAllAsVerifiedDefault } from './markAllAsVerifiedDefault';
import { isNotNil } from './isNotNil';
import { resetLinkPreview } from '../services/LinkPreview';
import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog';
export async function maybeForwardMessage(
messageAttributes: MessageAttributesType,
@ -29,9 +28,9 @@ export async function maybeForwardMessage(
});
}
const conversations = conversationIds.map(id =>
window.ConversationController.get(id)
);
const conversations = conversationIds
.map(id => window.ConversationController.get(id))
.filter(isNotNil);
const cannotSend = conversations.some(
conversation =>
@ -42,69 +41,16 @@ export async function maybeForwardMessage(
}
// Verify that all contacts that we're forwarding
// to are verified and trusted
const unverifiedContacts: Array<ConversationModel> = [];
const untrustedContacts: Array<ConversationModel> = [];
await Promise.all(
conversations.map(async conversation => {
if (conversation) {
await conversation.updateVerified();
const unverifieds = conversation.getUnverified();
if (unverifieds.length) {
unverifieds.forEach(unverifiedConversation =>
unverifiedContacts.push(unverifiedConversation)
);
}
const untrusted = conversation.getUntrusted();
if (untrusted.length) {
untrusted.forEach(untrustedConversation =>
untrustedContacts.push(untrustedConversation)
);
}
}
})
);
// to are verified and trusted.
// If there are any unverified or untrusted contacts, show the
// SendAnywayDialog and if we're fine with sending then mark all as
// verified and trusted and continue the send.
const iffyConversations = [...unverifiedContacts, ...untrustedContacts];
if (iffyConversations.length) {
const forwardMessageModal = document.querySelector<HTMLElement>(
'.module-ForwardMessageModal'
);
if (forwardMessageModal) {
forwardMessageModal.style.display = 'none';
}
const sendAnyway = await new Promise(resolve => {
showSafetyNumberChangeDialog({
contacts: iffyConversations,
reject: () => {
resolve(false);
},
resolve: () => {
resolve(true);
},
});
});
if (!sendAnyway) {
if (forwardMessageModal) {
forwardMessageModal.style.display = 'block';
}
return false;
}
let verifyPromise: Promise<void> | undefined;
let approvePromise: Promise<void> | undefined;
if (unverifiedContacts.length) {
verifyPromise = markAllAsVerifiedDefault(unverifiedContacts);
}
if (untrustedContacts.length) {
approvePromise = markAllAsApproved(untrustedContacts);
}
await Promise.all([verifyPromise, approvePromise]);
const canSend = await blockSendUntilConversationsAreVerified(
conversations,
SafetyNumberChangeSource.MessageSend
);
if (!canSend) {
return false;
}
const sendMessageOptions = { dontClearDraft: true };

View file

@ -51,7 +51,6 @@ import { getTheme } from '../state/selectors/user';
import { ReactWrapperView } from './ReactWrapperView';
import type { Lightbox } from '../components/Lightbox';
import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList';
import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog';
import * as log from '../logging/log';
import type { EmbeddedContactType } from '../types/EmbeddedContact';
import { createConversationView } from '../state/roots/createConversationView';
@ -84,8 +83,6 @@ import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpir
import { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge';
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
import { markAllAsApproved } from '../util/markAllAsApproved';
import { markAllAsVerifiedDefault } from '../util/markAllAsVerifiedDefault';
import { retryMessageSend } from '../util/retryMessageSend';
import { isNotNil } from '../util/isNotNil';
import { markViewed } from '../services/MessageUpdater';
@ -114,6 +111,8 @@ import { closeLightbox, showLightbox } from '../util/showLightbox';
import { saveAttachment } from '../util/saveAttachment';
import { sendDeleteForEveryoneMessage } from '../util/sendDeleteForEveryoneMessage';
import { SECOND } from '../util/durations';
import { blockSendUntilConversationsAreVerified } from '../util/blockSendUntilConversationsAreVerified';
import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
type AttachmentOptions = {
messageId: string;
@ -2321,57 +2320,33 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
async isCallSafe(): Promise<boolean> {
const contacts = await this.getUntrustedContacts();
if (contacts.length) {
const callAnyway = await this.showSendAnywayDialog(
contacts,
window.i18n('callAnyway')
const callAnyway = await blockSendUntilConversationsAreVerified(
[this.model],
SafetyNumberChangeSource.Calling
);
if (!callAnyway) {
log.info(
'Safety number change dialog not accepted, new call not allowed.'
);
if (!callAnyway) {
log.info(
'Safety number change dialog not accepted, new call not allowed.'
);
return false;
}
return false;
}
return true;
}
showSendAnywayDialog(
contacts: Array<ConversationModel>,
confirmText?: string
): Promise<boolean> {
return new Promise(resolve => {
showSafetyNumberChangeDialog({
confirmText,
contacts,
reject: () => {
resolve(false);
},
resolve: () => {
resolve(true);
},
});
});
}
async sendStickerMessage(options: {
packId: string;
stickerId: number;
force?: boolean;
}): Promise<void> {
const { model }: { model: ConversationModel } = this;
try {
const contacts = await this.getUntrustedContacts(options);
if (contacts.length) {
const sendAnyway = await this.showSendAnywayDialog(contacts);
if (sendAnyway) {
this.sendStickerMessage({ ...options, force: true });
}
const sendAnyway = await blockSendUntilConversationsAreVerified(
[this.model],
SafetyNumberChangeSource.MessageSend
);
if (!sendAnyway) {
return;
}
@ -2386,40 +2361,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
}
async getUntrustedContacts(
options: { force?: boolean } = {}
): Promise<Array<ConversationModel>> {
const { model }: { model: ConversationModel } = this;
// This will go to the trust store for the latest identity key information,
// and may result in the display of a new banner for this conversation.
await model.updateVerified();
const unverifiedContacts = model.getUnverified();
if (options.force) {
if (unverifiedContacts.length) {
await markAllAsVerifiedDefault(unverifiedContacts);
// We only want force to break us through one layer of checks
// eslint-disable-next-line no-param-reassign
options.force = false;
}
} else if (unverifiedContacts.length) {
return unverifiedContacts;
}
const untrustedContacts = model.getUntrusted();
if (options.force) {
if (untrustedContacts.length) {
await markAllAsApproved(untrustedContacts);
}
} else if (untrustedContacts.length) {
return untrustedContacts;
}
return [];
}
async setQuoteMessage(messageId: null | string): Promise<void> {
const { model } = this;
const message = messageId ? await getMessageById(messageId) : undefined;
@ -2546,7 +2487,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
mentions: BodyRangesType = [],
options: {
draftAttachments?: ReadonlyArray<AttachmentType>;
force?: boolean;
timestamp?: number;
voiceNoteAttachment?: AttachmentType;
} = {}
@ -2558,15 +2498,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
try {
this.disableMessageField();
const contacts = await this.getUntrustedContacts(options);
if (contacts.length) {
const sendAnyway = await this.showSendAnywayDialog(contacts);
if (sendAnyway) {
this.sendMessage(message, mentions, { force: true, timestamp });
return;
}
const sendAnyway = await blockSendUntilConversationsAreVerified(
[this.model],
SafetyNumberChangeSource.MessageSend
);
if (!sendAnyway) {
this.enableMessageField();
return;
}