Move all status/alert dialogs into the Left Pane
This commit is contained in:
parent
101070bf42
commit
18fd44f504
50 changed files with 1298 additions and 607 deletions
21
ts/components/ExpiredBuildDialog.stories.tsx
Normal file
21
ts/components/ExpiredBuildDialog.stories.tsx
Normal 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} />;
|
||||
}
|
||||
);
|
34
ts/components/ExpiredBuildDialog.tsx
Normal file
34
ts/components/ExpiredBuildDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
98
ts/components/NetworkStatus.stories.tsx
Normal file
98
ts/components/NetworkStatus.stories.tsx
Normal 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} />
|
||||
</>
|
||||
));
|
||||
});
|
83
ts/components/NetworkStatus.tsx
Normal file
83
ts/components/NetworkStatus.tsx
Normal 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,
|
||||
});
|
||||
};
|
73
ts/components/UpdateDialog.stories.tsx
Normal file
73
ts/components/UpdateDialog.stories.tsx
Normal 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} />
|
||||
</>
|
||||
));
|
||||
});
|
135
ts/components/UpdateDialog.tsx
Normal file
135
ts/components/UpdateDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue