Move all status/alert dialogs into the Left Pane

This commit is contained in:
Josh Perez 2020-02-12 13:30:58 -08:00 committed by GitHub
parent 101070bf42
commit 18fd44f504
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1298 additions and 607 deletions

View file

@ -0,0 +1,21 @@
import * as React from 'react';
import { ExpiredBuildDialog } from './ExpiredBuildDialog';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs';
const i18n = setupI18n('en', enMessages);
storiesOf('Components/ExpiredBuildDialog', module).add(
'ExpiredBuildDialog',
() => {
const hasExpired = boolean('hasExpired', true);
return <ExpiredBuildDialog hasExpired={hasExpired} i18n={i18n} />;
}
);

View file

@ -0,0 +1,34 @@
import React from 'react';
import { LocalizerType } from '../types/Util';
interface PropsType {
hasExpired: boolean;
i18n: LocalizerType;
}
export const ExpiredBuildDialog = ({
hasExpired,
i18n,
}: PropsType): JSX.Element | null => {
if (!hasExpired) {
return null;
}
return (
<div className="module-left-pane-dialog module-left-pane-dialog--error">
{i18n('expiredWarning')}
<div className="module-left-pane-dialog__actions">
<a
className="module-left-pane-dialog__link"
href="https://signal.org/download/"
rel="noreferrer"
tabIndex={-1}
target="_blank"
>
<button className="upgrade">{i18n('upgrade')}</button>
</a>
</div>
</div>
);
};

View file

