Improvements to SafetyNumberChangeDialog

This commit is contained in:
Scott Nonnenberg 2022-11-01 17:10:27 -07:00 committed by GitHub
parent 6700f6fa15
commit 4fc1b6388c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 329 additions and 114 deletions

View file

@ -433,22 +433,38 @@
}, },
"changedVerificationWarning": { "changedVerificationWarning": {
"message": "The following people may have reinstalled or changed devices. Verify your safety number with them to ensure privacy.", "message": "The following people may have reinstalled or changed devices. Verify your safety number with them to ensure privacy.",
"description": "(deleted 2022/11/26) Shown on confirmation dialog when user attempts to send a message"
},
"safetyNumberChangeDialog__message": {
"message": "The following people may have reinstalled Signal or changed devices. Click a recipient to confirm the new safety number. This is optional.",
"description": "Shown on confirmation dialog when user attempts to send a message" "description": "Shown on confirmation dialog when user attempts to send a message"
}, },
"safetyNumberChangeDialog__pending-messages": { "safetyNumberChangeDialog__pending-messages": {
"message": "Send pending messages", "message": "Send pending messages",
"description": "Shown on confirmation dialog when user attempts to send a message in the outbox" "description": "Shown on confirmation dialog when user attempts to send a message in the outbox"
}, },
"safetyNumberChangeDialog__review": {
"message": "Review",
"description": "Shown to enter 'review' mode if more than five contacts have changed safety numbers"
},
"icu:safetyNumberChangeDialog__many-contacts": {
"messageformat": "{count, plural, other {You have # connections who may have reinstalled Signal or changed devices. You can optionally review their safety numbers before sending.}}",
"description": "Shown during an attempted send when more than five contacts have changed their safety numbers"
},
"identityKeyErrorOnSend": { "identityKeyErrorOnSend": {
"message": "Your safety number with $name1$ has changed. This could either mean that someone is trying to intercept your communication or that $name2$ has simply reinstalled Signal. You may wish to verify your safety number with this contact.", "message": "Your safety number with $name1$ has changed. This could either mean that someone is trying to intercept your communication or that $name2$ has simply reinstalled Signal. You may wish to verify your safety number with this contact.",
"description": "Shown when user clicks on a failed recipient in the message detail view after an identity key change" "description": "Shown when user clicks on a failed recipient in the message detail view after an identity key change"
}, },
"sendAnyway": { "sendAnyway": {
"message": "Send Anyway", "message": "Send anyway",
"description": "Used on a warning dialog to make it clear that it might be risky to send the message."
},
"safetyNumberChangeDialog_send": {
"message": "Send",
"description": "Used on a warning dialog to make it clear that it might be risky to send the message." "description": "Used on a warning dialog to make it clear that it might be risky to send the message."
}, },
"callAnyway": { "callAnyway": {
"message": "Call Anyway", "message": "Call anyway",
"description": "Used on a warning dialog to make it clear that it might be risky to call the conversation." "description": "Used on a warning dialog to make it clear that it might be risky to call the conversation."
}, },
"continueCall": { "continueCall": {

View file

@ -2,9 +2,61 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
.module-SafetyNumberChangeDialog { .module-SafetyNumberChangeDialog {
&__confirm-dialog__header {
padding-bottom: 0px;
// We've got no title, but we want the X button from ConfirmationDialog, so
// we need to bump the dialog contents up into the header area just a bit.
margin-bottom: -5px;
}
// Used to ensure that a set of spans reverse order under RTL
&__rtl-span {
display: inline-block;
}
&__shield-icon {
margin-left: auto;
margin-right: auto;
height: 24px;
width: 24px;
@include light-theme {
@include color-svg(
'../images/icons/v2/safety-number-outline-24.svg',
$color-gray-90
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/safety-number-outline-24.svg',
$color-white
);
}
}
&__title {
@include font-body-1-bold;
text-align: center;
margin-top: 8px;
@include light-theme {
color: $color-gray-90;
}
@include dark-theme {
color: $color-white;
}
}
&__message { &__message {
@include font-body-2; @include font-body-2;
text-align: center; text-align: center;
margin-top: 8px;
margin-bottom: 24px;
padding-left: 4px;
padding-right: 4px;
@include light-theme { @include light-theme {
color: $color-gray-60; color: $color-gray-60;
@ -18,11 +70,12 @@
&__contacts { &__contacts {
list-style-type: none; list-style-type: none;
max-height: 300px; max-height: 300px;
overflow-y: scroll;
padding: 0; padding: 0;
} }
&__contact { &__contact {
$contact: &;
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -34,14 +87,16 @@
} }
&--name { &--name {
@include font-body-1-bold; @include font-body-1;
@include dark-theme { @include dark-theme {
color: $color-white; color: $color-white;
} }
} }
&--number { &--subtitle {
@include font-subtitle;
@include light-theme { @include light-theme {
color: $color-gray-60; color: $color-gray-60;
} }
@ -52,27 +107,22 @@
} }
&--view { &--view {
@include font-body-1-bold; @include button-reset;
background: inherit; @include button-secondary-blue-text;
border: none;
cursor: pointer; opacity: 0;
margin-right: 2px; transition: opacity 150ms cubic-bezier(0.17, 0.17, 0, 1);
outline: none;
// Using keyboard/mouse classes directly; mixins were doing weird things
.mouse-mode #{$contact}:hover & {
opacity: 1;
}
.keyboard-mode #{$contact}:focus-within & {
opacity: 1;
}
border-radius: 4px;
padding: 8px 14px; padding: 8px 14px;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-ultramarine;
}
}
@include light-theme {
color: $color-ultramarine;
}
@include dark-theme {
color: $color-gray-05;
}
} }
} }
} }

