Modals redesign and SessionToast improvements (#713)
Modals redesign and SessionToast improvementspull/737/head
commit
ea0ab2c7cc
@ -0,0 +1,54 @@
|
||||
/* global Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.SessionConfirmView = Whisper.View.extend({
|
||||
initialize(options) {
|
||||
this.props = {
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
onClickOk: this.ok.bind(this),
|
||||
onClickClose: this.cancel.bind(this),
|
||||
resolve: options.resolve,
|
||||
reject: options.reject,
|
||||
okText: options.okText,
|
||||
cancelText: options.cancelText,
|
||||
hideCancel: options.hideCancel,
|
||||
};
|
||||
},
|
||||
|
||||
render() {
|
||||
this.$('.session-confirm-wrapper').remove();
|
||||
|
||||
this.confirmView = new Whisper.ReactWrapperView({
|
||||
className: 'session-confirm-wrapper',
|
||||
Component: window.Signal.Components.SessionConfirm,
|
||||
props: this.props,
|
||||
});
|
||||
|
||||
this.$el.append(this.confirmView.el);
|
||||
},
|
||||
|
||||
ok() {
|
||||
this.$('.session-confirm-wrapper').remove();
|
||||
if (this.props.resolve) {
|
||||
this.props.resolve();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.$('.session-confirm-wrapper').remove();
|
||||
if (this.props.reject) {
|
||||
this.props.reject();
|
||||
}
|
||||
},
|
||||
onKeyup(event) {
|
||||
if (event.key === 'Escape' || event.key === 'Esc') {
|
||||
this.cancel();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
@ -0,0 +1,256 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SessionModal } from './session/SessionModal';
|
||||
import { SessionButton } from './session/SessionButton';
|
||||
import { SessionSpinner } from './session/SessionSpinner';
|
||||
|
||||
interface Props {
|
||||
i18n: any;
|
||||
onClose: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
title: string;
|
||||
error: string | null;
|
||||
connecting: boolean;
|
||||
success: boolean;
|
||||
view: 'connecting' | 'default';
|
||||
serverURL: string;
|
||||
}
|
||||
|
||||
export class AddServerDialog extends React.Component<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
title: this.props.i18n('addServerDialogTitle'),
|
||||
error: null,
|
||||
connecting: false,
|
||||
success: false,
|
||||
view: 'default',
|
||||
serverURL: '',
|
||||
};
|
||||
|
||||
this.showError = this.showError.bind(this);
|
||||
this.showView = this.showView.bind(this);
|
||||
this.attemptConnection = this.attemptConnection.bind(this);
|
||||
|
||||
this.closeDialog = this.closeDialog.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { i18n } = this.props;
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
title={this.state.title}
|
||||
onOk={() => null}
|
||||
onClose={this.closeDialog}
|
||||
>
|
||||
{this.state.view === 'default' && (
|
||||
<>
|
||||
<div className="spacer-lg" />
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="server-url"
|
||||
placeholder={i18n('serverURL')}
|
||||
defaultValue={this.state.serverURL}
|
||||
/>
|
||||
<div className="spacer-sm" />
|
||||
|
||||
{this.showError()}
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton
|
||||
text={i18n('connect')}
|
||||
onClick={() => this.showView('connecting')}
|
||||
/>
|
||||
|
||||
<SessionButton text={i18n('cancel')} onClick={this.closeDialog} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{this.state.view === 'connecting' && (
|
||||
<>
|
||||
<div className="session-modal__centered">
|
||||
<div className="spacer-lg" />
|
||||
<SessionSpinner />
|
||||
<div className="spacer-lg" />
|
||||
</div>
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton
|
||||
text={i18n('cancel')}
|
||||
onClick={() => this.showView('default')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SessionModal>
|
||||
);
|
||||
}
|
||||
|
||||
private showView(view: 'default' | 'connecting', error?: string) {
|
||||
const { i18n } = this.props;
|
||||
|
||||
const isDefaultView = view === 'default';
|
||||
const isConnectingView = view === 'connecting';
|
||||
|
||||
if (isDefaultView) {
|
||||
this.setState({
|
||||
title: i18n('addServerDialogTitle'),
|
||||
error: error || null,
|
||||
view: 'default',
|
||||
connecting: false,
|
||||
success: false,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isConnectingView) {
|
||||
// TODO: Make this not hard coded
|
||||
const channelId = 1;
|
||||
const serverURL = String(
|
||||
$('.session-modal #server-url').val()
|
||||
).toLowerCase();
|
||||
|
||||
const serverURLExists = serverURL.length > 0;
|
||||
|
||||
if (!serverURLExists) {
|
||||
this.setState({
|
||||
error: i18n('noServerURL'),
|
||||
view: 'default',
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
title: i18n('connectingLoad'),
|
||||
serverURL: serverURL,
|
||||
view: 'connecting',
|
||||
connecting: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const connectionResult = this.attemptConnection(serverURL, channelId);
|
||||
|
||||
// Give 5s maximum for promise to revole. Else, throw error.
|
||||
const maxConnectionDuration = 5000;
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
if (!this.state.success) {
|
||||
this.showView('default', i18n('connectToServerFail'));
|
||||
|
||||
return;
|
||||
}
|
||||
}, maxConnectionDuration);
|
||||
|
||||
connectionResult
|
||||
.then(() => {
|
||||
clearTimeout(connectionTimeout);
|
||||
|
||||
if (this.state.connecting) {
|
||||
this.setState({
|
||||
success: true,
|
||||
});
|
||||
window.pushToast({
|
||||
title: i18n('connectToServerSuccess'),
|
||||
id: 'connectToServerSuccess',
|
||||
type: 'success',
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
})
|
||||
.catch((connectionError: string) => {
|
||||
clearTimeout(connectionTimeout);
|
||||
this.showView('default', connectionError);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private showError() {
|
||||
const message = this.state.error;
|
||||
|
||||
return (
|
||||
<>
|
||||
{message && (
|
||||
<>
|
||||
<div className="session-label danger">{message}</div>
|
||||
<div className="spacer-lg" />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onKeyUp(event: any) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
if (this.state.view === 'default') {
|
||||
this.showView('connecting');
|
||||
}
|
||||
break;
|
||||
case 'Esc':
|
||||
case 'Escape':
|
||||
this.closeDialog();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
private closeDialog() {
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
private async attemptConnection(serverURL: string, channelId: number) {
|
||||
const { i18n } = this.props;
|
||||
|
||||
const rawserverURL = serverURL
|
||||
.replace(/^https?:\/\//i, '')
|
||||
.replace(/[/\\]+$/i, '');
|
||||
const sslserverURL = `https://${rawserverURL}`;
|
||||
const conversationId = `publicChat:${channelId}@${rawserverURL}`;
|
||||
|
||||
const conversationExists = window.ConversationController.get(
|
||||
conversationId
|
||||
);
|
||||
if (conversationExists) {
|
||||
// We are already a member of this public chat
|
||||
return new Promise((_resolve, reject) => {
|
||||
reject(i18n('publicChatExists'));
|
||||
});
|
||||
}
|
||||
|
||||
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(
|
||||
sslserverURL
|
||||
);
|
||||
if (!serverAPI) {
|
||||
// Url incorrect or server not compatible
|
||||
return new Promise((_resolve, reject) => {
|
||||
reject(i18n('connectToServerFail'));
|
||||
});
|
||||
}
|
||||
|
||||
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||
conversationId,
|
||||
'group'
|
||||
);
|
||||
|
||||
await serverAPI.findOrCreateChannel(channelId, conversationId);
|
||||
await conversation.setPublicSource(sslserverURL, channelId);
|
||||
await conversation.setFriendRequestStatus(
|
||||
window.friends.friendRequestStatusEnum.friends
|
||||
);
|
||||
|
||||
return conversation;
|
||||
}
|
||||
}
|
@ -0,0 +1,273 @@
|
||||
import React from 'react';
|
||||
import { QRCode } from 'react-qrcode';
|
||||
|
||||
import { SessionModal } from './session/SessionModal';
|
||||
import { SessionButton } from './session/SessionButton';
|
||||
|
||||
interface Props {
|
||||
i18n: any;
|
||||
onClose: any;
|
||||
pubKeyToUnpair: string | null;
|
||||
pubKey: string | null;
|
||||
}
|
||||
|
||||
interface State {
|
||||
currentPubKey: string | null;
|
||||
accepted: boolean;
|
||||
isListening: boolean;
|
||||
success: boolean;
|
||||
loading: boolean;
|
||||
view:
|
||||
| 'default'
|
||||
| 'waitingForRequest'
|
||||
| 'requestReceived'
|
||||
| 'requestAccepted'
|
||||
| 'confirmUnpair';
|
||||
pubKeyRequests: Array<any>;
|
||||
data: Array<any>;
|
||||
}
|
||||
|
||||
export class DevicePairingDialog extends React.Component<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.closeDialog = this.closeDialog.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
this.startReceivingRequests = this.startReceivingRequests.bind(this);
|
||||
this.stopReceivingRequests = this.stopReceivingRequests.bind(this);
|
||||
this.getPubkeyName = this.getPubkeyName.bind(this);
|
||||
|
||||
this.state = {
|
||||
currentPubKey: this.props.pubKey,
|
||||
accepted: false,
|
||||
isListening: false,
|
||||
success: false,
|
||||
loading: true,
|
||||
view: 'default',
|
||||
pubKeyRequests: [],
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.getSecondaryDevices();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { i18n } = this.props;
|
||||
|
||||
const waitingForRequest = this.state.view === 'waitingForRequest';
|
||||
const nothingPaired = this.state.data.length === 0;
|
||||
|
||||
const renderPairedDevices = this.state.data.map((pubKey: any) => {
|
||||
const pubKeyInfo = this.getPubkeyName(pubKey);
|
||||
const isFinalItem =
|
||||
this.state.data[this.state.data.length - 1] === pubKey;
|
||||
|
||||
return (
|
||||
<div key={pubKey}>
|
||||
<p>
|
||||
{pubKeyInfo.deviceAlias}
|
||||
<br />
|
||||
<span className="text-subtle">Pairing Secret:</span>{' '}
|
||||
{pubKeyInfo.secretWords}
|
||||
</p>
|
||||
{!isFinalItem ? <hr className="text-soft fullwidth" /> : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!this.state.loading && (
|
||||
<SessionModal
|
||||
title={i18n('pairedDevices')}
|
||||
onOk={() => null}
|
||||
onClose={this.closeDialog}
|
||||
>
|
||||
{waitingForRequest ? (
|
||||
<div className="session-modal__centered">
|
||||
<h3>{i18n('waitingForDeviceToRegister')}</h3>
|
||||
<small className="text-subtle">
|
||||
{i18n('pairNewDevicePrompt')}
|
||||
</small>
|
||||
<div className="spacer-lg" />
|
||||
|
||||
<div id="qr">
|
||||
<QRCode value={window.textsecure.storage.user.getNumber()} />
|
||||
</div>
|
||||
|
||||
<div className="spacer-lg" />
|
||||
<div className="session-modal__button-group__center">
|
||||
<SessionButton
|
||||
text={i18n('cancel')}
|
||||
onClick={this.stopReceivingRequests}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{nothingPaired ? (
|
||||
<div className="session-modal__centered">
|
||||
<div>{i18n('noPairedDevices')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="session-modal__centered">
|
||||
{renderPairedDevices}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="spacer-lg" />
|
||||
<div className="session-modal__button-group__center">
|
||||
<SessionButton
|
||||
text={i18n('pairNewDevice')}
|
||||
onClick={this.startReceivingRequests}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SessionModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private showView(
|
||||
view?:
|
||||
| 'default'
|
||||
| 'waitingForRequest'
|
||||
| 'requestReceived'
|
||||
| 'requestAccepted'
|
||||
| 'confirmUnpair'
|
||||
) {
|
||||
if (!view) {
|
||||
this.setState({
|
||||
view: 'default',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === 'waitingForRequest') {
|
||||
this.setState({
|
||||
view,
|
||||
isListening: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
this.setState({ view });
|
||||
}
|
||||
|
||||
private getSecondaryDevices() {
|
||||
const secondaryDevices = window.libloki.storage
|
||||
.getSecondaryDevicesFor(this.state.currentPubKey)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
data: secondaryDevices,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private startReceivingRequests() {
|
||||
this.showView('waitingForRequest');
|
||||
}
|
||||
|
||||
private getPubkeyName(pubKey: string | null) {
|
||||
if (!pubKey) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const secretWords = window.mnemonic.pubkey_to_secret_words(pubKey);
|
||||
const conv = window.ConversationController.get(this.state.currentPubKey);
|
||||
const deviceAlias = conv ? conv.getNickname() : 'Unnamed Device';
|
||||
|
||||
return { deviceAlias, secretWords };
|
||||
}
|
||||
|
||||
private stopReceivingRequests() {
|
||||
if (this.state.success) {
|
||||
const aliasKey = 'deviceAlias';
|
||||
const deviceAlias = this.getPubkeyName(this.state.currentPubKey)[
|
||||
aliasKey
|
||||
];
|
||||
|
||||
const conv = window.ConversationController.get(this.state.currentPubKey);
|
||||
if (conv) {
|
||||
conv.setNickname(deviceAlias);
|
||||
}
|
||||
}
|
||||
|
||||
this.showView();
|
||||
}
|
||||
|
||||
private requestReceived(secondaryDevicePubKey: string | EventHandlerNonNull) {
|
||||
// FIFO: push at the front of the array with unshift()
|
||||
this.state.pubKeyRequests.unshift(secondaryDevicePubKey);
|
||||
if (!this.state.currentPubKey) {
|
||||
this.nextPubKey();
|
||||
|
||||
this.showView('requestReceived');
|
||||
}
|
||||
}
|
||||
|
||||
private allowDevice() {
|
||||
this.setState({
|
||||
accepted: true,
|
||||
});
|
||||
window.Whisper.trigger(
|
||||
'devicePairingRequestAccepted',
|
||||
this.state.currentPubKey,
|
||||
(errors: any) => {
|
||||
this.transmisssionCB(errors);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
this.showView();
|
||||
}
|
||||
|
||||
private transmisssionCB(errors: any) {
|
||||
if (!errors) {
|
||||
this.setState({
|
||||
success: true,
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private skipDevice() {
|
||||
window.Whisper.trigger(
|
||||
'devicePairingRequestRejected',
|
||||
this.state.currentPubKey
|
||||
);
|
||||
this.nextPubKey();
|
||||
this.showView();
|
||||
}
|
||||
|
||||
private nextPubKey() {
|
||||
// FIFO: pop at the back of the array using pop()
|
||||
const pubKeyRequests = this.state.pubKeyRequests;
|
||||
this.setState({
|
||||
currentPubKey: pubKeyRequests.pop(),
|
||||
});
|
||||
}
|
||||
|
||||
private onKeyUp(event: any) {
|
||||
switch (event.key) {
|
||||
case 'Esc':
|
||||
case 'Escape':
|
||||
this.closeDialog();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
private closeDialog() {
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
this.stopReceivingRequests();
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { SessionModal } from './SessionModal';
|
||||
import { SessionButton } from './SessionButton';
|
||||
|
||||
interface Props {
|
||||
message: string;
|
||||
title: string;
|
||||
onOk?: any;
|
||||
onClose?: any;
|
||||
onClickOk: any;
|
||||
onClickClose: any;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
hideCancel: boolean;
|
||||
}
|
||||
|
||||
export class SessionConfirm extends React.Component<Props> {
|
||||
public static defaultProps = {
|
||||
title: '',
|
||||
hideCancel: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { title, message, onClickOk, onClickClose, hideCancel } = this.props;
|
||||
|
||||
const okText = this.props.okText || window.i18n('ok');
|
||||
const cancelText = this.props.cancelText || window.i18n('cancel');
|
||||
const showHeader = !!this.props.title;
|
||||
|
||||
return (
|
||||
<SessionModal
|
||||
title={title}
|
||||
onClose={() => null}
|
||||
onOk={() => null}
|
||||
showExitIcon={false}
|
||||
showHeader={showHeader}
|
||||
>
|
||||
{!showHeader && <div className="spacer-lg" />}
|
||||
|
||||
<div className="session-modal__centered">
|
||||
<span className="text-subtle">{message}</span>
|
||||
</div>
|
||||
|
||||
<div className="spacer-lg" />
|
||||
|
||||
<div className="session-modal__button-group">
|
||||
<SessionButton text={okText} onClick={onClickOk} />
|
||||
|
||||
{!hideCancel && (
|
||||
<SessionButton text={cancelText} onClick={onClickClose} />
|
||||
)}
|
||||
</div>
|
||||
</SessionModal>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class SessionSpinner extends React.Component<Props> {
|
||||
public static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { loading } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<div className="session-loader">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue