signal-desktop/ts/state/ducks/safetyNumber.ts
Jamie Kyle 27b55e472d
Refactor smart components
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
2024-03-13 13:44:13 -07:00

248 lines
6 KiB
TypeScript

// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
import type { ThunkAction } from 'redux-thunk';
import { omit } from 'lodash';
import { generateSafetyNumber } from '../../util/safetyNumber';
import type { SafetyNumberType } from '../../types/safetyNumber';
import type { ConversationType } from './conversations';
import {
reloadProfiles,
toggleVerification,
} from '../../shims/contactVerification';
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
import type { StateType as RootStateType } from '../reducer';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
export type SafetyNumberContactType = ReadonlyDeep<{
safetyNumber: SafetyNumberType;
safetyNumberChanged?: boolean;
verificationDisabled: boolean;
}>;
export type SafetyNumberStateType = ReadonlyDeep<{
contacts: {
[key: string]: SafetyNumberContactType;
};
}>;
const CLEAR_SAFETY_NUMBER = 'safetyNumber/CLEAR_SAFETY_NUMBER';
const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED';
const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING';
type ClearSafetyNumberActionType = ReadonlyDeep<{
type: 'safetyNumber/CLEAR_SAFETY_NUMBER';
payload: {
contactId: string;
};
}>;
type GenerateFulfilledActionType = ReadonlyDeep<{
type: 'safetyNumber/GENERATE_FULFILLED';
payload: {
contact: ConversationType;
safetyNumber: SafetyNumberType;
};
}>;
type ToggleVerifiedPendingActionType = ReadonlyDeep<{
type: 'safetyNumber/TOGGLE_VERIFIED_PENDING';
payload: {
contact: ConversationType;
};
}>;
type ToggleVerifiedFulfilledActionType = ReadonlyDeep<{
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
payload: {
contact: ConversationType;
safetyNumber?: SafetyNumberType;
safetyNumberChanged?: boolean;
};
}>;
export type SafetyNumberActionType = ReadonlyDeep<
| ClearSafetyNumberActionType
| GenerateFulfilledActionType
| ToggleVerifiedPendingActionType
| ToggleVerifiedFulfilledActionType
>;
function clearSafetyNumber(contactId: string): ClearSafetyNumberActionType {
return {
type: CLEAR_SAFETY_NUMBER,
payload: { contactId },
};
}
function generate(
contact: ConversationType
): ThunkAction<void, RootStateType, unknown, GenerateFulfilledActionType> {
return async dispatch => {
try {
const safetyNumber = await generateSafetyNumber(contact);
dispatch({
type: GENERATE_FULFILLED,
payload: {
contact,
safetyNumber,
},
});
} catch (error) {
log.error(
'failed to generate security number:',
Errors.toLogFormat(error)
);
}
};
}
function toggleVerified(
contact: ConversationType
): ThunkAction<
void,
RootStateType,
unknown,
ToggleVerifiedPendingActionType | ToggleVerifiedFulfilledActionType
> {
return async dispatch => {
dispatch({
type: TOGGLE_VERIFIED_PENDING,
payload: {
contact,
},
});
try {
await alterVerification(contact);
dispatch({
type: TOGGLE_VERIFIED_FULFILLED,
payload: {
contact,
},
});
} catch (err) {
if (err.name === 'OutgoingIdentityKeyError') {
await reloadProfiles(contact.id);
const safetyNumber = await generateSafetyNumber(contact);
dispatch({
type: TOGGLE_VERIFIED_FULFILLED,
payload: {
contact,
safetyNumber,
safetyNumberChanged: true,
},
});
}
}
};
}
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 {
log.error('failed to toggle verified:', Errors.toLogFormat(result));
}
} else {
const keyError = result.errors.find(
(error: Error) => error.name === 'OutgoingIdentityKeyError'
);
if (keyError) {
throw keyError;
} else {
result.errors.forEach((error: Error) => {
log.error('failed to toggle verified:', Errors.toLogFormat(error));
});
}
}
}
}
export const actions = {
clearSafetyNumber,
generateSafetyNumber: generate,
toggleVerified,
};
export const useSafetyNumberActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
export function getEmptyState(): SafetyNumberStateType {
return {
contacts: {},
};
}
export function reducer(
state: Readonly<SafetyNumberStateType> = getEmptyState(),
action: Readonly<SafetyNumberActionType>
): SafetyNumberStateType {
if (action.type === CLEAR_SAFETY_NUMBER) {
const { contactId } = action.payload;
return {
contacts: omit(state.contacts, contactId),
};
}
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;
}