Small UI fixes for left pane dialogs

This commit is contained in:
Fedor Indutny 2021-09-17 15:20:49 -07:00 committed by GitHub
parent 6c906d5da8
commit f3715411c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 298 additions and 226 deletions

View file

@ -2337,6 +2337,9 @@
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {
"message": "Later" "message": "Later"
}, },
"autoUpdateIgnoreButtonLabel": {
"message": "Ignore update"
},
"leftTheGroup": { "leftTheGroup": {
"message": "$name$ left the group.", "message": "$name$ left the group.",
"description": "Shown in the conversation history when a single person leaves the group", "description": "Shown in the conversation history when a single person leaves the group",

View file

@ -8,12 +8,17 @@
} }
.LeftPaneDialog { .LeftPaneDialog {
@include button-reset;
align-items: center; align-items: center;
background: $color-ultramarine; background: $color-ultramarine;
color: $color-white; color: $color-white;
display: flex; display: flex;
width: 100%;
min-height: 64px; min-height: 64px;
padding: 12px 18px; padding: 12px 18px;
user-select: none;
cursor: inherit;
&__container { &__container {
display: flex; display: flex;
@ -46,10 +51,11 @@
} }
&__icon { &__icon {
width: 20px; width: 24px;
height: 20px; height: 24px;
margin-right: 18px; margin-right: 18px;
background-color: $color-white; background-color: $color-white;
-webkit-mask-size: contain;
&--relink { &--relink {
-webkit-mask: url('../images/icons/v2/link-broken-16.svg') no-repeat -webkit-mask: url('../images/icons/v2/link-broken-16.svg') no-repeat

View file

@ -5,6 +5,8 @@ import React from 'react';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { LeftPaneDialog } from './LeftPaneDialog';
type PropsType = { type PropsType = {
hasExpired: boolean; hasExpired: boolean;
i18n: LocalizerType; i18n: LocalizerType;
@ -19,19 +21,17 @@ export const DialogExpiredBuild = ({
} }
return ( return (
<div className="LeftPaneDialog LeftPaneDialog--error"> <LeftPaneDialog type="error">
<div className="LeftPaneDialog__message"> {i18n('expiredWarning')}{' '}
{i18n('expiredWarning')}{' '} <a
<a className="LeftPaneDialog__action-text"
className="LeftPaneDialog__action-text" href="https://signal.org/download/"
href="https://signal.org/download/" rel="noreferrer"
rel="noreferrer" tabIndex={-1}
tabIndex={-1} target="_blank"
target="_blank" >
> {i18n('upgrade')}
{i18n('upgrade')} </a>
</a> </LeftPaneDialog>
</div>
</div>
); );
}; };

View file

@ -1,8 +1,9 @@
// Copyright 2020-2021 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { useEffect } from 'react';
import { LeftPaneDialog } from './LeftPaneDialog';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { SocketStatus } from '../types/SocketStatus'; import { SocketStatus } from '../types/SocketStatus';
@ -16,42 +17,6 @@ export type PropsType = NetworkStateType & {
manualReconnect: () => void; manualReconnect: () => void;
}; };
type RenderDialogTypes = {
isConnecting?: boolean;
title: string;
subtext: string;
renderActionableButton?: () => JSX.Element;
};
function renderDialog({
isConnecting,
title,
subtext,
renderActionableButton,
}: RenderDialogTypes): JSX.Element {
return (
<div className="LeftPaneDialog LeftPaneDialog--warning">
{isConnecting ? (
<div className="LeftPaneDialog__spinner-container">
<Spinner
direction="on-avatar"
moduleClassName="LeftPaneDialog__spinner"
size="22px"
svgSize="small"
/>
</div>
) : (
<div className="LeftPaneDialog__icon LeftPaneDialog__icon--network" />
)}
<div className="LeftPaneDialog__message">
<h3>{title}</h3>
<span>{subtext}</span>
<div>{renderActionableButton && renderActionableButton()}</div>
</div>
</div>
);
}
export const DialogNetworkStatus = ({ export const DialogNetworkStatus = ({
hasNetworkDialog, hasNetworkDialog,
i18n, i18n,
@ -59,8 +24,10 @@ export const DialogNetworkStatus = ({
socketStatus, socketStatus,
manualReconnect, manualReconnect,
}: PropsType): JSX.Element | null => { }: PropsType): JSX.Element | null => {
const [isConnecting, setIsConnecting] = React.useState<boolean>(false); const [isConnecting, setIsConnecting] = React.useState<boolean>(
React.useEffect(() => { socketStatus === SocketStatus.CONNECTING
);
useEffect(() => {
if (!hasNetworkDialog) { if (!hasNetworkDialog) {
return () => null; return () => null;
} }
@ -80,62 +47,46 @@ export const DialogNetworkStatus = ({
}; };
}, [hasNetworkDialog, isConnecting, setIsConnecting]); }, [hasNetworkDialog, isConnecting, setIsConnecting]);
if (!hasNetworkDialog) {
return null;
}
const reconnect = () => { const reconnect = () => {
setIsConnecting(true); setIsConnecting(true);
manualReconnect(); manualReconnect();
}; };
const manualReconnectButton = (): JSX.Element => ( if (!hasNetworkDialog) {
<button return null;
className="LeftPaneDialog__action-text" }
onClick={reconnect}
type="button"
>
{i18n('connect')}
</button>
);
if (isConnecting) { if (isConnecting) {
return renderDialog({ const spinner = (
isConnecting: true, <div className="LeftPaneDialog__spinner-container">
subtext: i18n('connectingHangOn'), <Spinner
title: i18n('connecting'), direction="on-avatar"
}); moduleClassName="LeftPaneDialog__spinner"
size="22px"
svgSize="small"
/>
</div>
);
return (
<LeftPaneDialog
type="warning"
icon={spinner}
title={i18n('connecting')}
subtitle={i18n('connectingHangOn')}
/>
);
} }
if (!isOnline) { return (
return renderDialog({ <LeftPaneDialog
renderActionableButton: manualReconnectButton, type="warning"
subtext: i18n('checkNetworkConnection'), icon="network"
title: i18n('offline'), title={isOnline ? i18n('disconnected') : i18n('offline')}
}); subtitle={i18n('checkNetworkConnection')}
} hasAction
clickLabel={i18n('connect')}
let subtext = ''; onClick={reconnect}
let title = ''; />
let renderActionableButton; );
switch (socketStatus) {
case SocketStatus.CONNECTING:
subtext = i18n('connectingHangOn');
title = i18n('connecting');
break;
case SocketStatus.CLOSED:
case SocketStatus.CLOSING:
default:
renderActionableButton = manualReconnectButton;
title = i18n('disconnected');
subtext = i18n('checkNetworkConnection');
}
return renderDialog({
isConnecting: socketStatus === SocketStatus.CONNECTING,
renderActionableButton,
subtext,
title,
});
}; };

View file

@ -5,6 +5,8 @@ import React from 'react';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { LeftPaneDialog } from './LeftPaneDialog';
export type PropsType = { export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
isRegistrationDone: boolean; isRegistrationDone: boolean;
@ -21,20 +23,13 @@ export const DialogRelink = ({
} }
return ( return (
<div className="LeftPaneDialog LeftPaneDialog--warning"> <LeftPaneDialog
<div className="LeftPaneDialog__icon LeftPaneDialog__icon--relink" /> type="warning"
<div className="LeftPaneDialog__message"> icon="relink"
<h3>{i18n('unlinked')}</h3> clickLabel={i18n('unlinkedWarning')}
<div> onClick={relinkDevice}
<button title={i18n('unlinked')}
className="LeftPaneDialog__action-text" hasAction
onClick={relinkDevice} />
type="button"
>
{i18n('unlinkedWarning')}
</button>
</div>
</div>
</div>
); );
}; };

View file

@ -5,8 +5,9 @@ import React from 'react';
import formatFileSize from 'filesize'; import formatFileSize from 'filesize';
import { DialogType } from '../types/Dialogs'; import { DialogType } from '../types/Dialogs';
import { Intl } from './Intl';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { Intl } from './Intl';
import { LeftPaneDialog } from './LeftPaneDialog';
export type PropsType = { export type PropsType = {
dialogType: DialogType; dialogType: DialogType;
@ -48,132 +49,99 @@ export const DialogUpdate = ({
if (dialogType === DialogType.Cannot_Update) { if (dialogType === DialogType.Cannot_Update) {
return ( return (
<div className="LeftPaneDialog LeftPaneDialog--warning"> <LeftPaneDialog type="warning" title={i18n('cannotUpdate')}>
<div className="LeftPaneDialog__message"> <span>
<h3>{i18n('cannotUpdate')}</h3> <Intl
<span> components={[
<Intl <a
components={[ key="signal-download"
<a href="https://signal.org/download/"
key="signal-download" rel="noreferrer"
href="https://signal.org/download/" target="_blank"
rel="noreferrer" >
target="_blank" https://signal.org/download/
> </a>,
https://signal.org/download/ ]}
</a>, i18n={i18n}
]} id="cannotUpdateDetail"
i18n={i18n} />
id="cannotUpdateDetail" </span>
/> </LeftPaneDialog>
</span>
</div>
</div>
); );
} }
if (dialogType === DialogType.MacOS_Read_Only) { if (dialogType === DialogType.MacOS_Read_Only) {
return ( return (
<div className="LeftPaneDialog LeftPaneDialog--warning"> <LeftPaneDialog
<div className="LeftPaneDialog__container"> type="warning"
<div className="LeftPaneDialog__message"> title={i18n('cannotUpdate')}
<h3>{i18n('cannotUpdate')}</h3> hasXButton
<span> closeLabel={i18n('close')}
<Intl onClose={dismissDialog}
components={{ >
app: <strong key="app">Signal.app</strong>, <span>
folder: <strong key="folder">/Applications</strong>, <Intl
}} components={{
i18n={i18n} app: <strong key="app">Signal.app</strong>,
id="readOnlyVolume" folder: <strong key="folder">/Applications</strong>,
/> }}
</span> i18n={i18n}
</div> id="readOnlyVolume"
</div>
<div className="LeftPaneDialog__container-close">
<button
aria-label={i18n('close')}
className="LeftPaneDialog__close-button"
onClick={dismissDialog}
tabIndex={0}
type="button"
/> />
</div> </span>
</div> </LeftPaneDialog>
); );
} }
let size: string | undefined; let title = i18n('autoUpdateNewVersionTitle');
if ( if (
downloadSize && downloadSize &&
(dialogType === DialogType.DownloadReady || (dialogType === DialogType.DownloadReady ||
dialogType === DialogType.Downloading) dialogType === DialogType.Downloading)
) { ) {
size = `(${formatFileSize(downloadSize, { round: 0 })})`; title += ` (${formatFileSize(downloadSize, { round: 0 })})`;
}
let updateSubText: JSX.Element;
if (dialogType === DialogType.DownloadReady) {
updateSubText = (
<button
className="LeftPaneDialog__action-text"
onClick={startUpdate}
type="button"
>
{i18n('downloadNewVersionMessage')}
</button>
);
} else if (dialogType === DialogType.Downloading) {
const width = Math.ceil(
((downloadedSize || 1) / (downloadSize || 1)) * 100
);
updateSubText = (
<div className="LeftPaneDialog__progress--container">
<div
className="LeftPaneDialog__progress--bar"
style={{ width: `${width}%` }}
/>
</div>
);
} else {
updateSubText = (
<button
className="LeftPaneDialog__action-text"
onClick={startUpdate}
type="button"
>
{i18n('autoUpdateNewVersionMessage')}
</button>
);
} }
const versionTitle = version const versionTitle = version
? i18n('DialogUpdate--version-available', [version]) ? i18n('DialogUpdate--version-available', [version])
: undefined; : undefined;
return ( if (dialogType === DialogType.Downloading) {
<div className="LeftPaneDialog" title={versionTitle}> const width = Math.ceil(
<div className="LeftPaneDialog__container"> ((downloadedSize || 1) / (downloadSize || 1)) * 100
<div className="LeftPaneDialog__icon LeftPaneDialog__icon--update" /> );
<div className="LeftPaneDialog__message">
<h3> return (
{i18n('autoUpdateNewVersionTitle')} {size} <LeftPaneDialog icon="update" title={title} hoverText={versionTitle}>
</h3> <div className="LeftPaneDialog__progress--container">
{updateSubText} <div
</div> className="LeftPaneDialog__progress--bar"
</div> style={{ width: `${width}%` }}
<div className="LeftPaneDialog__container-close">
{dialogType !== DialogType.Downloading && (
<button
aria-label={i18n('close')}
className="LeftPaneDialog__close-button"
onClick={snoozeUpdate}
tabIndex={0}
type="button"
/> />
)} </div>
</div> </LeftPaneDialog>
</div> );
}
let clickLabel: string;
if (dialogType === DialogType.DownloadReady) {
clickLabel = i18n('downloadNewVersionMessage');
} else {
clickLabel = i18n('autoUpdateNewVersionMessage');
}
return (
<LeftPaneDialog
icon="update"
title={title}
hoverText={versionTitle}
hasAction
onClick={startUpdate}
clickLabel={clickLabel}
hasXButton
onClose={snoozeUpdate}
closeLabel={i18n('autoUpdateIgnoreButtonLabel')}
/>
); );
}; };

View file

@ -0,0 +1,149 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactNode } from 'react';
import classNames from 'classnames';
const BASE_CLASS_NAME = 'LeftPaneDialog';
export type PropsType = {
type?: 'warning' | 'error';
icon?: 'update' | 'relink' | 'network' | ReactNode;
title?: string;
subtitle?: string;
children?: ReactNode;
hoverText?: string;
} & (
| {
onClick?: undefined;
clickLabel?: undefined;
hasAction?: false;
}
| {
onClick: () => void;
clickLabel: string;
hasAction: true;
}
) &
(
| {
onClose?: undefined;
closeLabel?: undefined;
hasXButton?: false;
}
| {
onClose: () => void;
closeLabel: string;
hasXButton: true;
}
);
export const LeftPaneDialog: React.FC<PropsType> = ({
icon,
type,
onClick,
clickLabel,
title,
subtitle,
children,
hoverText,
hasAction,
hasXButton,
onClose,
closeLabel,
}) => {
const onClickWrap = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onClick?.();
};
const onCloseWrap = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onClose?.();
};
const iconClassName =
typeof icon === 'string'
? classNames([
`${BASE_CLASS_NAME}__icon`,
`${BASE_CLASS_NAME}__icon--${icon}`,
])
: undefined;
let action: ReactNode;
if (hasAction) {
action = (
<button
title={clickLabel}
aria-label={clickLabel}
className={`${BASE_CLASS_NAME}__action-text`}
onClick={onClickWrap}
tabIndex={0}
type="button"
>
{clickLabel}
</button>
);
}
let xButton: ReactNode;
if (hasXButton) {
xButton = (
<div className={`${BASE_CLASS_NAME}__container-close`}>
<button
title={closeLabel}
aria-label={closeLabel}
className={`${BASE_CLASS_NAME}__close-button`}
onClick={onCloseWrap}
tabIndex={0}
type="button"
/>
</div>
);
}
const className = classNames([
BASE_CLASS_NAME,
type === undefined ? undefined : `${BASE_CLASS_NAME}--${type}`,
]);
const content = (
<>
<div className={`${BASE_CLASS_NAME}__container`}>
{typeof icon === 'string' ? <div className={iconClassName} /> : icon}
<div className={`${BASE_CLASS_NAME}__message`}>
{title === undefined ? undefined : <h3>{title}</h3>}
{subtitle === undefined ? undefined : <div>{subtitle}</div>}
{children}
{action}
</div>
</div>
{xButton}
</>
);
if (onClick) {
return (
<button
className={className}
type="button"
onClick={onClickWrap}
aria-label={clickLabel}
title={hoverText}
tabIndex={0}
>
{content}
</button>
);
}
return (
<div className={className} title={hoverText}>
{content}
</div>
);
};