Update safety number change warning dialog
This commit is contained in:
parent
e87a0103cc
commit
5b83485c89
38 changed files with 1221 additions and 425 deletions
|
@ -1,4 +1,3 @@
|
|||
// tslint:disable: no-backbone-get-set-outside-model
|
||||
import { get, throttle } from 'lodash';
|
||||
import { WebAPIType } from './textsecure/WebAPI';
|
||||
|
||||
|
|
|
@ -70,33 +70,35 @@ export const ConfirmationDialog = React.memo(
|
|||
<div className="module-confirmation-dialog__container__content">
|
||||
{children}
|
||||
</div>
|
||||
<div className="module-confirmation-dialog__container__buttons">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
ref={focusRef}
|
||||
className="module-confirmation-dialog__container__buttons__button"
|
||||
>
|
||||
{i18n('confirmation-dialog--Cancel')}
|
||||
</button>
|
||||
{actions.map((action, i) => (
|
||||
{actions.length > 0 && (
|
||||
<div className="module-confirmation-dialog__container__buttons">
|
||||
<button
|
||||
key={i}
|
||||
onClick={handleAction}
|
||||
data-action={i}
|
||||
className={classNames(
|
||||
'module-confirmation-dialog__container__buttons__button',
|
||||
action.style === 'affirmative'
|
||||
? 'module-confirmation-dialog__container__buttons__button--affirmative'
|
||||
: null,
|
||||
action.style === 'negative'
|
||||
? 'module-confirmation-dialog__container__buttons__button--negative'
|
||||
: null
|
||||
)}
|
||||
onClick={handleCancel}
|
||||
ref={focusRef}
|
||||
className="module-confirmation-dialog__container__buttons__button"
|
||||
>
|
||||
{action.text}
|
||||
{i18n('confirmation-dialog--Cancel')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{actions.map((action, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={handleAction}
|
||||
data-action={i}
|
||||
className={classNames(
|
||||
'module-confirmation-dialog__container__buttons__button',
|
||||
action.style === 'affirmative'
|
||||
? 'module-confirmation-dialog__container__buttons__button--affirmative'
|
||||
: null,
|
||||
action.style === 'negative'
|
||||
? 'module-confirmation-dialog__container__buttons__button--negative'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{action.text}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
76
ts/components/SafetyNumberChangeDialog.stories.tsx
Normal file
76
ts/components/SafetyNumberChangeDialog.stories.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import * as React from 'react';
|
||||
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const contact = {
|
||||
avatarPath: undefined,
|
||||
color: 'signal-blue',
|
||||
profileName: undefined,
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
} as ConversationType;
|
||||
|
||||
storiesOf('Components/SafetyNumberChangeDialog', module)
|
||||
.add('Single Contact Dialog', () => {
|
||||
return (
|
||||
<SafetyNumberChangeDialog
|
||||
contacts={[contact]}
|
||||
i18n={i18n}
|
||||
onCancel={action('cancel')}
|
||||
onConfirm={action('confirm')}
|
||||
renderSafetyNumber={() => {
|
||||
action('renderSafetyNumber');
|
||||
return <div>This is a mock Safety Number View</div>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Multi Contact Dialog', () => {
|
||||
return (
|
||||
<SafetyNumberChangeDialog
|
||||
contacts={[contact, contact, contact, contact]}
|
||||
i18n={i18n}
|
||||
onCancel={action('cancel')}
|
||||
onConfirm={action('confirm')}
|
||||
renderSafetyNumber={() => {
|
||||
action('renderSafetyNumber');
|
||||
return <div>This is a mock Safety Number View</div>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Scroll Dialog', () => {
|
||||
return (
|
||||
<SafetyNumberChangeDialog
|
||||
contacts={[
|
||||
contact,
|
||||
contact,
|
||||
contact,
|
||||
contact,
|
||||
contact,
|
||||
contact,
|
||||
contact,
|
||||
contact,
|
||||
contact,
|
||||
contact,
|
||||
]}
|
||||
i18n={i18n}
|
||||
onCancel={action('cancel')}
|
||||
onConfirm={action('confirm')}
|
||||
renderSafetyNumber={() => {
|
||||
action('renderSafetyNumber');
|
||||
return <div>This is a mock Safety Number View</div>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
135
ts/components/SafetyNumberChangeDialog.tsx
Normal file
135
ts/components/SafetyNumberChangeDialog.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
import * as React from 'react';
|
||||
import { Avatar } from './Avatar';
|
||||
import { ConfirmationModal } from './ConfirmationModal';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
type SafetyNumberProps = {
|
||||
contactID: string;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
readonly contacts: Array<ConversationType>;
|
||||
readonly i18n: LocalizerType;
|
||||
readonly onCancel: () => void;
|
||||
readonly onConfirm: () => void;
|
||||
readonly renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element;
|
||||
};
|
||||
|
||||
type SafetyDialogContentProps = Props & {
|
||||
readonly onView: (contact: ConversationType) => void;
|
||||
};
|
||||
|
||||
const SafetyDialogContents = ({
|
||||
contacts,
|
||||
i18n,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
onView,
|
||||
}: SafetyDialogContentProps): JSX.Element => {
|
||||
const cancelButtonRef = React.createRef<HTMLButtonElement>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (cancelButtonRef && cancelButtonRef.current) {
|
||||
cancelButtonRef.current.focus();
|
||||
}
|
||||
}, [contacts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="module-sfn-dialog__title">
|
||||
{i18n('safetyNumberChanges')}
|
||||
</h1>
|
||||
<div className="module-sfn-dialog__message">
|
||||
{i18n('changedVerificationWarning')}
|
||||
</div>
|
||||
<ul className="module-sfn-dialog__contacts">
|
||||
{contacts.map((contact: ConversationType) => (
|
||||
<li className="module-sfn-dialog__contact" key={contact.phoneNumber}>
|
||||
<Avatar
|
||||
avatarPath={contact.avatarPath}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={contact.name}
|
||||
phoneNumber={contact.phoneNumber}
|
||||
profileName={contact.profileName}
|
||||
size={52}
|
||||
/>
|
||||
<div className="module-sfn-dialog__contact--wrapper">
|
||||
{contact.name && (
|
||||
<>
|
||||
<div className="module-sfn-dialog__contact--name">
|
||||
{contact.name}
|
||||
</div>
|
||||
<div className="module-sfn-dialog__contact--number">
|
||||
{contact.phoneNumber}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!contact.name && (
|
||||
<div className="module-sfn-dialog__contact--name">
|
||||
{contact.phoneNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="module-sfn-dialog__contact--view"
|
||||
onClick={() => {
|
||||
onView(contact);
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
{i18n('view')}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="module-sfn-dialog__actions">
|
||||
<button
|
||||
className="module-sfn-dialog__actions--cancel"
|
||||
onClick={onCancel}
|
||||
ref={cancelButtonRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
{i18n('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="module-sfn-dialog__actions--confirm"
|
||||
onClick={onConfirm}
|
||||
tabIndex={0}
|
||||
>
|
||||
{i18n('sendMessageToContact')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SafetyNumberChangeDialog = (props: Props): JSX.Element => {
|
||||
const { i18n, onCancel, renderSafetyNumber } = props;
|
||||
const [contact, setViewSafetyNumber] = React.useState<
|
||||
ConversationType | undefined
|
||||
>(undefined);
|
||||
|
||||
const onClose = contact
|
||||
? () => {
|
||||
setViewSafetyNumber(undefined);
|
||||
}
|
||||
: onCancel;
|
||||
|
||||
return (
|
||||
<ConfirmationModal actions={[]} i18n={i18n} onClose={onClose}>
|
||||
{contact && renderSafetyNumber({ contactID: contact.id, onClose })}
|
||||
{!contact && (
|
||||
<SafetyDialogContents
|
||||
{...props}
|
||||
onView={selectedContact => {
|
||||
setViewSafetyNumber(selectedContact);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
};
|
85
ts/components/SafetyNumberViewer.stories.tsx
Normal file
85
ts/components/SafetyNumberViewer.stories.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import * as React from 'react';
|
||||
import { SafetyNumberViewer } from './SafetyNumberViewer';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean, text } from '@storybook/addon-knobs';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
contact: {
|
||||
title: 'Summer Smith',
|
||||
isVerified: true,
|
||||
} as ConversationType,
|
||||
generateSafetyNumber: action('generate-safety-number'),
|
||||
i18n,
|
||||
safetyNumber: 'XXX',
|
||||
safetyNumberChanged: false,
|
||||
toggleVerified: action('toggle-verified'),
|
||||
verificationDisabled: false,
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Safety Number',
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Safety Number (not verified)',
|
||||
props: {
|
||||
contact: {
|
||||
title: 'Morty Smith',
|
||||
isVerified: false,
|
||||
} as ConversationType,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Verification Disabled',
|
||||
props: {
|
||||
verificationDisabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Safety Number Changed',
|
||||
props: {
|
||||
safetyNumberChanged: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Safety Number (dialog close)',
|
||||
props: {
|
||||
onClose: action('close'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
storiesOf('Components/SafetyNumberViewer', module)
|
||||
.add('Knobs Playground', () => {
|
||||
const safetyNumber = text('safetyNumber', 'XXX');
|
||||
const safetyNumberChanged = boolean('safetyNumberChanged', false);
|
||||
const verificationDisabled = boolean('verificationDisabled', false);
|
||||
|
||||
return (
|
||||
<SafetyNumberViewer
|
||||
{...defaultProps}
|
||||
safetyNumber={safetyNumber}
|
||||
safetyNumberChanged={safetyNumberChanged}
|
||||
verificationDisabled={verificationDisabled}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<SafetyNumberViewer {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
82
ts/components/SafetyNumberViewer.tsx
Normal file
82
ts/components/SafetyNumberViewer.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { getPlaceholder } from '../util/safetyNumber';
|
||||
|
||||
type SafetyNumberViewerProps = {
|
||||
contact?: ConversationType;
|
||||
generateSafetyNumber: (contact: ConversationType) => void;
|
||||
i18n: LocalizerType;
|
||||
onClose?: () => void;
|
||||
safetyNumber: string;
|
||||
safetyNumberChanged?: boolean;
|
||||
toggleVerified: (contact: ConversationType) => void;
|
||||
verificationDisabled: boolean;
|
||||
};
|
||||
|
||||
export const SafetyNumberViewer = ({
|
||||
contact,
|
||||
generateSafetyNumber,
|
||||
i18n,
|
||||
onClose,
|
||||
safetyNumber,
|
||||
safetyNumberChanged,
|
||||
toggleVerified,
|
||||
verificationDisabled,
|
||||
}: SafetyNumberViewerProps): JSX.Element | null => {
|
||||
if (!contact) {
|
||||
return null;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
generateSafetyNumber(contact);
|
||||
}, [safetyNumber]);
|
||||
|
||||
const name = contact.title;
|
||||
const isVerified = contact.isVerified;
|
||||
const verifiedStatus = isVerified
|
||||
? i18n('isVerified', [name])
|
||||
: i18n('isNotVerified', [name]);
|
||||
const verifyButtonText = isVerified ? i18n('unverify') : i18n('verify');
|
||||
|
||||
return (
|
||||
<div className="module-safety-number">
|
||||
{onClose && (
|
||||
<div className="module-safety-number__close-button">
|
||||
<button onClick={onClose} tabIndex={0}>
|
||||
<span />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="module-safety-number__verification-label">
|
||||
{safetyNumberChanged
|
||||
? i18n('changedRightAfterVerify', [name, name])
|
||||
: i18n('yourSafetyNumberWith', [name])}
|
||||
</div>
|
||||
<div className="module-safety-number__number">
|
||||
{safetyNumber || getPlaceholder()}
|
||||
</div>
|
||||
{i18n('verifyHelp', [name])}
|
||||
<div className="module-safety-number__verification-status">
|
||||
{isVerified ? (
|
||||
<span className="module-safety-number__icon--verified" />
|
||||
) : (
|
||||
<span className="module-safety-number__icon--shield" />
|
||||
)}
|
||||
{verifiedStatus}
|
||||
</div>
|
||||
<div className="module-safety-number__verify-container">
|
||||
<button
|
||||
className="module-safety-number__button--verify"
|
||||
disabled={verificationDisabled}
|
||||
onClick={() => {
|
||||
toggleVerified(contact);
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
{verifyButtonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -37,7 +37,6 @@ const SENT = 'sent' as 'sent';
|
|||
const START_NEW_CONVERSATION = 'start-new-conversation' as 'start-new-conversation';
|
||||
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as 'sms-mms-not-supported-text';
|
||||
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
messageLookup.set('1-guid-guid-guid-guid-guid', {
|
||||
id: '1-guid-guid-guid-guid-guid',
|
||||
conversationId: '(202) 555-0015',
|
||||
|
@ -56,7 +55,6 @@ messageLookup.set('1-guid-guid-guid-guid-guid', {
|
|||
},
|
||||
});
|
||||
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
messageLookup.set('2-guid-guid-guid-guid-guid', {
|
||||
id: '2-guid-guid-guid-guid-guid',
|
||||
conversationId: '(202) 555-0016',
|
||||
|
@ -73,7 +71,6 @@ messageLookup.set('2-guid-guid-guid-guid-guid', {
|
|||
},
|
||||
});
|
||||
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
messageLookup.set('3-guid-guid-guid-guid-guid', {
|
||||
id: '3-guid-guid-guid-guid-guid',
|
||||
conversationId: 'EveryoneGroupID',
|
||||
|
@ -91,7 +88,6 @@ messageLookup.set('3-guid-guid-guid-guid-guid', {
|
|||
},
|
||||
});
|
||||
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
messageLookup.set('4-guid-guid-guid-guid-guid', {
|
||||
id: '4-guid-guid-guid-guid-guid',
|
||||
conversationId: 'EveryoneGroupID',
|
||||
|
|
13
ts/shims/contactVerification.ts
Normal file
13
ts/shims/contactVerification.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export async function toggleVerification(id: string): Promise<void> {
|
||||
const contact = window.getConversations().get(id);
|
||||
if (contact) {
|
||||
await contact.toggleVerified();
|
||||
}
|
||||
}
|
||||
|
||||
export async function reloadProfiles(id: string): Promise<void> {
|
||||
const contact = window.getConversations().get(id);
|
||||
if (contact) {
|
||||
await contact.getProfiles();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// tslint:disable no-backbone-get-set-outside-model no-console no-default-export no-unnecessary-local-variable
|
||||
// tslint:disable no-console no-default-export no-unnecessary-local-variable
|
||||
|
||||
import { join } from 'path';
|
||||
import mkdirp from 'mkdirp';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?: {
|
||||
|
|
214
ts/state/ducks/safetyNumber.ts
Normal file
214
ts/state/ducks/safetyNumber.ts
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
21
ts/state/roots/createSafetyNumberViewer.tsx
Normal file
21
ts/state/roots/createSafetyNumberViewer.tsx
Normal 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>
|
||||
);
|
24
ts/state/selectors/safetyNumber.ts
Normal file
24
ts/state/selectors/safetyNumber.ts
Normal 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]
|
||||
);
|
25
ts/state/smart/SafetyNumberViewer.tsx
Normal file
25
ts/state/smart/SafetyNumberViewer.tsx
Normal 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);
|
|
@ -14,6 +14,7 @@ describe('state/selectors/conversations', () => {
|
|||
const data: ConversationLookupType = {
|
||||
id1: {
|
||||
id: 'id1',
|
||||
e164: '+18005551111',
|
||||
activeAt: Date.now(),
|
||||
name: 'No timestamp',
|
||||
timestamp: 0,
|
||||
|
@ -24,6 +25,7 @@ describe('state/selectors/conversations', () => {
|
|||
type: 'direct',
|
||||
isMe: false,
|
||||
lastUpdated: Date.now(),
|
||||
title: 'No timestamp',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContact: {
|
||||
|
@ -36,6 +38,7 @@ describe('state/selectors/conversations', () => {
|
|||
},
|
||||
id2: {
|
||||
id: 'id2',
|
||||
e164: '+18005551111',
|
||||
activeAt: Date.now(),
|
||||
name: 'B',
|
||||
timestamp: 20,
|
||||
|
@ -46,6 +49,7 @@ describe('state/selectors/conversations', () => {
|
|||
type: 'direct',
|
||||
isMe: false,
|
||||
lastUpdated: Date.now(),
|
||||
title: 'B',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContact: {
|
||||
|
@ -58,6 +62,7 @@ describe('state/selectors/conversations', () => {
|
|||
},
|
||||
id3: {
|
||||
id: 'id3',
|
||||
e164: '+18005551111',
|
||||
activeAt: Date.now(),
|
||||
name: 'C',
|
||||
timestamp: 20,
|
||||
|
@ -68,6 +73,7 @@ describe('state/selectors/conversations', () => {
|
|||
type: 'direct',
|
||||
isMe: false,
|
||||
lastUpdated: Date.now(),
|
||||
title: 'C',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContact: {
|
||||
|
@ -80,6 +86,7 @@ describe('state/selectors/conversations', () => {
|
|||
},
|
||||
id4: {
|
||||
id: 'id4',
|
||||
e164: '+18005551111',
|
||||
activeAt: Date.now(),
|
||||
name: 'Á',
|
||||
timestamp: 20,
|
||||
|
@ -90,6 +97,7 @@ describe('state/selectors/conversations', () => {
|
|||
type: 'direct',
|
||||
isMe: false,
|
||||
lastUpdated: Date.now(),
|
||||
title: 'A',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContact: {
|
||||
|
@ -102,6 +110,7 @@ describe('state/selectors/conversations', () => {
|
|||
},
|
||||
id5: {
|
||||
id: 'id5',
|
||||
e164: '+18005551111',
|
||||
activeAt: Date.now(),
|
||||
name: 'First!',
|
||||
timestamp: 30,
|
||||
|
@ -112,6 +121,7 @@ describe('state/selectors/conversations', () => {
|
|||
type: 'direct',
|
||||
isMe: false,
|
||||
lastUpdated: Date.now(),
|
||||
title: 'First!',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContact: {
|
||||
|
|
17
ts/textsecure.d.ts
vendored
17
ts/textsecure.d.ts
vendored
|
@ -96,6 +96,14 @@ type StoredSignedPreKeyType = SignedPreKeyType & {
|
|||
created_at: number;
|
||||
};
|
||||
|
||||
type IdentityKeyRecord = {
|
||||
publicKey: ArrayBuffer;
|
||||
firstUse: boolean;
|
||||
timestamp: number;
|
||||
verified: number;
|
||||
nonblockingApproval: boolean;
|
||||
};
|
||||
|
||||
export type StorageProtocolType = StorageType & {
|
||||
VerifiedStatus: {
|
||||
DEFAULT: number;
|
||||
|
@ -105,6 +113,7 @@ export type StorageProtocolType = StorageType & {
|
|||
archiveSiblingSessions: (identifier: string) => Promise<void>;
|
||||
removeSession: (identifier: string) => Promise<void>;
|
||||
getDeviceIds: (identifier: string) => Promise<Array<number>>;
|
||||
getIdentityRecord: (identifier: string) => IdentityKeyRecord | undefined;
|
||||
hydrateCaches: () => Promise<void>;
|
||||
clearPreKeyStore: () => Promise<void>;
|
||||
clearSignedPreKeysStore: () => Promise<void>;
|
||||
|
@ -119,13 +128,7 @@ export type StorageProtocolType = StorageType & {
|
|||
loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
|
||||
saveIdentityWithAttributes: (
|
||||
number: string,
|
||||
options: {
|
||||
publicKey: ArrayBuffer;
|
||||
firstUse: boolean;
|
||||
timestamp: number;
|
||||
verified: number;
|
||||
nonblockingApproval: boolean;
|
||||
}
|
||||
options: IdentityKeyRecord
|
||||
) => Promise<void>;
|
||||
removeSignedPreKey: (keyId: number) => Promise<void>;
|
||||
removeAllData: () => Promise<void>;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// tslint:disable no-backbone-get-set-outside-model no-default-export no-unnecessary-local-variable
|
||||
// tslint:disable no-default-export no-unnecessary-local-variable
|
||||
|
||||
import EventTarget from './EventTarget';
|
||||
import { WebAPIType } from './WebAPI';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// tslint:disable no-backbone-get-set-outside-model no-bitwise no-default-export
|
||||
// tslint:disable no-bitwise no-default-export
|
||||
|
||||
import { without } from 'lodash';
|
||||
import PQueue from 'p-queue';
|
||||
|
|
|
@ -189,7 +189,6 @@ const agents: AgentCacheType = {};
|
|||
|
||||
function getContentType(response: Response) {
|
||||
if (response.headers && response.headers.get) {
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
return response.headers.get('content-type');
|
||||
}
|
||||
|
||||
|
@ -310,7 +309,6 @@ async function _promiseAjax(
|
|||
let resultPromise;
|
||||
if (
|
||||
options.responseType === 'json' &&
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
response.headers.get('Content-Type') === 'application/json'
|
||||
) {
|
||||
resultPromise = response.json();
|
||||
|
@ -1464,7 +1462,6 @@ export function initialize({
|
|||
throw new Error('makeProxiedRequest: Problem retrieving header value');
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
const range = response.headers.get('content-range');
|
||||
const match = PARSE_RANGE_HEADER.exec(range);
|
||||
|
||||
|
|
|
@ -6,6 +6,10 @@ import { createBatcher } from './batcher';
|
|||
import { createWaitBatcher } from './waitBatcher';
|
||||
import { deleteForEveryone } from './deleteForEveryone';
|
||||
import { downloadAttachment } from './downloadAttachment';
|
||||
import {
|
||||
generateSecurityNumber,
|
||||
getPlaceholder as getSafetyNumberPlaceholder,
|
||||
} from './safetyNumber';
|
||||
import { hasExpired } from './hasExpired';
|
||||
import { isFileDangerous } from './isFileDangerous';
|
||||
import { makeLookup } from './makeLookup';
|
||||
|
@ -20,6 +24,8 @@ export {
|
|||
createWaitBatcher,
|
||||
deleteForEveryone,
|
||||
downloadAttachment,
|
||||
generateSecurityNumber,
|
||||
getSafetyNumberPlaceholder,
|
||||
GoogleChrome,
|
||||
hasExpired,
|
||||
isFileDangerous,
|
||||
|
|
|
@ -223,7 +223,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/models/conversations.js",
|
||||
"line": " await wrap(",
|
||||
"lineNumber": 641,
|
||||
"lineNumber": 644,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-06-09T20:26:46.515Z"
|
||||
},
|
||||
|
@ -710,46 +710,18 @@
|
|||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/key_verification_view.js",
|
||||
"line": " new QRCode(this.$('.qr')[0]).makeCode(",
|
||||
"lineNumber": 43,
|
||||
"line": " this.$('.key-verification-wrapper').append(view.el);",
|
||||
"lineNumber": 23,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-02-14T20:02:37.507Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
"updated": "2020-06-23T06:48:06.829Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/key_verification_view.js",
|
||||
"line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')",
|
||||
"lineNumber": 44,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-02-14T20:02:37.507Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-insertBefore(",
|
||||
"path": "js/views/key_verification_view.js",
|
||||
"line": " dialog.$el.insertBefore(this.el);",
|
||||
"lineNumber": 86,
|
||||
"line": " this.$('.key-verification-wrapper').append(view.el);",
|
||||
"lineNumber": 23,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-02-14T20:02:37.507Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/key_verification_view.js",
|
||||
"line": " this.$('button.verify').attr('disabled', true);",
|
||||
"lineNumber": 90,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-02-14T20:02:37.507Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/key_verification_view.js",
|
||||
"line": " this.$('button.verify').removeAttr('disabled');",
|
||||
"lineNumber": 121,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-02-14T20:02:37.507Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
"updated": "2020-06-23T06:48:06.829Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
|
@ -841,6 +813,22 @@
|
|||
"updated": "2018-10-11T19:22:47.331Z",
|
||||
"reasonDetail": "Operating on already-existing DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/safety_number_change_dialog_view.js",
|
||||
"line": " this.$('.safety-number-change-dialog-wrapper').append(dialog.el);",
|
||||
"lineNumber": 36,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-23T06:48:06.829Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/safety_number_change_dialog_view.js",
|
||||
"line": " this.$('.safety-number-change-dialog-wrapper').append(dialog.el);",
|
||||
"lineNumber": 36,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-23T06:48:06.829Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
|
@ -11579,6 +11567,15 @@
|
|||
"updated": "2020-02-14T20:02:37.507Z",
|
||||
"reasonDetail": "Used only to set focus"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/SafetyNumberChangeDialog.js",
|
||||
"line": " const cancelButtonRef = React.createRef();",
|
||||
"lineNumber": 14,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-23T06:48:06.829Z",
|
||||
"reasonDetail": "Used to focus cancel button when dialog opens"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/SearchResults.js",
|
||||
|
|
|
@ -13,11 +13,9 @@ export function remove() {
|
|||
}
|
||||
|
||||
export function isDone() {
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
return window.storage.get('chromiumRegistrationDone') === '';
|
||||
}
|
||||
|
||||
export function everDone() {
|
||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
||||
return window.storage.get('chromiumRegistrationDoneEver') === '' || isDone();
|
||||
}
|
||||
|
|
58
ts/util/safetyNumber.ts
Normal file
58
ts/util/safetyNumber.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export async function generateSecurityNumber(
|
||||
ourNumber: string,
|
||||
ourKey: ArrayBuffer,
|
||||
theirNumber: string,
|
||||
theirKey: ArrayBuffer
|
||||
): Promise<string> {
|
||||
return new window.libsignal.FingerprintGenerator(5200).createFor(
|
||||
ourNumber,
|
||||
ourKey,
|
||||
theirNumber,
|
||||
theirKey
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlaceholder(): string {
|
||||
return Array.from(Array(12))
|
||||
.map(() => 'XXXXX')
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export async function generateSecurityNumberBlock(
|
||||
contact: ConversationType
|
||||
): Promise<Array<string>> {
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
|
||||
const us = window.textsecure.storage.protocol.getIdentityRecord(
|
||||
ourUuid || ourNumber
|
||||
);
|
||||
const ourKey = us ? us.publicKey : null;
|
||||
|
||||
const them = window.textsecure.storage.protocol.getIdentityRecord(contact.id);
|
||||
const theirKey = them ? them.publicKey : null;
|
||||
|
||||
if (!ourKey) {
|
||||
throw new Error('Could not load our key');
|
||||
}
|
||||
|
||||
if (!theirKey) {
|
||||
throw new Error('Could not load their key');
|
||||
}
|
||||
|
||||
const securityNumber = await generateSecurityNumber(
|
||||
ourNumber,
|
||||
ourKey,
|
||||
contact.e164,
|
||||
theirKey
|
||||
);
|
||||
|
||||
const chunks = [];
|
||||
for (let i = 0; i < securityNumber.length; i += 5) {
|
||||
chunks.push(securityNumber.substring(i, i + 5));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
7
ts/window.d.ts
vendored
7
ts/window.d.ts
vendored
|
@ -13,9 +13,12 @@ import * as Crypto from './Crypto';
|
|||
import { ColorType, LocalizerType } from './types/Util';
|
||||
import { SendOptionsType } from './textsecure/SendMessage';
|
||||
|
||||
type TaskResultType = any;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
dcodeIO: DCodeIOType;
|
||||
getConversations: () => ConversationControllerType;
|
||||
getExpiration: () => string;
|
||||
getEnvironment: () => string;
|
||||
getSocketStatus: () => number;
|
||||
|
@ -83,12 +86,16 @@ export type ConversationType = {
|
|||
getColor(): ColorType | undefined;
|
||||
getName(): string | undefined;
|
||||
getNumber(): string;
|
||||
getProfiles(): Promise<Array<Promise<void>>>;
|
||||
getProfileName(): string | undefined;
|
||||
getRecipients: () => Array<string>;
|
||||
getSendOptions(): SendOptionsType;
|
||||
getTitle(): string;
|
||||
isVerified(): boolean;
|
||||
safeGetVerified(): Promise<number>;
|
||||
getIsAddedByContact(): boolean;
|
||||
addCallHistory(details: CallHistoryDetailsType): void;
|
||||
toggleVerified(): Promise<TaskResultType>;
|
||||
};
|
||||
|
||||
export type ConversationControllerType = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue