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": {
"message": "Later"
},
"autoUpdateIgnoreButtonLabel": {
"message": "Ignore update"
},
"leftTheGroup": {
"message": "$name$ left the group.",
"description": "Shown in the conversation history when a single person leaves the group",

View File

@ -8,12 +8,17 @@
}
.LeftPaneDialog {
@include button-reset;
align-items: center;
background: $color-ultramarine;
color: $color-white;
display: flex;
width: 100%;
min-height: 64px;
padding: 12px 18px;
user-select: none;
cursor: inherit;
&__container {
display: flex;
@ -46,10 +51,11 @@
}
&__icon {
width: 20px;
height: 20px;
width: 24px;
height: 24px;
margin-right: 18px;
background-color: $color-white;
-webkit-mask-size: contain;
&--relink {
-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 { LeftPaneDialog } from './LeftPaneDialog';
type PropsType = {
hasExpired: boolean;
i18n: LocalizerType;
@ -19,19 +21,17 @@ export const DialogExpiredBuild = ({
}
return (
<div className="LeftPaneDialog LeftPaneDialog--error">
<div className="LeftPaneDialog__message">
{i18n('expiredWarning')}{' '}
<a
className="LeftPaneDialog__action-text"
href="https://signal.org/download/"
rel="noreferrer"
tabIndex={-1}
target="_blank"
>
{i18n('upgrade')}
</a>
</div>
</div>
<LeftPaneDialog type="error">
{i18n('expiredWarning')}{' '}
<a
className="LeftPaneDialog__action-text"
href="https://signal.org/download/"
rel="noreferrer"
tabIndex={-1}
target="_blank"
>
{i18n('upgrade')}
</a>
</LeftPaneDialog>
);
};

View File

@ -1,8 +1,9 @@
// Copyright 2020-2021 Signal Messenger, LLC
// 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 { LocalizerType } from '../types/Util';
import { SocketStatus } from '../types/SocketStatus';
@ -16,42 +17,6 @@ export type PropsType = NetworkStateType & {
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 = ({
hasNetworkDialog,
i18n,
@ -59,8 +24,10 @@ export const DialogNetworkStatus = ({
socketStatus,
manualReconnect,
}: PropsType): JSX.Element | null => {
const [isConnecting, setIsConnecting] = React.useState<boolean>(false);
React.useEffect(() => {
const [isConnecting, setIsConnecting] = React.useState<boolean>(
socketStatus === SocketStatus.CONNECTING
);
useEffect(() => {
if (!hasNetworkDialog) {
return () => null;
}
@ -80,62 +47,46 @@ export const DialogNetworkStatus = ({
};
}, [hasNetworkDialog, isConnecting, setIsConnecting]);
if (!hasNetworkDialog) {
return null;
}
const reconnect = () => {
setIsConnecting(true);
manualReconnect();
};
const manualReconnectButton = (): JSX.Element => (
<button
className="LeftPaneDialog__action-text"
onClick={reconnect}
type="button"
>
{i18n('connect')}
</button>
);
if (!hasNetworkDialog) {
return null;
}
if (isConnecting) {
return renderDialog({
isConnecting: true,
subtext: i18n('connectingHangOn'),
title: i18n('connecting'),
});
const spinner = (
<div className="LeftPaneDialog__spinner-container">
<Spinner
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 renderDialog({
renderActionableButton: manualReconnectButton,
subtext: i18n('checkNetworkConnection'),
title: i18n('offline'),
});
}
let subtext = '';
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,
});
return (
<LeftPaneDialog
type="warning"
icon="network"
title={isOnline ? i18n('disconnected') : i18n('offline')}
subtitle={i18n('checkNetworkConnection')}
hasAction
clickLabel={i18n('connect')}
onClick={reconnect}
/>
);
};

View File

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

View File

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