@ -32,8 +32,11 @@ export interface PropsType {
showInbox: () => void;
// Render Props
renderExpiredBuildDialog: () => JSX.Element;
renderMainHeader: () => JSX.Element;
renderMessageSearchResult: (id: string) => JSX.Element;
renderNetworkStatus: () => JSX.Element;
renderUpdateDialog: () => JSX.Element;
}
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
@ -378,13 +381,22 @@ export class LeftPane extends React.Component<PropsType> {
};
public render(): JSX.Element {
const { renderMainHeader, showArchived } = this.props;
const {
renderExpiredBuildDialog,
renderMainHeader,
renderNetworkStatus,
renderUpdateDialog,
showArchived,
} = this.props;
return (
<div className="module-left-pane">
<div className="module-left-pane__header">
{showArchived ? this.renderArchivedHeader() : renderMainHeader()}
</div>
{renderExpiredBuildDialog()}
{renderNetworkStatus()}
{renderUpdateDialog()}
{this.renderList()}
</div>
);

View file

@ -0,0 +1,98 @@
import * as React from 'react';
import { NetworkStatus } from './NetworkStatus';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react';
import { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
hasNetworkDialog: true,
i18n,
isOnline: true,
isRegistrationDone: true,
socketStatus: 0,
relinkDevice: action('relink-device'),
withinConnectingGracePeriod: false,
};
const permutations = [
{
title: 'Connecting',
props: {
socketStatus: 0,
},
},
{
title: 'Closing (online)',
props: {
socketStatus: 2,
},
},
{
title: 'Closed (online)',
props: {
socketStatus: 3,
},
},
{
title: 'Offline',
props: {
isOnline: false,
},
},
{
title: 'Unlinked (online)',
props: {
isRegistrationDone: false,
},
},
{
title: 'Unlinked (offline)',
props: {
isOnline: false,
isRegistrationDone: false,
},
},
];
storiesOf('Components/NetworkStatus', module)
.add('Knobs Playground', () => {
const hasNetworkDialog = boolean('hasNetworkDialog', true);
const isOnline = boolean('isOnline', true);
const isRegistrationDone = boolean('isRegistrationDone', true);
const socketStatus = select(
'socketStatus',
{
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
},
0
);
return (
<NetworkStatus
{...defaultProps}
hasNetworkDialog={hasNetworkDialog}
isOnline={isOnline}
isRegistrationDone={isRegistrationDone}
socketStatus={socketStatus}
/>
);
})
.add('Iterations', () => {
return permutations.map(({ props, title }) => (
<>
<h3>{title}</h3>
<NetworkStatus {...defaultProps} {...props} />
</>
));
});

View file

@ -0,0 +1,83 @@
import React from 'react';
import { LocalizerType } from '../types/Util';
import { NetworkStateType } from '../state/ducks/network';
export interface PropsType extends NetworkStateType {
hasNetworkDialog: boolean;
i18n: LocalizerType;
isRegistrationDone: boolean;
relinkDevice: () => void;
}
type RenderDialogTypes = {
title: string;
subtext: string;
renderActionableButton?: () => JSX.Element;
};
function renderDialog({
title,
subtext,
renderActionableButton,
}: RenderDialogTypes): JSX.Element {
return (
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
<div className="module-left-pane-dialog__message">
<h3>{title}</h3>
<span>{subtext}</span>
</div>
{renderActionableButton && renderActionableButton()}
</div>
);
}
export const NetworkStatus = ({
hasNetworkDialog,
i18n,
isOnline,
isRegistrationDone,
socketStatus,
relinkDevice,
}: PropsType): JSX.Element | null => {
if (!hasNetworkDialog) {
return null;
}
if (!isOnline) {
return renderDialog({
subtext: i18n('checkNetworkConnection'),
title: i18n('offline'),
});
} else if (!isRegistrationDone) {
return renderDialog({
renderActionableButton: (): JSX.Element => (
<div className="module-left-pane-dialog__actions">
<button onClick={relinkDevice}>{i18n('relink')}</button>
</div>
),
subtext: i18n('unlinkedWarning'),
title: i18n('unlinked'),
});
}
let subtext = '';
let title = '';
switch (socketStatus) {
case WebSocket.CONNECTING:
subtext = i18n('connectingHangOn');
title = i18n('connecting');
break;
case WebSocket.CLOSED:
case WebSocket.CLOSING:
default:
title = i18n('disconnected');
subtext = i18n('checkNetworkConnection');
}
return renderDialog({
subtext,
title,
});
};

View file

@ -0,0 +1,73 @@
import * as React from 'react';
import { UpdateDialog } from './UpdateDialog';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react';
import { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
ackRender: action('ack-render'),
dismissDialog: action('dismiss-dialog'),
hasNetworkDialog: false,
i18n,
startUpdate: action('start-update'),
};
const permutations = [
{
title: 'Update',
props: {
dialogType: 1,
},
},
{
title: 'Cannot Update',
props: {
dialogType: 2,
},
},
{
title: 'MacOS Read Only Error',
props: {
dialogType: 3,
},
},
];
storiesOf('Components/UpdateDialog', module)
.add('Knobs Playground', () => {
const dialogType = select(
'dialogType',
{
None: 0,
Update: 1,
Cannot_Update: 2,
MacOS_Read_Only: 3,
},
1
);
const hasNetworkDialog = boolean('hasNetworkDialog', false);
return (
<UpdateDialog
{...defaultProps}
dialogType={dialogType}
hasNetworkDialog={hasNetworkDialog}
/>
);
})
.add('Iterations', () => {
return permutations.map(({ props, title }) => (
<>
<h3>{title}</h3>
<UpdateDialog {...defaultProps} {...props} />
</>
));
});

View file

@ -0,0 +1,135 @@
import React from 'react';
import moment from 'moment';
import { Dialogs } from '../types/Dialogs';
import { Intl } from './Intl';
import { LocalizerType } from '../types/Util';
export interface PropsType {
ackRender: () => void;
dialogType: Dialogs;
dismissDialog: () => void;
hasNetworkDialog: boolean;
i18n: LocalizerType;
startUpdate: () => void;
}
type MaybeMoment = moment.Moment | null;
type ReactSnoozeHook = React.Dispatch<React.SetStateAction<MaybeMoment>>;
const SNOOZE_TIMER = 60 * 1000 * 30;
function handleSnooze(setSnoozeForLater: ReactSnoozeHook) {
setSnoozeForLater(moment().add(SNOOZE_TIMER));
setTimeout(() => {
setSnoozeForLater(moment());
}, SNOOZE_TIMER);
}
function canSnooze(snoozeUntil: MaybeMoment) {
return snoozeUntil === null;
}
function isSnoozed(snoozeUntil: MaybeMoment) {
if (snoozeUntil === null) {
return false;
}
return moment().isBefore(snoozeUntil);
}
export const UpdateDialog = ({
ackRender,
dialogType,
dismissDialog,
hasNetworkDialog,
i18n,
startUpdate,
}: PropsType): JSX.Element | null => {
const [snoozeUntil, setSnoozeForLater] = React.useState<MaybeMoment>(null);
React.useEffect(() => {
ackRender();
});
if (hasNetworkDialog) {
return null;
}
if (dialogType === Dialogs.None || isSnoozed(snoozeUntil)) {
return null;
}
if (dialogType === Dialogs.Cannot_Update) {
return (
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
<div className="module-left-pane-dialog__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>
);
}
if (dialogType === Dialogs.MacOS_Read_Only) {
return (
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
<div className="module-left-pane-dialog__message">
<h3>{i18n('cannotUpdate')}</h3>
<span>
<Intl
components={[
<strong key="app">Signal.app</strong>,
<strong key="folder">/Applications</strong>,
]}
i18n={i18n}
id="readOnlyVolume"
/>
</span>
</div>
<div className="module-left-pane-dialog__actions">
<button onClick={dismissDialog}>{i18n('ok')}</button>
</div>
</div>
);
}
return (
<div className="module-left-pane-dialog">
<div className="module-left-pane-dialog__message">
<h3>{i18n('autoUpdateNewVersionTitle')}</h3>
<span>{i18n('autoUpdateNewVersionMessage')}</span>
</div>
<div className="module-left-pane-dialog__actions">
{canSnooze(snoozeUntil) && (
<button
className="module-left-pane-dialog__button--no-border"
onClick={() => {
handleSnooze(setSnoozeForLater);
}}
>
{i18n('autoUpdateLaterButtonLabel')}
</button>
)}
<button onClick={startUpdate}>
{i18n('autoUpdateRestartButtonLabel')}
</button>
</div>
</div>
);
};