mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-14 11:26:24 +00:00
Refactor Encrypted chat to component (#297)
* Refactor Encrypted chat to component * Rebase * CR * Better performance * Bad performance * Fix styles Co-authored-by: Reckless_Satoshi <reckless.satoshi@protonmail.com>
This commit is contained in:
parent
1502f9555b
commit
5e6f7165d7
@ -1,565 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Badge,
|
||||
Tooltip,
|
||||
TextField,
|
||||
Grid,
|
||||
Container,
|
||||
Card,
|
||||
CardHeader,
|
||||
Paper,
|
||||
Avatar,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
import { encryptMessage, decryptMessage } from '../../utils/pgp';
|
||||
import { saveAsJson } from '../../utils/saveFile';
|
||||
import { AuditPGPDialog } from '../Dialogs';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import { systemClient } from '../../services/System';
|
||||
import { websocketClient } from '../../services/Websocket';
|
||||
|
||||
// Icons
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import KeyIcon from '@mui/icons-material/Key';
|
||||
import { ExportIcon } from '../Icons';
|
||||
|
||||
class Chat extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
state = {
|
||||
own_pub_key: systemClient.getCookie('pub_key').split('\\').join('\n'),
|
||||
own_enc_priv_key: systemClient.getCookie('enc_priv_key').split('\\').join('\n'),
|
||||
peer_pub_key: null,
|
||||
token: systemClient.getCookie('robot_token'),
|
||||
messages: [],
|
||||
value: '',
|
||||
connected: false,
|
||||
connection: null,
|
||||
peer_connected: false,
|
||||
audit: false,
|
||||
showPGP: new Array(),
|
||||
waitingEcho: false,
|
||||
lastSent: '---BLANK---',
|
||||
latestIndex: 0,
|
||||
scrollNow: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
websocketClient
|
||||
.open(`ws://${window.location.host}/ws/chat/${this.props.orderId}/`)
|
||||
.then((connection) => {
|
||||
console.log('Connected!');
|
||||
|
||||
connection.send({
|
||||
message: this.state.own_pub_key,
|
||||
nick: this.props.ur_nick,
|
||||
});
|
||||
|
||||
connection.onMessage(this.onMessage);
|
||||
connection.onClose(() => {
|
||||
console.log('Socket is closed. Reconnect will be attempted');
|
||||
this.setState({ connected: false });
|
||||
});
|
||||
connection.onError(() => {
|
||||
console.error('Socket encountered error: Closing socket');
|
||||
this.setState({ connected: false });
|
||||
});
|
||||
|
||||
this.setState({ connected: true, connection });
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Only fire the scroll and audio when the reason for Update is a new message
|
||||
if (this.state.scrollNow) {
|
||||
const audio = new Audio(`/static/assets/sounds/chat-open.mp3`);
|
||||
audio.play();
|
||||
this.scrollToBottom();
|
||||
this.setState({ scrollNow: false });
|
||||
}
|
||||
}
|
||||
|
||||
onMessage = (message) => {
|
||||
const dataFromServer = JSON.parse(message.data);
|
||||
console.log('Got reply!', dataFromServer.type);
|
||||
console.log('PGP message index', dataFromServer.index, ' latestIndex ', this.state.latestIndex);
|
||||
if (dataFromServer) {
|
||||
console.log(dataFromServer);
|
||||
this.setState({ peer_connected: dataFromServer.peer_connected });
|
||||
|
||||
// If we receive our own key on a message
|
||||
if (dataFromServer.message == this.state.own_pub_key) {
|
||||
console.log('OWN PUB KEY RECEIVED!!');
|
||||
}
|
||||
|
||||
// If we receive a public key other than ours (our peer key!)
|
||||
if (
|
||||
dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` &&
|
||||
dataFromServer.message != this.state.own_pub_key
|
||||
) {
|
||||
if (dataFromServer.message == this.state.peer_pub_key) {
|
||||
console.log('PEER HAS RECONNECTED USING HIS PREVIOUSLY KNOWN PUBKEY');
|
||||
} else if (
|
||||
(dataFromServer.message != this.state.peer_pub_key) &
|
||||
(this.state.peer_pub_key != null)
|
||||
) {
|
||||
console.log('PEER PUBKEY HAS CHANGED');
|
||||
}
|
||||
console.log('PEER PUBKEY RECEIVED!!');
|
||||
this.setState({ peer_pub_key: dataFromServer.message });
|
||||
|
||||
// After receiving the peer pubkey we ask the server for the historic messages if any
|
||||
this.state.connection.send({
|
||||
message: `-----SERVE HISTORY-----`,
|
||||
nick: this.props.ur_nick,
|
||||
});
|
||||
}
|
||||
|
||||
// If we receive an encrypted message
|
||||
else if (
|
||||
dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----` &&
|
||||
dataFromServer.index > this.state.latestIndex
|
||||
) {
|
||||
decryptMessage(
|
||||
dataFromServer.message.split('\\').join('\n'),
|
||||
dataFromServer.user_nick == this.props.ur_nick
|
||||
? this.state.own_pub_key
|
||||
: this.state.peer_pub_key,
|
||||
this.state.own_enc_priv_key,
|
||||
this.state.token,
|
||||
).then((decryptedData) =>
|
||||
this.setState((state) => ({
|
||||
scrollNow: true,
|
||||
waitingEcho:
|
||||
this.state.waitingEcho == true
|
||||
? decryptedData.decryptedMessage != this.state.lastSent
|
||||
: false,
|
||||
lastSent:
|
||||
decryptedData.decryptedMessage == this.state.lastSent
|
||||
? '----BLANK----'
|
||||
: this.state.lastSent,
|
||||
latestIndex:
|
||||
dataFromServer.index > this.state.latestIndex
|
||||
? dataFromServer.index
|
||||
: this.state.latestIndex,
|
||||
messages: [
|
||||
...state.messages,
|
||||
{
|
||||
index: dataFromServer.index,
|
||||
encryptedMessage: dataFromServer.message.split('\\').join('\n'),
|
||||
plainTextMessage: decryptedData.decryptedMessage,
|
||||
validSignature: decryptedData.validSignature,
|
||||
userNick: dataFromServer.user_nick,
|
||||
time: dataFromServer.time,
|
||||
},
|
||||
].sort(function (a, b) {
|
||||
// order the message array by their index (increasing)
|
||||
return a.index - b.index;
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// We allow plaintext communication. The user must write # to start
|
||||
// If we receive an plaintext message
|
||||
else if (dataFromServer.message.substring(0, 1) == '#') {
|
||||
console.log('Got plaintext message', dataFromServer.message);
|
||||
this.setState((state) => ({
|
||||
scrollNow: true,
|
||||
messages: [
|
||||
...state.messages,
|
||||
{
|
||||
index: this.state.latestIndex + 0.001,
|
||||
encryptedMessage: dataFromServer.message,
|
||||
plainTextMessage: dataFromServer.message,
|
||||
validSignature: false,
|
||||
userNick: dataFromServer.user_nick,
|
||||
time: new Date().toString(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scrollToBottom = () => {
|
||||
this.messagesEnd.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
onButtonClicked = (e) => {
|
||||
// If input string contains token. Do not set message
|
||||
if (this.state.value.indexOf(this.state.token) !== -1) {
|
||||
alert(
|
||||
`Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
|
||||
);
|
||||
this.setState({ value: '' });
|
||||
}
|
||||
|
||||
// If input string contains '#' send unencrypted and unlogged message
|
||||
else if (this.state.value.substring(0, 1) == '#') {
|
||||
this.state.connection.send({
|
||||
message: this.state.value,
|
||||
nick: this.props.ur_nick,
|
||||
});
|
||||
this.setState({ value: '' });
|
||||
}
|
||||
|
||||
// Else if message is not empty send message
|
||||
else if (this.state.value != '') {
|
||||
this.setState({ value: '', waitingEcho: true, lastSent: this.state.value });
|
||||
encryptMessage(
|
||||
this.state.value,
|
||||
this.state.own_pub_key,
|
||||
this.state.peer_pub_key,
|
||||
this.state.own_enc_priv_key,
|
||||
this.state.token,
|
||||
).then(
|
||||
(encryptedMessage) =>
|
||||
console.log('Sending Encrypted MESSAGE', encryptedMessage) &
|
||||
this.state.connection.send({
|
||||
message: encryptedMessage.split('\n').join('\\'),
|
||||
nick: this.props.ur_nick,
|
||||
}),
|
||||
);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
createJsonFile = () => {
|
||||
return {
|
||||
credentials: {
|
||||
own_public_key: this.state.own_pub_key,
|
||||
peer_public_key: this.state.peer_pub_key,
|
||||
encrypted_private_key: this.state.own_enc_priv_key,
|
||||
passphrase: this.state.token,
|
||||
},
|
||||
messages: this.state.messages,
|
||||
};
|
||||
};
|
||||
|
||||
messageCard = (props) => {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Card elevation={5} align='left'>
|
||||
<CardHeader
|
||||
sx={{ color: '#333333' }}
|
||||
avatar={
|
||||
<RobotAvatar
|
||||
statusColor={props.userConnected ? 'success' : 'error'}
|
||||
nickname={props.message.userNick}
|
||||
/>
|
||||
}
|
||||
style={{ backgroundColor: props.cardColor }}
|
||||
title={
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t(
|
||||
props.message.validSignature
|
||||
? 'Verified signature by {{nickname}}'
|
||||
: 'Cannot verify signature of {{nickname}}',
|
||||
{ nickname: props.message.userNick },
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
position: 'relative',
|
||||
left: -5,
|
||||
width: 240,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: 168, display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}
|
||||
>
|
||||
{props.message.userNick}
|
||||
{props.message.validSignature ? (
|
||||
<CheckIcon sx={{ height: 16 }} color='success' />
|
||||
) : (
|
||||
<CloseIcon sx={{ height: 16 }} color='error' />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ width: 20 }}>
|
||||
<IconButton
|
||||
sx={{ height: 18, width: 18 }}
|
||||
onClick={() =>
|
||||
this.setState((prevState) => {
|
||||
const newShowPGP = [...prevState.showPGP];
|
||||
newShowPGP[props.index] = !newShowPGP[props.index];
|
||||
return { showPGP: newShowPGP };
|
||||
})
|
||||
}
|
||||
>
|
||||
<VisibilityIcon
|
||||
color={this.state.showPGP[props.index] ? 'primary' : 'inherit'}
|
||||
sx={{
|
||||
height: 16,
|
||||
width: 16,
|
||||
color: this.state.showPGP[props.index] ? 'primary' : '#333333',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ width: 20 }}>
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<IconButton
|
||||
sx={{ height: 18, width: 18 }}
|
||||
onClick={() =>
|
||||
systemClient.copyToClipboard(
|
||||
this.state.showPGP[props.index]
|
||||
? props.message.encryptedMessage
|
||||
: props.message.plainTextMessage,
|
||||
)
|
||||
}
|
||||
>
|
||||
<ContentCopy sx={{ height: 16, width: 16, color: '#333333' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
subheader={
|
||||
this.state.showPGP[props.index] ? (
|
||||
<a>
|
||||
{' '}
|
||||
{props.message.time} <br /> {'Valid signature: ' + props.message.validSignature}{' '}
|
||||
<br /> {props.message.encryptedMessage}{' '}
|
||||
</a>
|
||||
) : (
|
||||
props.message.plainTextMessage
|
||||
)
|
||||
}
|
||||
subheaderTypographyProps={{
|
||||
sx: {
|
||||
wordWrap: 'break-word',
|
||||
width: '200px',
|
||||
color: '#444444',
|
||||
fontSize: this.state.showPGP[props.index] ? 11 : null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Container component='main'>
|
||||
<Grid container spacing={0.5}>
|
||||
<Grid item xs={0.3} />
|
||||
<Grid item xs={5.5}>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={
|
||||
this.state.connected
|
||||
? { backgroundColor: '#e8ffe6' }
|
||||
: { backgroundColor: '#FFF1C5' }
|
||||
}
|
||||
>
|
||||
<Typography variant='caption' sx={{ color: '#333333' }}>
|
||||
{t('You') + ': '}
|
||||
{this.state.connected ? t('connected') : t('disconnected')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={0.4} />
|
||||
<Grid item xs={5.5}>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={
|
||||
this.state.peer_connected
|
||||
? { backgroundColor: '#e8ffe6' }
|
||||
: { backgroundColor: '#FFF1C5' }
|
||||
}
|
||||
>
|
||||
<Typography variant='caption' sx={{ color: '#333333' }}>
|
||||
{t('Peer') + ': '}
|
||||
{this.state.peer_connected ? t('connected') : t('disconnected')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={0.3} />
|
||||
</Grid>
|
||||
<div style={{ position: 'relative', left: '-2px', margin: '0 auto', width: '285px' }}>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={{
|
||||
height: '300px',
|
||||
maxHeight: '300px',
|
||||
width: '285px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#F7F7F7',
|
||||
}}
|
||||
>
|
||||
{this.state.messages.map((message, index) => (
|
||||
<li style={{ listStyleType: 'none' }} key={index}>
|
||||
{message.userNick == this.props.ur_nick ? (
|
||||
<this.messageCard
|
||||
message={message}
|
||||
index={index}
|
||||
cardColor={'#eeeeee'}
|
||||
userConnected={this.state.connected}
|
||||
/>
|
||||
) : (
|
||||
<this.messageCard
|
||||
message={message}
|
||||
index={index}
|
||||
cardColor={'#fafafa'}
|
||||
userConnected={this.state.peer_connected}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<div
|
||||
style={{ float: 'left', clear: 'both' }}
|
||||
ref={(el) => {
|
||||
this.messagesEnd = el;
|
||||
}}
|
||||
></div>
|
||||
</Paper>
|
||||
<form noValidate onSubmit={this.onButtonClicked}>
|
||||
<Grid alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<TextField
|
||||
label={t('Type a message')}
|
||||
variant='standard'
|
||||
size='small'
|
||||
helperText={
|
||||
this.state.connected
|
||||
? this.state.peer_pub_key
|
||||
? null
|
||||
: t('Waiting for peer public key...')
|
||||
: t('Connecting...')
|
||||
}
|
||||
value={this.state.value}
|
||||
onChange={(e) => {
|
||||
this.setState({ value: e.target.value });
|
||||
this.value = this.state.value;
|
||||
}}
|
||||
sx={{ width: 219 }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Button
|
||||
sx={{ width: 68 }}
|
||||
disabled={
|
||||
!this.state.connected ||
|
||||
this.state.waitingEcho ||
|
||||
this.state.peer_pub_key == null
|
||||
}
|
||||
type='submit'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
>
|
||||
{this.state.waitingEcho ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
minWidth: 68,
|
||||
width: 68,
|
||||
position: 'relative',
|
||||
left: 15,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 20 }}>
|
||||
<KeyIcon sx={{ width: 18 }} />
|
||||
</div>
|
||||
<div style={{ width: 18 }}>
|
||||
<CircularProgress size={16} thickness={5} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
t('Send')
|
||||
)}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 4 }} />
|
||||
|
||||
<Grid container spacing={0}>
|
||||
<AuditPGPDialog
|
||||
open={this.state.audit}
|
||||
onClose={() => this.setState({ audit: false })}
|
||||
orderId={Number(this.props.orderId)}
|
||||
messages={this.state.messages}
|
||||
own_pub_key={this.state.own_pub_key}
|
||||
own_enc_priv_key={this.state.own_enc_priv_key}
|
||||
peer_pub_key={this.state.peer_pub_key ? this.state.peer_pub_key : 'Not received yet'}
|
||||
passphrase={this.state.token}
|
||||
onClickBack={() => this.setState({ audit: false })}
|
||||
/>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t('Verify your privacy')}
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
onClick={() => this.setState({ audit: !this.state.audit })}
|
||||
>
|
||||
<KeyIcon />
|
||||
{t('Audit PGP')}{' '}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t('Save full log as a JSON file (messages and credentials)')}
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
onClick={() =>
|
||||
saveAsJson(
|
||||
'complete_log_chat_' + this.props.orderId + '.json',
|
||||
this.createJsonFile(),
|
||||
)
|
||||
}
|
||||
>
|
||||
<div style={{ width: 28, height: 20 }}>
|
||||
<ExportIcon sx={{ width: 20, height: 20 }} />
|
||||
</div>{' '}
|
||||
{t('Export')}{' '}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(Chat);
|
523
frontend/src/components/TradeBox/EncryptedChat/index.tsx
Normal file
523
frontend/src/components/TradeBox/EncryptedChat/index.tsx
Normal file
@ -0,0 +1,523 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
TextField,
|
||||
Grid,
|
||||
Container,
|
||||
Card,
|
||||
CardHeader,
|
||||
Paper,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { encryptMessage, decryptMessage } from '../../../utils/pgp';
|
||||
import { saveAsJson } from '../../../utils/saveFile';
|
||||
import { AuditPGPDialog } from '../../Dialogs';
|
||||
import RobotAvatar from '../../RobotAvatar';
|
||||
import { systemClient } from '../../../services/System';
|
||||
import { websocketClient, WebsocketConnection } from '../../../services/Websocket';
|
||||
|
||||
// Icons
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import KeyIcon from '@mui/icons-material/Key';
|
||||
import { ExportIcon } from '../../Icons';
|
||||
import { useTheme } from '@mui/system';
|
||||
|
||||
interface Props {
|
||||
orderId: number;
|
||||
userNick: string;
|
||||
}
|
||||
|
||||
interface EncryptedChatMessage {
|
||||
userNick: string;
|
||||
validSignature: boolean;
|
||||
plainTextMessage: string;
|
||||
encryptedMessage: string;
|
||||
time: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const EncryptedChat: React.FC<Props> = ({ orderId, userNick }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const audio = new Audio(`/static/assets/sounds/chat-open.mp3`);
|
||||
const [connected, setConnected] = useState<boolean>(false);
|
||||
const [peerConnected, setPeerConnected] = useState<boolean>(false);
|
||||
const [ownPubKey] = useState<string>(
|
||||
(systemClient.getCookie('pub_key') ?? '').split('\\').join('\n'),
|
||||
);
|
||||
const [ownEncPrivKey] = useState<string>(
|
||||
(systemClient.getCookie('enc_priv_key') ?? '').split('\\').join('\n'),
|
||||
);
|
||||
const [peerPubKey, setPeerPubKey] = useState<string>();
|
||||
const [token] = useState<string>(systemClient.getCookie('robot_token') || '');
|
||||
const [messages, setMessages] = useState<EncryptedChatMessage[]>([]);
|
||||
const [serverMessages, setServerMessages] = useState<any[]>([]);
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [connection, setConnection] = useState<WebsocketConnection>();
|
||||
const [audit, setAudit] = useState<boolean>(false);
|
||||
const [showPGP, setShowPGP] = useState<boolean[]>([]);
|
||||
const [waitingEcho, setWaitingEcho] = useState<boolean>(false);
|
||||
const [lastSent, setLastSent] = useState<string>('---BLANK---');
|
||||
const [messageCount, setMessageCount] = useState<number>(0);
|
||||
const [receivedIndexes, setReceivedIndexes] = useState<number[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected) {
|
||||
connectWebsocket();
|
||||
}
|
||||
}, [connected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > messageCount) {
|
||||
audio.play();
|
||||
setMessageCount(messages.length);
|
||||
}
|
||||
}, [messages, messageCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverMessages) {
|
||||
serverMessages.forEach(onMessage);
|
||||
}
|
||||
}, [serverMessages]);
|
||||
|
||||
const connectWebsocket = () => {
|
||||
websocketClient.open(`ws://${window.location.host}/ws/chat/${orderId}/`).then((connection) => {
|
||||
setConnection(connection);
|
||||
setConnected(true);
|
||||
|
||||
connection.send({
|
||||
message: ownPubKey,
|
||||
nick: userNick,
|
||||
});
|
||||
|
||||
connection.onMessage((message) => setServerMessages((prev) => [...prev, message]));
|
||||
connection.onClose(() => setConnected(false));
|
||||
connection.onError(() => setConnected(false));
|
||||
});
|
||||
};
|
||||
|
||||
const createJsonFile: () => object = () => {
|
||||
return {
|
||||
credentials: {
|
||||
own_public_key: ownPubKey,
|
||||
peer_public_key: peerPubKey,
|
||||
encrypted_private_key: ownEncPrivKey,
|
||||
passphrase: token,
|
||||
},
|
||||
messages: messages,
|
||||
};
|
||||
};
|
||||
|
||||
const onMessage: (message: any) => void = (message) => {
|
||||
const dataFromServer = JSON.parse(message.data);
|
||||
|
||||
if (dataFromServer && !receivedIndexes.includes(dataFromServer.index)) {
|
||||
setReceivedIndexes((prev) => [...prev, dataFromServer.index]);
|
||||
setPeerConnected(dataFromServer.peer_connected);
|
||||
// If we receive a public key other than ours (our peer key!)
|
||||
if (
|
||||
connection &&
|
||||
dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` &&
|
||||
dataFromServer.message != ownPubKey
|
||||
) {
|
||||
setPeerPubKey(dataFromServer.message);
|
||||
connection.send({
|
||||
message: `-----SERVE HISTORY-----`,
|
||||
nick: userNick,
|
||||
});
|
||||
}
|
||||
// If we receive an encrypted message
|
||||
else if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) {
|
||||
decryptMessage(
|
||||
dataFromServer.message.split('\\').join('\n'),
|
||||
dataFromServer.user_nick == userNick ? ownPubKey : peerPubKey,
|
||||
ownEncPrivKey,
|
||||
token,
|
||||
).then((decryptedData) => {
|
||||
setWaitingEcho(waitingEcho ? decryptedData.decryptedMessage !== lastSent : false);
|
||||
setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent);
|
||||
setMessages((prev) => {
|
||||
const existingMessage = prev.find((item) => item.index === dataFromServer.index);
|
||||
if (existingMessage) {
|
||||
return prev;
|
||||
} else {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
index: dataFromServer.index,
|
||||
encryptedMessage: dataFromServer.message.split('\\').join('\n'),
|
||||
plainTextMessage: decryptedData.decryptedMessage,
|
||||
validSignature: decryptedData.validSignature,
|
||||
userNick: dataFromServer.user_nick,
|
||||
time: dataFromServer.time,
|
||||
} as EncryptedChatMessage,
|
||||
].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// We allow plaintext communication. The user must write # to start
|
||||
// If we receive an plaintext message
|
||||
else if (dataFromServer.message.substring(0, 1) == '#') {
|
||||
setMessages((prev) => {
|
||||
const existingMessage = prev.find(
|
||||
(item) => item.plainTextMessage === dataFromServer.message,
|
||||
);
|
||||
if (existingMessage) {
|
||||
return prev;
|
||||
} else {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
index: prev.length + 0.001,
|
||||
encryptedMessage: dataFromServer.message,
|
||||
plainTextMessage: dataFromServer.message,
|
||||
validSignature: false,
|
||||
userNick: dataFromServer.user_nick,
|
||||
time: new Date().toString(),
|
||||
} as EncryptedChatMessage,
|
||||
].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onButtonClicked = (e: any) => {
|
||||
if (token && value.indexOf(token) !== -1) {
|
||||
alert(
|
||||
`Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
|
||||
);
|
||||
setValue('');
|
||||
}
|
||||
// If input string contains '#' send unencrypted and unlogged message
|
||||
else if (connection && value.substring(0, 1) == '#') {
|
||||
connection.send({
|
||||
message: value,
|
||||
nick: userNick,
|
||||
});
|
||||
setValue('');
|
||||
}
|
||||
|
||||
// Else if message is not empty send message
|
||||
else if (value != '') {
|
||||
setValue('');
|
||||
setWaitingEcho(true);
|
||||
setLastSent(value);
|
||||
encryptMessage(value, ownPubKey, peerPubKey, ownEncPrivKey, token).then(
|
||||
(encryptedMessage) => {
|
||||
if (connection) {
|
||||
connection.send({
|
||||
message: encryptedMessage.toString().split('\n').join('\\'),
|
||||
nick: userNick,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const messageCard: (
|
||||
message: EncryptedChatMessage,
|
||||
index: number,
|
||||
cardColor: string,
|
||||
userConnected: boolean,
|
||||
) => JSX.Element = (message, index, cardColor, userConnected) => {
|
||||
return (
|
||||
<Card elevation={5}>
|
||||
<CardHeader
|
||||
sx={{ color: theme.palette.text.secondary }}
|
||||
avatar={
|
||||
<RobotAvatar
|
||||
statusColor={userConnected ? 'success' : 'error'}
|
||||
nickname={message.userNick}
|
||||
/>
|
||||
}
|
||||
style={{ backgroundColor: cardColor }}
|
||||
title={
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t(
|
||||
message.validSignature
|
||||
? 'Verified signature by {{nickname}}'
|
||||
: 'Cannot verify signature of {{nickname}}',
|
||||
{ nickname: message.userNick },
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
position: 'relative',
|
||||
left: '-0.35em',
|
||||
width: '17.14em',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '11.78em',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{message.userNick}
|
||||
{message.validSignature ? (
|
||||
<CheckIcon sx={{ height: '0.8em' }} color='success' />
|
||||
) : (
|
||||
<CloseIcon sx={{ height: '0.8em' }} color='error' />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ width: '1.4em' }}>
|
||||
<IconButton
|
||||
sx={{ height: '1.2em', width: '1.2em', position: 'relative', right: '0.15em' }}
|
||||
onClick={() => {
|
||||
const newShowPGP = [...showPGP];
|
||||
newShowPGP[index] = !newShowPGP[index];
|
||||
setShowPGP(newShowPGP);
|
||||
}}
|
||||
>
|
||||
<VisibilityIcon
|
||||
color={showPGP[index] ? 'primary' : 'inherit'}
|
||||
sx={{
|
||||
height: '0.6em',
|
||||
width: '0.6em',
|
||||
color: showPGP[index] ? 'primary' : theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ width: '1.4em' }}>
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<IconButton
|
||||
sx={{ height: '0.8em', width: '0.8em' }}
|
||||
onClick={() =>
|
||||
systemClient.copyToClipboard(
|
||||
showPGP[index] ? message.encryptedMessage : message.plainTextMessage,
|
||||
)
|
||||
}
|
||||
>
|
||||
<ContentCopy
|
||||
sx={{
|
||||
height: '0.7em',
|
||||
width: '0.7em',
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
subheader={
|
||||
showPGP[index] ? (
|
||||
<a>
|
||||
{' '}
|
||||
{message.time} <br /> {'Valid signature: ' + message.validSignature} <br />{' '}
|
||||
{message.encryptedMessage}{' '}
|
||||
</a>
|
||||
) : (
|
||||
message.plainTextMessage
|
||||
)
|
||||
}
|
||||
subheaderTypographyProps={{
|
||||
sx: {
|
||||
wordWrap: 'break-word',
|
||||
width: '14.3em',
|
||||
position: 'relative',
|
||||
right: '1.5em',
|
||||
textAlign: 'left',
|
||||
fontSize: showPGP[index] ? theme.typography.fontSize * 0.78 : null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const connectedColor = theme.palette.mode === 'light' ? '#b5e3b7' : '#153717';
|
||||
const connectedTextColor = theme.palette.getContrastText(connectedColor);
|
||||
const ownCardColor = theme.palette.mode === 'light' ? '#d1e6fa' : '#082745';
|
||||
const peerCardColor = theme.palette.mode === 'light' ? '#f2d5f6' : '#380d3f';
|
||||
|
||||
return (
|
||||
<Container component='main'>
|
||||
<Grid container spacing={0.5}>
|
||||
<Grid item xs={0.3} />
|
||||
<Grid item xs={5.5}>
|
||||
<Paper elevation={1} sx={connected ? { backgroundColor: connectedColor } : {}}>
|
||||
<Typography variant='caption' sx={{ color: connectedTextColor }}>
|
||||
{t('You') + ': '}
|
||||
{connected ? t('connected') : t('disconnected')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={0.4} />
|
||||
<Grid item xs={5.5}>
|
||||
<Paper elevation={1} sx={peerConnected ? { backgroundColor: connectedColor } : {}}>
|
||||
<Typography variant='caption' sx={{ color: connectedTextColor }}>
|
||||
{t('Peer') + ': '}
|
||||
{peerConnected ? t('connected') : t('disconnected')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={0.3} />
|
||||
</Grid>
|
||||
<div style={{ position: 'relative', left: '-0.14em', margin: '0 auto', width: '17.7em' }}>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={{
|
||||
height: '21.42em',
|
||||
maxHeight: '21.42em',
|
||||
width: '17.7em',
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<li style={{ listStyleType: 'none' }} key={index}>
|
||||
{message.userNick == userNick
|
||||
? messageCard(message, index, ownCardColor, connected)
|
||||
: messageCard(message, index, peerCardColor, peerConnected)}
|
||||
</li>
|
||||
))}
|
||||
<div
|
||||
style={{ float: 'left', clear: 'both' }}
|
||||
ref={(el) => {
|
||||
if (messages.length > messageCount) el?.scrollIntoView();
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
<form noValidate onSubmit={onButtonClicked}>
|
||||
<Grid alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<TextField
|
||||
label={t('Type a message')}
|
||||
variant='standard'
|
||||
size='small'
|
||||
helperText={
|
||||
connected
|
||||
? peerPubKey
|
||||
? null
|
||||
: t('Waiting for peer public key...')
|
||||
: t('Connecting...')
|
||||
}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
sx={{ width: '13.7em' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Button
|
||||
sx={{ width: '4.68em' }}
|
||||
disabled={!connected || waitingEcho || !peerPubKey}
|
||||
type='submit'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
>
|
||||
{waitingEcho ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
minWidth: '4.68em',
|
||||
width: '4.68em',
|
||||
position: 'relative',
|
||||
left: '1em',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '1.2em' }}>
|
||||
<KeyIcon sx={{ width: '1em' }} />
|
||||
</div>
|
||||
<div style={{ width: '1em', position: 'relative', left: '0.5em' }}>
|
||||
<CircularProgress size={1.1 * theme.typography.fontSize} thickness={5} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
t('Send')
|
||||
)}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style={{ height: '0.3em' }} />
|
||||
|
||||
<Grid container spacing={0}>
|
||||
<AuditPGPDialog
|
||||
open={audit}
|
||||
onClose={() => setAudit(false)}
|
||||
orderId={Number(orderId)}
|
||||
messages={messages}
|
||||
own_pub_key={ownPubKey || ''}
|
||||
own_enc_priv_key={ownEncPrivKey || ''}
|
||||
peer_pub_key={peerPubKey || 'Not received yet'}
|
||||
passphrase={token || ''}
|
||||
onClickBack={() => setAudit(false)}
|
||||
/>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t('Verify your privacy')}
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
onClick={() => setAudit(!audit)}
|
||||
>
|
||||
<KeyIcon />
|
||||
{t('Audit PGP')}{' '}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t('Save full log as a JSON file (messages and credentials)')}
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
onClick={() => saveAsJson('complete_log_chat_' + orderId + '.json', createJsonFile())}
|
||||
>
|
||||
<div style={{ width: '1.4em', height: '1.4em' }}>
|
||||
<ExportIcon sx={{ width: '0.8em', height: '0.8em' }} />
|
||||
</div>{' '}
|
||||
{t('Export')}{' '}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default EncryptedChat;
|
@ -29,7 +29,7 @@ import {
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import QRCode from 'react-qr-code';
|
||||
import Countdown, { zeroPad } from 'react-countdown';
|
||||
import Chat from './EncryptedChat';
|
||||
import EncryptedChat from './EncryptedChat';
|
||||
import TradeSummary from './TradeSummary';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { systemClient } from '../../services/System';
|
||||
@ -1469,7 +1469,7 @@ class TradeBox extends Component {
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Chat orderId={this.props.data.id} ur_nick={this.props.data.ur_nick} />
|
||||
<EncryptedChat orderId={this.props.data.id} userNick={this.props.data.ur_nick} />
|
||||
<Grid item xs={12} align='center'>
|
||||
{showDisputeButton ? this.showOpenDisputeButton() : ''}
|
||||
{showSendButton ? this.showFiatSentButton() : ''}
|
||||
|
@ -17,7 +17,7 @@ class WebsocketConnectionWeb implements WebsocketConnection {
|
||||
);
|
||||
};
|
||||
|
||||
public onMessage: (event: (message: object) => void) => void = (event) => {
|
||||
public onMessage: (event: (message: any) => void) => void = (event) => {
|
||||
this.rws.addEventListener('message', event);
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ import WebsocketWebClient from './WebsocketWebClient';
|
||||
|
||||
export interface WebsocketConnection {
|
||||
send: (message: object) => void;
|
||||
onMessage: (event: (message: object) => void) => void;
|
||||
onMessage: (event: (message: any) => void) => void;
|
||||
onClose: (event: () => void) => void;
|
||||
onError: (event: () => void) => void;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user