New option for control over update downloads
This commit is contained in:
parent
80c1ad6ee3
commit
e9308bbafb
49 changed files with 1230 additions and 803 deletions
|
@ -36,6 +36,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
conversationTypeMap,
|
||||
overrideProps.conversationType || 'direct'
|
||||
),
|
||||
hasPendingUpdate: Boolean(overrideProps.hasPendingUpdate),
|
||||
i18n,
|
||||
isMe: true,
|
||||
name: text('name', overrideProps.name || ''),
|
||||
|
@ -47,6 +48,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
profileName: text('profileName', overrideProps.profileName || ''),
|
||||
sharedGroupNames: [],
|
||||
size: 80,
|
||||
startUpdate: action('startUpdate'),
|
||||
style: {},
|
||||
title: text('title', overrideProps.title || ''),
|
||||
});
|
||||
|
@ -83,3 +85,11 @@ stories.add('Phone Number', () => {
|
|||
|
||||
return <AvatarPopup {...props} />;
|
||||
});
|
||||
|
||||
stories.add('Update Available', () => {
|
||||
const props = createProps({
|
||||
hasPendingUpdate: true,
|
||||
});
|
||||
|
||||
return <AvatarPopup {...props} />;
|
||||
});
|
||||
|
|
|
@ -12,6 +12,9 @@ import { LocalizerType } from '../types/Util';
|
|||
export type Props = {
|
||||
readonly i18n: LocalizerType;
|
||||
|
||||
hasPendingUpdate: boolean;
|
||||
startUpdate: () => unknown;
|
||||
|
||||
onEditProfile: () => unknown;
|
||||
onViewPreferences: () => unknown;
|
||||
onViewArchive: () => unknown;
|
||||
|
@ -23,15 +26,17 @@ export type Props = {
|
|||
|
||||
export const AvatarPopup = (props: Props): JSX.Element => {
|
||||
const {
|
||||
hasPendingUpdate,
|
||||
i18n,
|
||||
name,
|
||||
profileName,
|
||||
phoneNumber,
|
||||
title,
|
||||
onEditProfile,
|
||||
onViewPreferences,
|
||||
onViewArchive,
|
||||
onViewPreferences,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
startUpdate,
|
||||
style,
|
||||
title,
|
||||
} = props;
|
||||
|
||||
const shouldShowNumber = Boolean(name || profileName);
|
||||
|
@ -92,6 +97,24 @@ export const AvatarPopup = (props: Props): JSX.Element => {
|
|||
{i18n('avatarMenuViewArchive')}
|
||||
</div>
|
||||
</button>
|
||||
{hasPendingUpdate && (
|
||||
<button
|
||||
type="button"
|
||||
className="module-avatar-popup__item"
|
||||
onClick={startUpdate}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-avatar-popup__item__icon',
|
||||
'module-avatar-popup__item__icon--update'
|
||||
)}
|
||||
/>
|
||||
<div className="module-avatar-popup__item__text">
|
||||
{i18n('avatarMenuUpdateAvailable')}
|
||||
</div>
|
||||
<div className="module-avatar-popup__item--badge" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,17 +5,17 @@ import * as React from 'react';
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
|
||||
import { ExpiredBuildDialog } from './ExpiredBuildDialog';
|
||||
import { DialogExpiredBuild } from './DialogExpiredBuild';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
storiesOf('Components/ExpiredBuildDialog', module).add(
|
||||
'ExpiredBuildDialog',
|
||||
storiesOf('Components/DialogExpiredBuild', module).add(
|
||||
'DialogExpiredBuild',
|
||||
() => {
|
||||
const hasExpired = boolean('hasExpired', true);
|
||||
|
||||
return <ExpiredBuildDialog hasExpired={hasExpired} i18n={i18n} />;
|
||||
return <DialogExpiredBuild hasExpired={hasExpired} i18n={i18n} />;
|
||||
}
|
||||
);
|
|
@ -10,7 +10,7 @@ type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export const ExpiredBuildDialog = ({
|
||||
export const DialogExpiredBuild = ({
|
||||
hasExpired,
|
||||
i18n,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
|
@ -19,19 +19,17 @@ export const ExpiredBuildDialog = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="module-left-pane-dialog module-left-pane-dialog--error">
|
||||
{i18n('expiredWarning')}
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<div className="LeftPaneDialog LeftPaneDialog--error">
|
||||
<div className="LeftPaneDialog__message">
|
||||
{i18n('expiredWarning')}{' '}
|
||||
<a
|
||||
className="module-left-pane-dialog__link"
|
||||
className="LeftPaneDialog__action-text"
|
||||
href="https://signal.org/download/"
|
||||
rel="noreferrer"
|
||||
tabIndex={-1}
|
||||
target="_blank"
|
||||
>
|
||||
<button type="button" className="upgrade">
|
||||
{i18n('upgrade')}
|
||||
</button>
|
||||
{i18n('upgrade')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
69
ts/components/DialogNetworkStatus.stories.tsx
Normal file
69
ts/components/DialogNetworkStatus.stories.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { DialogNetworkStatus } from './DialogNetworkStatus';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
hasNetworkDialog: true,
|
||||
i18n,
|
||||
isOnline: true,
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
manualReconnect: action('manual-reconnect'),
|
||||
withinConnectingGracePeriod: false,
|
||||
challengeStatus: 'idle' as const,
|
||||
};
|
||||
|
||||
const story = storiesOf('Components/DialogNetworkStatus', module);
|
||||
|
||||
story.add('Knobs Playground', () => {
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', true);
|
||||
const isOnline = boolean('isOnline', true);
|
||||
const socketStatus = select(
|
||||
'socketStatus',
|
||||
{
|
||||
CONNECTING: SocketStatus.CONNECTING,
|
||||
OPEN: SocketStatus.OPEN,
|
||||
CLOSING: SocketStatus.CLOSING,
|
||||
CLOSED: SocketStatus.CLOSED,
|
||||
},
|
||||
SocketStatus.CONNECTING
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogNetworkStatus
|
||||
{...defaultProps}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
isOnline={isOnline}
|
||||
socketStatus={socketStatus}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Connecting', () => (
|
||||
<DialogNetworkStatus
|
||||
{...defaultProps}
|
||||
socketStatus={SocketStatus.CONNECTING}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Closing', () => (
|
||||
<DialogNetworkStatus {...defaultProps} socketStatus={SocketStatus.CLOSING} />
|
||||
));
|
||||
|
||||
story.add('Closed', () => (
|
||||
<DialogNetworkStatus {...defaultProps} socketStatus={SocketStatus.CLOSED} />
|
||||
));
|
||||
|
||||
story.add('Offline', () => (
|
||||
<DialogNetworkStatus {...defaultProps} isOnline={false} />
|
||||
));
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { Spinner } from './Spinner';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { NetworkStateType } from '../state/ducks/network';
|
||||
|
@ -16,28 +17,42 @@ export type PropsType = NetworkStateType & {
|
|||
};
|
||||
|
||||
type RenderDialogTypes = {
|
||||
isConnecting?: boolean;
|
||||
title: string;
|
||||
subtext: string;
|
||||
renderActionableButton?: () => JSX.Element;
|
||||
};
|
||||
|
||||
function renderDialog({
|
||||
isConnecting,
|
||||
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">
|
||||
<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>
|
||||
{renderActionableButton && renderActionableButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const NetworkStatus = ({
|
||||
export const DialogNetworkStatus = ({
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
isOnline,
|
||||
|
@ -75,19 +90,23 @@ export const NetworkStatus = ({
|
|||
};
|
||||
|
||||
const manualReconnectButton = (): JSX.Element => (
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<button onClick={reconnect} type="button">
|
||||
{i18n('connect')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="LeftPaneDialog__action-text"
|
||||
onClick={reconnect}
|
||||
type="button"
|
||||
>
|
||||
{i18n('connect')}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (isConnecting) {
|
||||
return renderDialog({
|
||||
isConnecting: true,
|
||||
subtext: i18n('connectingHangOn'),
|
||||
title: i18n('connecting'),
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOnline) {
|
||||
return renderDialog({
|
||||
renderActionableButton: manualReconnectButton,
|
||||
|
@ -114,6 +133,7 @@ export const NetworkStatus = ({
|
|||
}
|
||||
|
||||
return renderDialog({
|
||||
isConnecting: socketStatus === SocketStatus.CONNECTING,
|
||||
renderActionableButton,
|
||||
subtext,
|
||||
title,
|
64
ts/components/DialogUpdate.stories.tsx
Normal file
64
ts/components/DialogUpdate.stories.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { DialogUpdate } from './DialogUpdate';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
dismissDialog: action('dismiss-dialog'),
|
||||
downloadSize: 116504357,
|
||||
downloadedSize: 61003110,
|
||||
hasNetworkDialog: false,
|
||||
i18n,
|
||||
didSnooze: false,
|
||||
showEventsCount: 0,
|
||||
snoozeUpdate: action('snooze-update'),
|
||||
startUpdate: action('start-update'),
|
||||
version: 'v7.7.7',
|
||||
};
|
||||
|
||||
const story = storiesOf('Components/DialogUpdate', module);
|
||||
|
||||
story.add('Knobs Playground', () => {
|
||||
const dialogType = select('dialogType', DialogType, DialogType.Update);
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', false);
|
||||
const didSnooze = boolean('didSnooze', false);
|
||||
|
||||
return (
|
||||
<DialogUpdate
|
||||
{...defaultProps}
|
||||
dialogType={dialogType}
|
||||
didSnooze={didSnooze}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Update', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Update} />
|
||||
));
|
||||
|
||||
story.add('Download Ready', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.DownloadReady} />
|
||||
));
|
||||
|
||||
story.add('Downloading', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Downloading} />
|
||||
));
|
||||
|
||||
story.add('Cannot Update', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Cannot_Update} />
|
||||
));
|
||||
|
||||
story.add('macOS RO Error', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.MacOS_Read_Only} />
|
||||
));
|
179
ts/components/DialogUpdate.tsx
Normal file
179
ts/components/DialogUpdate.tsx
Normal file
|
@ -0,0 +1,179 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import formatFileSize from 'filesize';
|
||||
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { Intl } from './Intl';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
dialogType: DialogType;
|
||||
didSnooze: boolean;
|
||||
dismissDialog: () => void;
|
||||
downloadSize?: number;
|
||||
downloadedSize?: number;
|
||||
hasNetworkDialog: boolean;
|
||||
i18n: LocalizerType;
|
||||
showEventsCount: number;
|
||||
snoozeUpdate: () => void;
|
||||
startUpdate: () => void;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export const DialogUpdate = ({
|
||||
dialogType,
|
||||
didSnooze,
|
||||
dismissDialog,
|
||||
downloadSize,
|
||||
downloadedSize,
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
snoozeUpdate,
|
||||
startUpdate,
|
||||
version,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (hasNetworkDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === DialogType.None) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (didSnooze) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let size: string | undefined;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -45,6 +45,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
title: requiredText('title', overrideProps.title),
|
||||
name: optionalText('name', overrideProps.name),
|
||||
avatarPath: optionalText('avatarPath', overrideProps.avatarPath),
|
||||
hasPendingUpdate: Boolean(overrideProps.hasPendingUpdate),
|
||||
|
||||
i18n,
|
||||
|
||||
|
@ -55,6 +56,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
searchInConversation: action('searchInConversation'),
|
||||
clearConversationSearch: action('clearConversationSearch'),
|
||||
clearSearch: action('clearSearch'),
|
||||
startUpdate: action('startUpdate'),
|
||||
|
||||
showArchivedConversations: action('showArchivedConversations'),
|
||||
startComposing: action('startComposing'),
|
||||
|
@ -115,3 +117,9 @@ story.add('Searching Conversation with Term', () => {
|
|||
|
||||
return <MainHeader {...props} />;
|
||||
});
|
||||
|
||||
story.add('Update Available', () => {
|
||||
const props = createProps({ hasPendingUpdate: true });
|
||||
|
||||
return <MainHeader {...props} />;
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ export type PropsType = {
|
|||
profileName?: string;
|
||||
title: string;
|
||||
avatarPath?: string;
|
||||
hasPendingUpdate: boolean;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
||||
|
@ -59,6 +60,7 @@ export type PropsType = {
|
|||
noteToSelf: string;
|
||||
}
|
||||
) => void;
|
||||
startUpdate: () => unknown;
|
||||
clearConversationSearch: () => void;
|
||||
clearSearch: () => void;
|
||||
|
||||
|
@ -342,16 +344,18 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
avatarPath,
|
||||
color,
|
||||
disabled,
|
||||
hasPendingUpdate,
|
||||
i18n,
|
||||
name,
|
||||
startComposing,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
searchTerm,
|
||||
showArchivedConversations,
|
||||
startComposing,
|
||||
startUpdate,
|
||||
title,
|
||||
toggleProfileEditor,
|
||||
} = this.props;
|
||||
const { showingAvatarPopup, popperRoot } = this.state;
|
||||
|
@ -369,25 +373,30 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPath={avatarPath}
|
||||
className="module-main-header__avatar"
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
// `sharedGroupNames` makes no sense for yourself, but `<Avatar>` needs it
|
||||
// to determine blurring.
|
||||
sharedGroupNames={[]}
|
||||
size={28}
|
||||
innerRef={ref}
|
||||
onClick={this.showAvatarPopup}
|
||||
/>
|
||||
<div className="module-main-header__avatar--container">
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPath={avatarPath}
|
||||
className="module-main-header__avatar"
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
// `sharedGroupNames` makes no sense for yourself, but
|
||||
// `<Avatar>` needs it to determine blurring.
|
||||
sharedGroupNames={[]}
|
||||
size={28}
|
||||
innerRef={ref}
|
||||
onClick={this.showAvatarPopup}
|
||||
/>
|
||||
{hasPendingUpdate && (
|
||||
<div className="module-main-header__avatar--badged" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
{showingAvatarPopup && popperRoot
|
||||
|
@ -408,6 +417,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
title={title}
|
||||
avatarPath={avatarPath}
|
||||
size={28}
|
||||
hasPendingUpdate={hasPendingUpdate}
|
||||
startUpdate={startUpdate}
|
||||
// See the comment above about `sharedGroupNames`.
|
||||
sharedGroupNames={[]}
|
||||
onEditProfile={() => {
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { NetworkStatus } from './NetworkStatus';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
hasNetworkDialog: true,
|
||||
i18n,
|
||||
isOnline: true,
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
manualReconnect: action('manual-reconnect'),
|
||||
withinConnectingGracePeriod: false,
|
||||
challengeStatus: 'idle' as const,
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Connecting',
|
||||
props: {
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Closing (online)',
|
||||
props: {
|
||||
socketStatus: SocketStatus.CLOSING,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Closed (online)',
|
||||
props: {
|
||||
socketStatus: SocketStatus.CLOSED,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Offline',
|
||||
props: {
|
||||
isOnline: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
storiesOf('Components/NetworkStatus', module)
|
||||
.add('Knobs Playground', () => {
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', true);
|
||||
const isOnline = boolean('isOnline', true);
|
||||
const socketStatus = select(
|
||||
'socketStatus',
|
||||
{
|
||||
CONNECTING: SocketStatus.CONNECTING,
|
||||
OPEN: SocketStatus.OPEN,
|
||||
CLOSING: SocketStatus.CLOSING,
|
||||
CLOSED: SocketStatus.CLOSED,
|
||||
},
|
||||
SocketStatus.CONNECTING
|
||||
);
|
||||
|
||||
return (
|
||||
<NetworkStatus
|
||||
{...defaultProps}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
isOnline={isOnline}
|
||||
socketStatus={socketStatus}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<NetworkStatus {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
|
@ -69,6 +69,7 @@ const createProps = (): PropsType => ({
|
|||
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
||||
deviceName: 'Work Windows ME',
|
||||
hasAudioNotifications: true,
|
||||
hasAutoDownloadUpdate: true,
|
||||
hasAutoLaunch: true,
|
||||
hasCallNotifications: true,
|
||||
hasCallRingtoneNotification: false,
|
||||
|
@ -125,6 +126,7 @@ const createProps = (): PropsType => ({
|
|||
isSystemTraySupported: true,
|
||||
|
||||
onAudioNotificationsChange: action('onAudioNotificationsChange'),
|
||||
onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
|
||||
onAutoLaunchChange: action('onAutoLaunchChange'),
|
||||
onCallNotificationsChange: action('onCallNotificationsChange'),
|
||||
onCallRingtoneNotificationChange: action('onCallRingtoneNotificationChange'),
|
||||
|
|
|
@ -43,6 +43,7 @@ export type PropsType = {
|
|||
defaultConversationColor: DefaultConversationColorType;
|
||||
deviceName?: string;
|
||||
hasAudioNotifications?: boolean;
|
||||
hasAutoDownloadUpdate: boolean;
|
||||
hasAutoLaunch: boolean;
|
||||
hasCallNotifications: boolean;
|
||||
hasCallRingtoneNotification: boolean;
|
||||
|
@ -104,6 +105,7 @@ export type PropsType = {
|
|||
|
||||
// Change handlers
|
||||
onAudioNotificationsChange: CheckboxChangeHandlerType;
|
||||
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
|
||||
onAutoLaunchChange: CheckboxChangeHandlerType;
|
||||
onCallNotificationsChange: CheckboxChangeHandlerType;
|
||||
onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
|
||||
|
@ -161,6 +163,7 @@ export const Preferences = ({
|
|||
editCustomColor,
|
||||
getConversationsWithCustomColor,
|
||||
hasAudioNotifications,
|
||||
hasAutoDownloadUpdate,
|
||||
hasAutoLaunch,
|
||||
hasCallNotifications,
|
||||
hasCallRingtoneNotification,
|
||||
|
@ -191,6 +194,7 @@ export const Preferences = ({
|
|||
makeSyncRequest,
|
||||
notificationContent,
|
||||
onAudioNotificationsChange,
|
||||
onAutoDownloadUpdateChange,
|
||||
onAutoLaunchChange,
|
||||
onCallNotificationsChange,
|
||||
onCallRingtoneNotificationChange,
|
||||
|
@ -340,6 +344,15 @@ export const Preferences = ({
|
|||
onChange={onMediaCameraPermissionsChange}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow title={i18n('Preferences--updates')}>
|
||||
<Checkbox
|
||||
checked={hasAutoDownloadUpdate}
|
||||
label={i18n('Preferences__download-update')}
|
||||
moduleClassName="Preferences__checkbox"
|
||||
name="autoDownloadUpdate"
|
||||
onChange={onAutoDownloadUpdateChange}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
} else if (page === Page.Appearance) {
|
||||
|
|
|
@ -21,12 +21,12 @@ export const RelinkDialog = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
|
||||
<div className="module-left-pane-dialog__message">
|
||||
<div className="LeftPaneDialog LeftPaneDialog--warning">
|
||||
<div className="LeftPaneDialog__message">
|
||||
<h3>{i18n('unlinked')}</h3>
|
||||
<span>{i18n('unlinkedWarning')}</span>
|
||||
</div>
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<div className="LeftPaneDialog__actions">
|
||||
<button onClick={relinkDevice} type="button">
|
||||
{i18n('relink')}
|
||||
</button>
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { UpdateDialog } from './UpdateDialog';
|
||||
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
ackRender: action('ack-render'),
|
||||
dismissDialog: action('dismiss-dialog'),
|
||||
hasNetworkDialog: false,
|
||||
i18n,
|
||||
didSnooze: false,
|
||||
showEventsCount: 0,
|
||||
snoozeUpdate: action('snooze-update'),
|
||||
startUpdate: action('start-update'),
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Update',
|
||||
props: {
|
||||
dialogType: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Update (didSnooze=true)',
|
||||
props: {
|
||||
dialogType: 1,
|
||||
didSnooze: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
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);
|
||||
const didSnooze = boolean('didSnooze', false);
|
||||
|
||||
return (
|
||||
<UpdateDialog
|
||||
{...defaultProps}
|
||||
dialogType={dialogType}
|
||||
didSnooze={didSnooze}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<UpdateDialog {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
|
@ -1,117 +0,0 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Dialogs } from '../types/Dialogs';
|
||||
import { Intl } from './Intl';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
ackRender: () => void;
|
||||
dialogType: Dialogs;
|
||||
didSnooze: boolean;
|
||||
dismissDialog: () => void;
|
||||
hasNetworkDialog: boolean;
|
||||
i18n: LocalizerType;
|
||||
showEventsCount: number;
|
||||
snoozeUpdate: () => void;
|
||||
startUpdate: () => void;
|
||||
};
|
||||
|
||||
export const UpdateDialog = ({
|
||||
ackRender,
|
||||
dialogType,
|
||||
didSnooze,
|
||||
dismissDialog,
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
snoozeUpdate,
|
||||
startUpdate,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
React.useEffect(() => {
|
||||
ackRender();
|
||||
});
|
||||
|
||||
if (hasNetworkDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === Dialogs.None) {
|
||||
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={{
|
||||
app: <strong key="app">Signal.app</strong>,
|
||||
folder: <strong key="folder">/Applications</strong>,
|
||||
}}
|
||||
i18n={i18n}
|
||||
id="readOnlyVolume"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<button type="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">
|
||||
{!didSnooze && (
|
||||
<button
|
||||
type="button"
|
||||
className="module-left-pane-dialog__button--no-border"
|
||||
onClick={snoozeUpdate}
|
||||
>
|
||||
{i18n('autoUpdateLaterButtonLabel')}
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={startUpdate}>
|
||||
{i18n('autoUpdateRestartButtonLabel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
75
ts/components/WhatsNew.tsx
Normal file
75
ts/components/WhatsNew.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Modal } from './Modal';
|
||||
import { Intl } from './Intl';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type ReleaseNotesType = {
|
||||
date: Date;
|
||||
version: string;
|
||||
features: Array<string>;
|
||||
};
|
||||
|
||||
export const WhatsNew = ({ i18n }: PropsType): JSX.Element => {
|
||||
const [releaseNotes, setReleaseNotes] = useState<
|
||||
ReleaseNotesType | undefined
|
||||
>();
|
||||
|
||||
const viewReleaseNotes = () => {
|
||||
setReleaseNotes({
|
||||
date: new Date('08/17/2021'),
|
||||
version: window.getVersion(),
|
||||
features: [
|
||||
'WhatsNew__v5.15--1',
|
||||
'WhatsNew__v5.15--2',
|
||||
'WhatsNew__v5.15--3',
|
||||
'WhatsNew__v5.15--4',
|
||||
'WhatsNew__v5.15--5',
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{releaseNotes && (
|
||||
<Modal
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={() => setReleaseNotes(undefined)}
|
||||
title={i18n('WhatsNew__modal-title')}
|
||||
>
|
||||
<>
|
||||
<span>
|
||||
{moment(releaseNotes.date).format('LL')} ·{' '}
|
||||
{releaseNotes.version}
|
||||
</span>
|
||||
<ul>
|
||||
{releaseNotes.features.map(featureKey => (
|
||||
<li key={featureKey}>
|
||||
<Intl i18n={i18n} id={featureKey} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
</Modal>
|
||||
)}
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="whatsNew"
|
||||
components={[
|
||||
<button className="WhatsNew" type="button" onClick={viewReleaseNotes}>
|
||||
{i18n('viewReleaseNotes')}
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue