Update safety number change warning dialog

This commit is contained in:
Josh Perez 2020-06-25 20:08:58 -04:00 committed by Scott Nonnenberg
parent e87a0103cc
commit 5b83485c89
38 changed files with 1221 additions and 425 deletions

View file

@ -4,6 +4,7 @@ import { actions as emojis } from './ducks/emojis';
import { actions as expiration } from './ducks/expiration';
import { actions as items } from './ducks/items';
import { actions as network } from './ducks/network';
import { actions as safetyNumber } from './ducks/safetyNumber';
import { actions as search } from './ducks/search';
import { actions as stickers } from './ducks/stickers';
import { actions as updates } from './ducks/updates';
@ -16,6 +17,7 @@ export const mapDispatchToProps = {
...expiration,
...items,
...network,
...safetyNumber,
...search,
...stickers,
...updates,

View file

@ -24,12 +24,15 @@ export type DBConversationType = {
};
export type ConversationType = {
id: string;
uuid?: string;
e164: string;
name?: string;
profileName?: string;
avatarPath?: string;
color?: ColorType;
isArchived?: boolean;
isBlocked?: boolean;
isVerified?: boolean;
activeAt?: number;
timestamp: number;
inboxPosition: number;
@ -42,6 +45,7 @@ export type ConversationType = {
type: 'direct' | 'group';
isMe: boolean;
lastUpdated: number;
title: string;
unreadCount: number;
isSelected: boolean;
typingContact?: {

View file

@ -0,0 +1,214 @@
import { generateSecurityNumberBlock } from '../../util/safetyNumber';
import { ConversationType } from './conversations';
import {
reloadProfiles,
toggleVerification,
} from '../../shims/contactVerification';
export type SafetyNumberContactType = {
safetyNumber: string;
safetyNumberChanged?: boolean;
verificationDisabled: boolean;
};
export type SafetyNumberStateType = {
contacts: {
[key: string]: SafetyNumberContactType;
};
};
const GENERATE = 'safetyNumber/GENERATE';
const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED';
const TOGGLE_VERIFIED = 'safetyNumber/TOGGLE_VERIFIED';
const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING';
type GenerateAsyncActionType = {
contact: ConversationType;
safetyNumber: string;
};
type GenerateActionType = {
type: 'safetyNumber/GENERATE';
payload: Promise<GenerateAsyncActionType>;
};
type GenerateFulfilledActionType = {
type: 'safetyNumber/GENERATE_FULFILLED';
payload: GenerateAsyncActionType;
};
type ToggleVerifiedAsyncActionType = {
contact: ConversationType;
safetyNumber?: string;
safetyNumberChanged?: boolean;
};
type ToggleVerifiedActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED';
payload: {
data: { contact: ConversationType };
promise: Promise<ToggleVerifiedAsyncActionType>;
};
};
type ToggleVerifiedPendingActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED_PENDING';
payload: ToggleVerifiedAsyncActionType;
};
type ToggleVerifiedFulfilledActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
payload: ToggleVerifiedAsyncActionType;
};
export type SafetyNumberActionTypes =
| GenerateActionType
| GenerateFulfilledActionType
| ToggleVerifiedActionType
| ToggleVerifiedPendingActionType
| ToggleVerifiedFulfilledActionType;
function generate(contact: ConversationType): GenerateActionType {
return {
type: GENERATE,
payload: doGenerate(contact),
};
}
async function doGenerate(
contact: ConversationType
): Promise<GenerateAsyncActionType> {
const securityNumberBlock = await generateSecurityNumberBlock(contact);
return {
contact,
safetyNumber: securityNumberBlock.join(' '),
};
}
function toggleVerified(contact: ConversationType): ToggleVerifiedActionType {
return {
type: TOGGLE_VERIFIED,
payload: {
data: { contact },
promise: doToggleVerified(contact),
},
};
}
async function alterVerification(contact: ConversationType): Promise<void> {
try {
await toggleVerification(contact.id);
} catch (result) {
if (result instanceof Error) {
if (result.name === 'OutgoingIdentityKeyError') {
throw result;
} else {
window.log.error(
'failed to toggle verified:',
result && result.stack ? result.stack : result
);
}
} else {
const keyError = result.errors.find(
(error: Error) => error.name === 'OutgoingIdentityKeyError'
);
if (keyError) {
throw keyError;
} else {
result.errors.forEach((error: Error) => {
window.log.error(
'failed to toggle verified:',
error && error.stack ? error.stack : error
);
});
}
}
}
}
async function doToggleVerified(
contact: ConversationType
): Promise<ToggleVerifiedAsyncActionType> {
try {
await alterVerification(contact);
} catch (err) {
if (err.name === 'OutgoingIdentityKeyError') {
await reloadProfiles(contact.id);
const securityNumberBlock = await generateSecurityNumberBlock(contact);
return {
contact,
safetyNumber: securityNumberBlock.join(' '),
safetyNumberChanged: true,
};
}
}
return { contact };
}
export const actions = {
generateSafetyNumber: generate,
toggleVerified,
};
function getEmptyState(): SafetyNumberStateType {
return {
contacts: {},
};
}
export function reducer(
state: SafetyNumberStateType = getEmptyState(),
action: SafetyNumberActionTypes
): SafetyNumberStateType {
if (action.type === TOGGLE_VERIFIED_PENDING) {
const { contact } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
safetyNumberChanged: false,
verificationDisabled: true,
},
},
};
}
if (action.type === TOGGLE_VERIFIED_FULFILLED) {
const { contact, ...restProps } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
...restProps,
verificationDisabled: false,
},
},
};
}
if (action.type === GENERATE_FULFILLED) {
const { contact, safetyNumber } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
safetyNumber,
},
},
};
}
return state;
}

View file

@ -30,6 +30,11 @@ import {
NetworkStateType,
reducer as network,
} from './ducks/network';
import {
reducer as safetyNumber,
SafetyNumberActionTypes,
SafetyNumberStateType,
} from './ducks/safetyNumber';
import {
reducer as search,
SEARCH_TYPES as SearchActionType,
@ -54,6 +59,7 @@ export type StateType = {
expiration: ExpirationStateType;
items: ItemsStateType;
network: NetworkStateType;
safetyNumber: SafetyNumberStateType;
search: SearchStateType;
stickers: StickersStateType;
updates: UpdatesStateType;
@ -67,6 +73,7 @@ export type ActionsType =
| ConversationActionType
| ItemsActionType
| NetworkActionType
| SafetyNumberActionTypes
| StickersActionType
| SearchActionType
| UpdatesActionType;
@ -78,6 +85,7 @@ export const reducers = {
expiration,
items,
network,
safetyNumber,
search,
stickers,
updates,

View file

@ -0,0 +1,21 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import { SmartSafetyNumberViewer } from '../smart/SafetyNumberViewer';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
const FilteredSafetyNumberViewer = SmartSafetyNumberViewer as any;
type Props = {
contactID: string;
onClose?: () => void;
};
export const createSafetyNumberViewer = (store: Store, props: Props) => (
<Provider store={store}>
<FilteredSafetyNumberViewer {...props} />
</Provider>
);

View file

@ -0,0 +1,24 @@
import { createSelector } from 'reselect';
import { StateType } from '../reducer';
import {
SafetyNumberContactType,
SafetyNumberStateType,
} from '../ducks/safetyNumber';
const getSafetyNumber = (state: StateType): SafetyNumberStateType =>
state.safetyNumber;
type Props = {
contactID: string;
};
const getContactID = (_: StateType, props: Props): string => props.contactID;
export const getContactSafetyNumber = createSelector(
[getSafetyNumber, getContactID],
(
{ contacts }: SafetyNumberStateType,
contactID: string
): SafetyNumberContactType => contacts[contactID]
);

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { SafetyNumberViewer } from '../../components/SafetyNumberViewer';
import { StateType } from '../reducer';
import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
type Props = {
contactID: string;
onClose?: () => void;
};
const mapStateToProps = (state: StateType, props: Props) => {
return {
...props,
...getContactSafetyNumber(state, props),
contact: getConversationSelector(state)(props.contactID),
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartSafetyNumberViewer = smart(SafetyNumberViewer);