Show challenge when requested by server

This commit is contained in:
Fedor Indutny 2021-05-05 17:09:29 -07:00 committed by GitHub
parent 03c68da17d
commit 986d8a66bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1986 additions and 128 deletions

View file

@ -0,0 +1,33 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { boolean } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import { CaptchaDialog } from './CaptchaDialog';
import { Button } from './Button';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const story = storiesOf('Components/CaptchaDialog', module);
const i18n = setupI18n('en', enMessages);
story.add('CaptchaDialog', () => {
const [isSkipped, setIsSkipped] = useState(false);
if (isSkipped) {
return <Button onClick={() => setIsSkipped(false)}>Show again</Button>;
}
return (
<CaptchaDialog
i18n={i18n}
isPending={boolean('isPending', false)}
onContinue={action('onContinue')}
onSkip={() => setIsSkipped(true)}
/>
);
});

View file

@ -0,0 +1,99 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useRef, useState } from 'react';
import { LocalizerType } from '../types/Util';
import { Button, ButtonVariant } from './Button';
import { Modal } from './Modal';
import { Spinner } from './Spinner';
type PropsType = {
i18n: LocalizerType;
isPending: boolean;
onContinue: () => void;
onSkip: () => void;
};
export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element {
const { i18n, isPending, onSkip, onContinue } = props;
const [isClosing, setIsClosing] = useState(false);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const onCancelClick = (event: React.MouseEvent) => {
event.preventDefault();
setIsClosing(false);
};
const onSkipClick = (event: React.MouseEvent) => {
event.preventDefault();
onSkip();
};
if (isClosing && !isPending) {
return (
<Modal
moduleClassName="module-Modal"
i18n={i18n}
title={i18n('CaptchaDialog--can-close__title')}
>
<section>
<p>{i18n('CaptchaDialog--can-close__body')}</p>
</section>
<Modal.Footer>
<Button onClick={onCancelClick} variant={ButtonVariant.Secondary}>
{i18n('cancel')}
</Button>
<Button onClick={onSkipClick} variant={ButtonVariant.Destructive}>
{i18n('CaptchaDialog--can_close__skip-verification')}
</Button>
</Modal.Footer>
</Modal>
);
}
const onContinueClick = (event: React.MouseEvent) => {
event.preventDefault();
onContinue();
};
const updateButtonRef = (button: HTMLButtonElement): void => {
buttonRef.current = button;
if (button) {
button.focus();
}
};
return (
<Modal
moduleClassName="module-Modal--important"
i18n={i18n}
title={i18n('CaptchaDialog__title')}
hasXButton
onClose={() => setIsClosing(true)}
>
<section>
<p>{i18n('CaptchaDialog__first-paragraph')}</p>
<p>{i18n('CaptchaDialog__second-paragraph')}</p>
</section>
<Modal.Footer>
<Button
disabled={isPending}
onClick={onContinueClick}
ref={updateButtonRef}
variant={ButtonVariant.Primary}
>
{isPending ? (
<Spinner size="22px" svgSize="small" direction="on-captcha" />
) : (
'Continue'
)}
</Button>
</Modal.Footer>
</Modal>
);
}

View file

@ -4,9 +4,11 @@
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { select } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import { LeftPane, LeftPaneMode, PropsType } from './LeftPane';
import { CaptchaDialog } from './CaptchaDialog';
import { PropsData as ConversationListItemPropsType } from './conversationList/ConversationListItem';
import { MessageSearchResult } from './conversationList/MessageSearchResult';
import { setup as setupI18n } from '../../js/modules/i18n';
@ -106,6 +108,12 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
modeSpecificProps: defaultModeSpecificProps,
openConversationInternal: action('openConversationInternal'),
regionCode: 'US',
challengeStatus: select(
'challengeStatus',
['idle', 'required', 'pending'],
'idle'
),
setChallengeStatus: action('setChallengeStatus'),
renderExpiredBuildDialog: () => <div />,
renderMainHeader: () => <div />,
renderMessageSearchResult: (id: string, style: React.CSSProperties) => (
@ -126,6 +134,14 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
renderNetworkStatus: () => <div />,
renderRelinkDialog: () => <div />,
renderUpdateDialog: () => <div />,
renderCaptchaDialog: () => (
<CaptchaDialog
i18n={i18n}
isPending={overrideProps.challengeStatus === 'pending'}
onContinue={action('onCaptchaContinue')}
onSkip={action('onCaptchaSkip')}
/>
),
selectedConversationId: undefined,
selectedMessageId: undefined,
setComposeSearchTerm: action('setComposeSearchTerm'),
@ -468,3 +484,33 @@ story.add('Compose: some contacts, some groups, with a search term', () => (
})}
/>
));
// Captcha flow
story.add('Captcha dialog: required', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Inbox,
pinnedConversations,
conversations: defaultConversations,
archivedConversations: [],
},
challengeStatus: 'required',
})}
/>
));
story.add('Captcha dialog: pending', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Inbox,
pinnedConversations,
conversations: defaultConversations,
archivedConversations: [],
},
challengeStatus: 'pending',
})}
/>
));

View file