View file

@ -3,7 +3,6 @@
import * as React from 'react'; import * as React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { text } from '@storybook/addon-knobs';
import { ConfirmationDialog } from './ConfirmationDialog'; import { ConfirmationDialog } from './ConfirmationDialog';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
@ -21,7 +20,7 @@ export const _ConfirmationDialog = (): JSX.Element => {
dialogName="test" dialogName="test"
i18n={i18n} i18n={i18n}
onClose={action('onClose')} onClose={action('onClose')}
title={text('Title', 'Foo bar banana baz?')} title="Foo bar banana baz?"
actions={[ actions={[
{ {
text: 'Negate', text: 'Negate',
@ -35,7 +34,7 @@ export const _ConfirmationDialog = (): JSX.Element => {
}, },
]} ]}
> >
{text('Child text', 'asdf blip')} asdf blip
</ConfirmationDialog> </ConfirmationDialog>
); );
}; };
@ -51,7 +50,7 @@ export const CustomCancelText = (): JSX.Element => {
cancelText="Nah" cancelText="Nah"
i18n={i18n} i18n={i18n}
onClose={action('onClose')} onClose={action('onClose')}
title={text('Title', 'Foo bar banana baz?')} title="Maybs?"
actions={[ actions={[
{ {
text: 'Maybe', text: 'Maybe',
@ -60,7 +59,7 @@ export const CustomCancelText = (): JSX.Element => {
}, },
]} ]}
> >
{text('Child text', 'asdf blip')} Because.
</ConfirmationDialog> </ConfirmationDialog>
); );
}; };
@ -68,3 +67,24 @@ export const CustomCancelText = (): JSX.Element => {
CustomCancelText.story = { CustomCancelText.story = {
name: 'Custom cancel text', name: 'Custom cancel text',
}; };
export const NoDefaultCancel = (): JSX.Element => {
return (
<ConfirmationDialog
dialogName="test"
noDefaultCancelButton
i18n={i18n}
onClose={action('onClose')}
title="Do you?"
actions={[
{
text: 'Yep',
style: 'affirmative',
action: action('affirmative'),
},
]}
>
No default cancel!
</ConfirmationDialog>
);
};

View file

@ -27,6 +27,7 @@ export type OwnProps = Readonly<{
i18n: LocalizerType; i18n: LocalizerType;
moduleClassName?: string; moduleClassName?: string;
noMouseClose?: boolean; noMouseClose?: boolean;
noDefaultCancelButton?: boolean;
onCancel?: () => unknown; onCancel?: () => unknown;
onClose: () => unknown; onClose: () => unknown;
onTopOfEverything?: boolean; onTopOfEverything?: boolean;
@ -67,6 +68,7 @@ export const ConfirmationDialog = React.memo(
i18n, i18n,
moduleClassName, moduleClassName,
noMouseClose, noMouseClose,
noDefaultCancelButton,
onCancel, onCancel,
onClose, onClose,
onTopOfEverything, onTopOfEverything,
@ -98,16 +100,18 @@ export const ConfirmationDialog = React.memo(
const footer = ( const footer = (
<> <>
<Button {!noDefaultCancelButton ? (
onClick={handleCancel} <Button
ref={focusRef} onClick={handleCancel}
variant={ ref={focusRef}
cancelButtonVariant || variant={
(hasActions ? ButtonVariant.Secondary : ButtonVariant.Primary) cancelButtonVariant ||
} (hasActions ? ButtonVariant.Secondary : ButtonVariant.Primary)
> }
{cancelText || i18n('confirmation-dialog--Cancel')} >
</Button> {cancelText || i18n('confirmation-dialog--Cancel')}
</Button>
) : null}
{actions.map((action, i) => ( {actions.map((action, i) => (
<Button <Button
key={action.text} key={action.text}

View file

@ -22,22 +22,24 @@ const contactWithAllData = getDefaultConversation({
phoneNumber: '(305) 123-4567', phoneNumber: '(305) 123-4567',
}); });
const contactWithJustProfile = getDefaultConversation({ const contactWithJustProfileVerified = getDefaultConversation({
id: 'def', id: 'def',
avatarPath: undefined, avatarPath: undefined,
title: '-*Smartest Dude*-', title: '-*Smartest Dude*-',
profileName: '-*Smartest Dude*-', profileName: '-*Smartest Dude*-',
name: undefined, name: undefined,
phoneNumber: '(305) 123-4567', phoneNumber: '(305) 123-4567',
isVerified: true,
}); });
const contactWithJustNumber = getDefaultConversation({ const contactWithJustNumberVerified = getDefaultConversation({
id: 'xyz', id: 'xyz',
avatarPath: undefined, avatarPath: undefined,
profileName: undefined, profileName: undefined,
name: undefined, name: undefined,
title: '(305) 123-4567', title: '(305) 123-4567',
phoneNumber: '(305) 123-4567', phoneNumber: '(305) 123-4567',
isVerified: true,
}); });
const contactWithNothing = getDefaultConversation({ const contactWithNothing = getDefaultConversation({
@ -98,8 +100,8 @@ export const MultiContactDialog = (): JSX.Element => {
<SafetyNumberChangeDialog <SafetyNumberChangeDialog
contacts={[ contacts={[
contactWithAllData, contactWithAllData,
contactWithJustProfile, contactWithJustProfileVerified,
contactWithJustNumber, contactWithJustNumberVerified,
contactWithNothing, contactWithNothing,
]} ]}
getPreferredBadge={() => undefined} getPreferredBadge={() => undefined}
@ -115,14 +117,35 @@ export const MultiContactDialog = (): JSX.Element => {
); );
}; };
export const AllVerified = (): JSX.Element => {
const theme = useTheme();
return (
<SafetyNumberChangeDialog
contacts={[contactWithJustProfileVerified, contactWithJustNumberVerified]}
getPreferredBadge={() => undefined}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
renderSafetyNumber={() => {
action('renderSafetyNumber');
return <div>This is a mock Safety Number View</div>;
}}
theme={theme}
/>
);
};
AllVerified.story = {
name: 'All verified; Send button instead',
};
export const MultipleContactsAllWithBadges = (): JSX.Element => { export const MultipleContactsAllWithBadges = (): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<SafetyNumberChangeDialog <SafetyNumberChangeDialog
contacts={[ contacts={[
contactWithAllData, contactWithAllData,
contactWithJustProfile, contactWithJustProfileVerified,
contactWithJustNumber, contactWithJustNumberVerified,
contactWithNothing, contactWithNothing,
]} ]}
getPreferredBadge={() => getFakeBadge()} getPreferredBadge={() => getFakeBadge()}
@ -142,14 +165,14 @@ MultipleContactsAllWithBadges.story = {
name: 'Multiple contacts, all with badges', name: 'Multiple contacts, all with badges',
}; };
export const ScrollDialog = (): JSX.Element => { export const TenContacts = (): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<SafetyNumberChangeDialog <SafetyNumberChangeDialog
contacts={[ contacts={[
contactWithAllData, contactWithAllData,
contactWithJustProfile, contactWithJustProfileVerified,
contactWithJustNumber, contactWithJustNumberVerified,
contactWithNothing, contactWithNothing,
contactWithAllData, contactWithAllData,
contactWithAllData, contactWithAllData,
@ -170,3 +193,7 @@ export const ScrollDialog = (): JSX.Element => {
/> />
); );
}; };
TenContacts.story = {
name: 'Ten contacts; first isReviewing = false, then scrolling dialog',
};

View file

@ -5,6 +5,7 @@ import * as React from 'react';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { Avatar } from './Avatar'; import { Avatar } from './Avatar';
import type { ActionSpec } from './ConfirmationDialog';
import { ConfirmationDialog } from './ConfirmationDialog'; import { ConfirmationDialog } from './ConfirmationDialog';
import { InContactsIcon } from './InContactsIcon'; import { InContactsIcon } from './InContactsIcon';
import { Modal } from './Modal'; import { Modal } from './Modal';
@ -46,6 +47,9 @@ export const SafetyNumberChangeDialog = ({
renderSafetyNumber, renderSafetyNumber,
theme, theme,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [isReviewing, setIsReviewing] = React.useState<boolean>(
contacts.length <= 5
);
const [selectedContact, setSelectedContact] = React.useState< const [selectedContact, setSelectedContact] = React.useState<
ConversationType | undefined ConversationType | undefined
>(undefined); >(undefined);
@ -76,80 +80,174 @@ export const SafetyNumberChangeDialog = ({
); );
} }
const allVerified = contacts.every(contact => contact.isVerified);
const actions: Array<ActionSpec> = [
{
action: onConfirm,
text:
confirmText ||
(allVerified
? i18n('safetyNumberChangeDialog_send')
: i18n('sendAnyway')),
style: 'affirmative',
},
];
if (isReviewing) {
return (
<ConfirmationDialog
key="SafetyNumberChangeDialog.reviewing"
dialogName="SafetyNumberChangeDialog.reviewing"
actions={actions}
hasXButton
i18n={i18n}
moduleClassName="module-SafetyNumberChangeDialog__confirm-dialog"
noMouseClose
noDefaultCancelButton={!isReviewing}
onCancel={onClose}
onClose={noop}
>
<div className="module-SafetyNumberChangeDialog__shield-icon" />
<div className="module-SafetyNumberChangeDialog__title">
{i18n('safetyNumberChanges')}
</div>
<div className="module-SafetyNumberChangeDialog__message">
{i18n('safetyNumberChangeDialog__message')}
</div>
<ul className="module-SafetyNumberChangeDialog__contacts">
{contacts.map((contact: ConversationType) => {
const shouldShowNumber = Boolean(
contact.name || contact.profileName
);
return (
<ContactRow
contact={contact}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
setSelectedContact={setSelectedContact}
shouldShowNumber={shouldShowNumber}
theme={theme}
/>
);
})}
</ul>
</ConfirmationDialog>
);
}
actions.unshift({
action: () => setIsReviewing(true),
text: i18n('safetyNumberChangeDialog__review'),
});
return ( return (
<ConfirmationDialog <ConfirmationDialog
dialogName="SafetyNumberChangeDialog.confirmSend" key="SafetyNumberChangeDialog.manyContacts"
actions={[ dialogName="SafetyNumberChangeDialog.manyContacts"
{ actions={actions}
action: onConfirm, hasXButton
text: confirmText || i18n('sendMessageToContact'),
style: 'affirmative',
},
]}
i18n={i18n} i18n={i18n}
moduleClassName="module-SafetyNumberChangeDialog__confirm-dialog"
noMouseClose noMouseClose
noDefaultCancelButton={!isReviewing}
onCancel={onClose} onCancel={onClose}
onClose={noop} onClose={noop}
title={i18n('safetyNumberChanges')}
> >
<div className="module-SafetyNumberChangeDialog__message"> <div className="module-SafetyNumberChangeDialog__shield-icon" />
{i18n('changedVerificationWarning')} <div className="module-SafetyNumberChangeDialog__title">
{i18n('safetyNumberChanges')}
</div> </div>
<ul className="module-SafetyNumberChangeDialog__contacts"> <div className="module-SafetyNumberChangeDialog__message">
{contacts.map((contact: ConversationType) => { {i18n('icu:safetyNumberChangeDialog__many-contacts', {
const shouldShowNumber = Boolean(contact.name || contact.profileName); count: contacts.length,
return (
<li
className="module-SafetyNumberChangeDialog__contact"
key={contact.id}
>
<Avatar
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath}
badge={getPreferredBadge(contact.badges)}
color={contact.color}
conversationType="direct"
i18n={i18n}
isMe={contact.isMe}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
theme={theme}
title={contact.title}
sharedGroupNames={contact.sharedGroupNames}
size={52}
unblurredAvatarPath={contact.unblurredAvatarPath}
/>
<div className="module-SafetyNumberChangeDialog__contact--wrapper">
<div className="module-SafetyNumberChangeDialog__contact--name">
{contact.title}
{isInSystemContacts(contact) ? (
<span>
{' '}
<InContactsIcon i18n={i18n} />
</span>
) : null}
</div>
{shouldShowNumber ? (
<div className="module-SafetyNumberChangeDialog__contact--number">
{contact.phoneNumber}
</div>
) : null}
</div>
<button
className="module-SafetyNumberChangeDialog__contact--view"
onClick={() => {
setSelectedContact(contact);
}}
tabIndex={0}
type="button"
>
{i18n('view')}
</button>
</li>
);
})} })}
</ul> </div>
</ConfirmationDialog> </ConfirmationDialog>
); );
}; };
type ContactRowProps = Readonly<{
contact: ConversationType;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
setSelectedContact: (contact: ConversationType) => void;
shouldShowNumber: boolean;
theme: ThemeType;
}>;
function ContactRow({
contact,
getPreferredBadge,
i18n,
setSelectedContact,
shouldShowNumber,
theme,
}: ContactRowProps) {
return (
<li className="module-SafetyNumberChangeDialog__contact" key={contact.id}>
<Avatar
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath}
badge={getPreferredBadge(contact.badges)}
color={contact.color}
conversationType="direct"
i18n={i18n}
isMe={contact.isMe}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
theme={theme}
title={contact.title}
sharedGroupNames={contact.sharedGroupNames}
size={36}
unblurredAvatarPath={contact.unblurredAvatarPath}
/>
<div className="module-SafetyNumberChangeDialog__contact--wrapper">
<div className="module-SafetyNumberChangeDialog__contact--name">
{contact.title}
{isInSystemContacts(contact) ? (
<span>
{' '}
<InContactsIcon i18n={i18n} />
</span>
) : null}
</div>
{shouldShowNumber || contact.isVerified ? (
<div className="module-SafetyNumberChangeDialog__contact--subtitle">
{shouldShowNumber ? (
<span className="module-SafetyNumberChangeDialog__rtl-span">
{contact.phoneNumber}
</span>
) : (
''
)}
{shouldShowNumber && contact.isVerified ? (
<span className="module-SafetyNumberChangeDialog__rtl-span">
&nbsp;&middot;&nbsp;
</span>
) : (
''
)}
{contact.isVerified ? (
<span className="module-SafetyNumberChangeDialog__rtl-span">
{i18n('verified')}
</span>
) : (
''
)}
</div>
) : null}
</div>
<button
className="module-SafetyNumberChangeDialog__contact--view"
onClick={() => {
setSelectedContact(contact);
}}
tabIndex={0}
type="button"
>
{i18n('view')}
</button>
</li>
);
}