@ -79,6 +79,8 @@ export type PropsType = {
selectedConversationId: undefined | string;
selectedMessageId: undefined | string;
regionCode: string;
challengeStatus: 'idle' | 'required' | 'pending';
setChallengeStatus: (status: 'idle') => void;
// Action Creators
cantAddContactToGroup: (conversationId: string) => void;
@ -110,6 +112,7 @@ export type PropsType = {
renderNetworkStatus: () => JSX.Element;
renderRelinkDialog: () => JSX.Element;
renderUpdateDialog: () => JSX.Element;
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
};
export const LeftPane: React.FC<PropsType> = ({
@ -121,6 +124,8 @@ export const LeftPane: React.FC<PropsType> = ({
createGroup,
i18n,
modeSpecificProps,
challengeStatus,
setChallengeStatus,
openConversationInternal,
renderExpiredBuildDialog,
renderMainHeader,
@ -128,6 +133,7 @@ export const LeftPane: React.FC<PropsType> = ({
renderNetworkStatus,
renderRelinkDialog,
renderUpdateDialog,
renderCaptchaDialog,
selectedConversationId,
selectedMessageId,
setComposeSearchTerm,
@ -464,6 +470,12 @@ export const LeftPane: React.FC<PropsType> = ({
{footerContents && (
<div className="module-left-pane__footer">{footerContents}</div>
)}
{challengeStatus !== 'idle' &&
renderCaptchaDialog({
onSkip() {
setChallengeStatus('idle');
},
})}
</div>
);
};

View file

@ -32,6 +32,7 @@ export type PropsType = {
isMe?: boolean;
name?: string;
color?: ColorType;
disabled?: boolean;
isVerified?: boolean;
profileName?: string;
title: string;
@ -339,6 +340,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
const {
avatarPath,
color,
disabled,
i18n,
name,
startComposing,
@ -437,6 +439,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
/>
)}
<input
disabled={disabled}
type="text"
ref={this.inputRef}
className={classNames(

View file

@ -48,12 +48,22 @@ export function Modal({
aria-label={i18n('close')}
type="button"
className="module-Modal__close-button"
tabIndex={0}
onClick={() => {
onClose();
}}
/>
)}
{title && <h1 className="module-Modal__title">{title}</h1>}
{title && (
<h1
className={classNames(
'module-Modal__title',
hasXButton ? 'module-Modal__title--with-x-button' : null
)}
>
{title}
</h1>
)}
</div>
)}
<div

View file

@ -19,6 +19,7 @@ const defaultProps = {
socketStatus: 0,
manualReconnect: action('manual-reconnect'),
withinConnectingGracePeriod: false,
challengeStatus: 'idle' as const,
};
const permutations = [

View file

@ -11,6 +11,7 @@ export const SpinnerDirections = [
'outgoing',
'incoming',
'on-background',
'on-captcha',
'on-progress-dialog',
'on-avatar',
] as const;

View file

@ -489,6 +489,15 @@ story.add('Error', () => {
return renderBothDirections(props);
});
story.add('Paused', () => {
const props = createProps({
status: 'paused',
text: 'I am up to a challenge',
});
return renderBothDirections(props);
});
story.add('Partial Send', () => {
const props = createProps({
status: 'partial-sent',

View file

@ -67,6 +67,7 @@ const THREE_HOURS = 3 * 60 * 60 * 1000;
export const MessageStatuses = [
'delivered',
'error',
'paused',
'partial-sent',
'read',
'sending',
@ -522,8 +523,31 @@ export class Message extends React.Component<Props, State> {
const isError = status === 'error' && direction === 'outgoing';
const isPartiallySent =
status === 'partial-sent' && direction === 'outgoing';
const isPaused = status === 'paused';
if (isError || isPartiallySent || isPaused) {
let statusInfo: React.ReactChild;
if (isError) {
statusInfo = i18n('sendFailed');
} else if (isPaused) {
statusInfo = i18n('sendPaused');
} else {
statusInfo = (
<button
type="button"
className="module-message__metadata__tapable"
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
showMessageDetail(id);
}}
>
{i18n('partiallySent')}
</button>
);
}
if (isError || isPartiallySent) {
return (
<span
className={classNames({
@ -533,22 +557,7 @@ export class Message extends React.Component<Props, State> {
'module-message__metadata__date--with-image-no-caption': withImageNoCaption,
})}
>
{isError ? (
i18n('sendFailed')
) : (
<button
type="button"
className="module-message__metadata__tapable"
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
showMessageDetail(id);
}}
>
{i18n('partiallySent')}
</button>
)}
{statusInfo}
</span>
);
}
@ -1232,7 +1241,15 @@ export class Message extends React.Component<Props, State> {
public renderError(isCorrectSide: boolean): JSX.Element | null {
const { status, direction } = this.props;
if (!isCorrectSide || (status !== 'error' && status !== 'partial-sent')) {
if (!isCorrectSide) {
return null;
}
if (
status !== 'paused' &&
status !== 'error' &&
status !== 'partial-sent'
) {
return null;
}
@ -1241,7 +1258,8 @@ export class Message extends React.Component<Props, State> {
<div
className={classNames(
'module-message__error',
`module-message__error--${direction}`
`module-message__error--${direction}`,
`module-message__error--${status}`
)}
/>
</div>
@ -1446,7 +1464,9 @@ export class Message extends React.Component<Props, State> {
const { canDeleteForEveryone } = this.state;
const showRetry =
(status === 'error' || status === 'partial-sent') &&
(status === 'paused' ||
status === 'error' ||
status === 'partial-sent') &&
direction === 'outgoing';
const multipleAttachments = attachments && attachments.length > 1;

View file

@ -27,6 +27,7 @@ export const MessageStatuses = [
'sent',
'delivered',
'read',
'paused',
'error',
'partial-sent',
] as const;