mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-10 16:21:36 +00:00
Chart turtle mode (#312)
* Add Chat Turtle Mode * Unused code * Switch and better UX * translations Co-authored-by: KoalaSat <yv1vtrul@duck.com>
This commit is contained in:
parent
2706703382
commit
3bae39931d
@ -18,7 +18,7 @@ class ChatSerializer(serializers.ModelSerializer):
|
|||||||
class PostMessageSerializer(serializers.ModelSerializer):
|
class PostMessageSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Message
|
model = Message
|
||||||
fields = ("PGP_message", "order", "offset")
|
fields = ("PGP_message", "order_id", "offset")
|
||||||
depth = 0
|
depth = 0
|
||||||
|
|
||||||
offset = serializers.IntegerField(
|
offset = serializers.IntegerField(
|
||||||
|
@ -58,6 +58,7 @@ class ChatView(viewsets.ViewSet):
|
|||||||
chatroom.maker_connected = True
|
chatroom.maker_connected = True
|
||||||
chatroom.save()
|
chatroom.save()
|
||||||
peer_connected = chatroom.taker_connected
|
peer_connected = chatroom.taker_connected
|
||||||
|
peer_public_key = order.taker.profile.public_key
|
||||||
elif chatroom.taker == request.user:
|
elif chatroom.taker == request.user:
|
||||||
chatroom.maker_connected = order.maker_last_seen > (
|
chatroom.maker_connected = order.maker_last_seen > (
|
||||||
timezone.now() - timedelta(minutes=1)
|
timezone.now() - timedelta(minutes=1)
|
||||||
@ -65,6 +66,7 @@ class ChatView(viewsets.ViewSet):
|
|||||||
chatroom.taker_connected = True
|
chatroom.taker_connected = True
|
||||||
chatroom.save()
|
chatroom.save()
|
||||||
peer_connected = chatroom.maker_connected
|
peer_connected = chatroom.maker_connected
|
||||||
|
peer_public_key = order.maker.profile.public_key
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
for message in queryset:
|
for message in queryset:
|
||||||
@ -79,7 +81,7 @@ class ChatView(viewsets.ViewSet):
|
|||||||
}
|
}
|
||||||
messages.append(data)
|
messages.append(data)
|
||||||
|
|
||||||
response = {"peer_connected": peer_connected, "messages": messages}
|
response = {"peer_connected": peer_connected, "messages": messages, "peer_pubkey": peer_public_key}
|
||||||
|
|
||||||
return Response(response, status.HTTP_200_OK)
|
return Response(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||||
|
@ -66,6 +66,7 @@ class OrderPage extends Component {
|
|||||||
openStoreToken: false,
|
openStoreToken: false,
|
||||||
tabValue: 1,
|
tabValue: 1,
|
||||||
orderId: this.props.match.params.orderId,
|
orderId: this.props.match.params.orderId,
|
||||||
|
chat_offset: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refresh delays according to Order status
|
// Refresh delays according to Order status
|
||||||
@ -109,6 +110,7 @@ class OrderPage extends Component {
|
|||||||
currencyCode: this.getCurrencyCode(newStateVars.currency),
|
currencyCode: this.getCurrencyCode(newStateVars.currency),
|
||||||
penalty: newStateVars.penalty, // in case penalty time has finished, it goes back to null
|
penalty: newStateVars.penalty, // in case penalty time has finished, it goes back to null
|
||||||
invoice_expired: newStateVars.invoice_expired, // in case invoice had expired, it goes back to null when it is valid again
|
invoice_expired: newStateVars.invoice_expired, // in case invoice had expired, it goes back to null when it is valid again
|
||||||
|
chat_offset: this.state.chat_offset + newStateVars?.chat?.messages.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeStateVars = Object.assign({}, newStateVars, otherStateVars);
|
const completeStateVars = Object.assign({}, newStateVars, otherStateVars);
|
||||||
@ -117,7 +119,10 @@ class OrderPage extends Component {
|
|||||||
|
|
||||||
getOrderDetails = (id) => {
|
getOrderDetails = (id) => {
|
||||||
this.setState({ orderId: id });
|
this.setState({ orderId: id });
|
||||||
apiClient.get('/api/order/?order_id=' + id).then(this.orderDetailsReceived);
|
|
||||||
|
apiClient
|
||||||
|
.get('/api/order/?order_id=' + id + '&offset=' + this.state.chat_offset)
|
||||||
|
.then(this.orderDetailsReceived);
|
||||||
};
|
};
|
||||||
|
|
||||||
orderDetailsReceived = (data) => {
|
orderDetailsReceived = (data) => {
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Grid, useTheme, Tooltip, Button } from '@mui/material';
|
||||||
|
import { ExportIcon } from '../../../Icons';
|
||||||
|
import KeyIcon from '@mui/icons-material/Key';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { saveAsJson } from '../../../../utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orderId: number;
|
||||||
|
setAudit: (audit: boolean) => void;
|
||||||
|
audit: boolean;
|
||||||
|
createJsonFile: () => object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatBottom;
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Grid, Paper, Typography, useTheme } from '@mui/material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
connected: boolean;
|
||||||
|
peerConnected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatHeader: React.FC<Props> = ({ connected, peerConnected }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const connectedColor = theme.palette.mode === 'light' ? '#b5e3b7' : '#153717';
|
||||||
|
const connectedTextColor = theme.palette.getContrastText(connectedColor);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatHeader;
|
@ -0,0 +1,328 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Tooltip, TextField, Grid, Container, Paper, Typography } from '@mui/material';
|
||||||
|
import { encryptMessage, decryptMessage } from '../../../../pgp';
|
||||||
|
import { saveAsJson } from '../../../../utils';
|
||||||
|
import { AuditPGPDialog } from '../../../Dialogs';
|
||||||
|
import { systemClient } from '../../../../services/System';
|
||||||
|
import { websocketClient, WebsocketConnection } from '../../../../services/Websocket';
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import KeyIcon from '@mui/icons-material/Key';
|
||||||
|
import { useTheme } from '@mui/system';
|
||||||
|
import MessageCard from '../MessageCard';
|
||||||
|
import ChatHeader from '../ChatHeader';
|
||||||
|
import { EncryptedChatMessage, ServerMessage } from '..';
|
||||||
|
import ChatBottom from '../ChatBottom';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orderId: number;
|
||||||
|
userNick: string;
|
||||||
|
takerNick: string;
|
||||||
|
messages: EncryptedChatMessage[];
|
||||||
|
setMessages: (messages: EncryptedChatMessage[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EncryptedSocketChat: React.FC<Props> = ({
|
||||||
|
orderId,
|
||||||
|
userNick,
|
||||||
|
takerNick,
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
}: 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 [serverMessages, setServerMessages] = useState<ServerMessage[]>([]);
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
const [connection, setConnection] = useState<WebsocketConnection>();
|
||||||
|
const [audit, setAudit] = useState<boolean>(false);
|
||||||
|
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: EncryptedChatMessage[]) => {
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container component='main'>
|
||||||
|
<ChatHeader connected={connected} peerConnected={peerConnected} />
|
||||||
|
<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) => {
|
||||||
|
const isTaker = takerNick === message.userNick;
|
||||||
|
const userConnected = message.userNick === userNick ? connected : peerConnected;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li style={{ listStyleType: 'none' }} key={index}>
|
||||||
|
<MessageCard message={message} isTaker={isTaker} userConnected={userConnected} />
|
||||||
|
</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)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChatBottom
|
||||||
|
orderId={orderId}
|
||||||
|
audit={audit}
|
||||||
|
setAudit={setAudit}
|
||||||
|
createJsonFile={createJsonFile}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EncryptedSocketChat;
|
@ -0,0 +1,303 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Tooltip, TextField, Grid, Container, Paper, Typography } from '@mui/material';
|
||||||
|
import { encryptMessage, decryptMessage } from '../../../../pgp';
|
||||||
|
import { saveAsJson } from '../../../../utils';
|
||||||
|
import { AuditPGPDialog } from '../../../Dialogs';
|
||||||
|
import { systemClient } from '../../../../services/System';
|
||||||
|
import { websocketClient, WebsocketConnection } from '../../../../services/Websocket';
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import KeyIcon from '@mui/icons-material/Key';
|
||||||
|
import { ExportIcon } from '../../../Icons';
|
||||||
|
import { useTheme } from '@mui/system';
|
||||||
|
import MessageCard from '../MessageCard';
|
||||||
|
import ChatHeader from '../ChatHeader';
|
||||||
|
import { EncryptedChatMessage, ServerMessage } from '..';
|
||||||
|
import { apiClient } from '../../../../services/api';
|
||||||
|
import ChatBottom from '../ChatBottom';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orderId: number;
|
||||||
|
userNick: string;
|
||||||
|
takerNick: string;
|
||||||
|
chatOffset: number;
|
||||||
|
messages: EncryptedChatMessage[];
|
||||||
|
setMessages: (messages: EncryptedChatMessage[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EncryptedTurtleChat: React.FC<Props> = ({
|
||||||
|
orderId,
|
||||||
|
userNick,
|
||||||
|
takerNick,
|
||||||
|
chatOffset,
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const audio = new Audio(`/static/assets/sounds/chat-open.mp3`);
|
||||||
|
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 [value, setValue] = useState<string>('');
|
||||||
|
const [audit, setAudit] = useState<boolean>(false);
|
||||||
|
const [waitingEcho, setWaitingEcho] = useState<boolean>(false);
|
||||||
|
const [lastSent, setLastSent] = useState<string>('---BLANK---');
|
||||||
|
const [messageCount, setMessageCount] = useState<number>(0);
|
||||||
|
const [serverMessages, setServerMessages] = useState<ServerMessage[]>([]);
|
||||||
|
const [lastIndex, setLastIndex] = useState<number>(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > messageCount) {
|
||||||
|
audio.play();
|
||||||
|
setMessageCount(messages.length);
|
||||||
|
}
|
||||||
|
}, [messages, messageCount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (serverMessages.length > 0 && peerPubKey) {
|
||||||
|
serverMessages.forEach(onMessage);
|
||||||
|
}
|
||||||
|
}, [serverMessages, peerPubKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatOffset > lastIndex) {
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
}, [chatOffset]);
|
||||||
|
|
||||||
|
const loadMessages: () => void = () => {
|
||||||
|
apiClient.get(`/api/chat?order_id=${orderId}&offset=${lastIndex}`).then((results: any) => {
|
||||||
|
if (results) {
|
||||||
|
setPeerConnected(results.peer_connected);
|
||||||
|
setPeerPubKey(results.peer_public_key);
|
||||||
|
setServerMessages(results.messages);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createJsonFile: () => object = () => {
|
||||||
|
return {
|
||||||
|
credentials: {
|
||||||
|
own_public_key: ownPubKey,
|
||||||
|
peer_public_key: peerPubKey,
|
||||||
|
encrypted_private_key: ownEncPrivKey,
|
||||||
|
passphrase: token,
|
||||||
|
},
|
||||||
|
messages: messages,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessage: (dataFromServer: ServerMessage) => void = (dataFromServer) => {
|
||||||
|
if (dataFromServer) {
|
||||||
|
// If we receive an encrypted message
|
||||||
|
if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) {
|
||||||
|
decryptMessage(
|
||||||
|
dataFromServer.message.split('\\').join('\n'),
|
||||||
|
dataFromServer.nick == userNick ? ownPubKey : peerPubKey,
|
||||||
|
ownEncPrivKey,
|
||||||
|
token,
|
||||||
|
).then((decryptedData) => {
|
||||||
|
setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent);
|
||||||
|
setLastIndex(lastIndex < dataFromServer.index ? dataFromServer.index : lastIndex);
|
||||||
|
setMessages((prev: EncryptedChatMessage[]) => {
|
||||||
|
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.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.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 (value.substring(0, 1) == '#') {
|
||||||
|
apiClient
|
||||||
|
.post(`/api/chat`, {
|
||||||
|
PGP_message: value,
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setWaitingEcho(false);
|
||||||
|
setValue('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Else if message is not empty send message
|
||||||
|
else if (value != '') {
|
||||||
|
setWaitingEcho(true);
|
||||||
|
setLastSent(value);
|
||||||
|
encryptMessage(value, ownPubKey, peerPubKey, ownEncPrivKey, token).then(
|
||||||
|
(encryptedMessage) => {
|
||||||
|
apiClient
|
||||||
|
.post(`/api/chat`, {
|
||||||
|
PGP_message: encryptedMessage.toString().split('\n').join('\\'),
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setWaitingEcho(false);
|
||||||
|
setValue('');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container component='main'>
|
||||||
|
<ChatHeader connected={true} peerConnected={peerConnected} />
|
||||||
|
<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) => {
|
||||||
|
const isTaker = takerNick === message.userNick;
|
||||||
|
const userConnected = message.userNick === userNick ? true : peerConnected;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li style={{ listStyleType: 'none' }} key={index}>
|
||||||
|
<MessageCard message={message} isTaker={isTaker} userConnected={userConnected} />
|
||||||
|
</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'
|
||||||
|
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={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)}
|
||||||
|
/>
|
||||||
|
<ChatBottom
|
||||||
|
orderId={orderId}
|
||||||
|
audit={audit}
|
||||||
|
setAudit={setAudit}
|
||||||
|
createJsonFile={createJsonFile}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EncryptedTurtleChat;
|
@ -0,0 +1,142 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { IconButton, Tooltip, Card, CardHeader, useTheme } from '@mui/material';
|
||||||
|
import RobotAvatar from '../../../RobotAvatar';
|
||||||
|
import { systemClient } from '../../../../services/System';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
// 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 { EncryptedChatMessage } from '..';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: EncryptedChatMessage;
|
||||||
|
isTaker: boolean;
|
||||||
|
userConnected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageCard: React.FC<Props> = ({ message, isTaker, userConnected }) => {
|
||||||
|
const [showPGP, setShowPGP] = useState<boolean>();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const takerCardColor = theme.palette.mode === 'light' ? '#d1e6fa' : '#082745';
|
||||||
|
const makerCardColor = theme.palette.mode === 'light' ? '#f2d5f6' : '#380d3f';
|
||||||
|
const cardColor = isTaker ? takerCardColor : makerCardColor;
|
||||||
|
|
||||||
|
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={() => setShowPGP(!showPGP)}
|
||||||
|
>
|
||||||
|
<VisibilityIcon
|
||||||
|
color={showPGP ? 'primary' : 'inherit'}
|
||||||
|
sx={{
|
||||||
|
height: '0.6em',
|
||||||
|
width: '0.6em',
|
||||||
|
color: showPGP ? '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 ? message.encryptedMessage : message.plainTextMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ContentCopy
|
||||||
|
sx={{
|
||||||
|
height: '0.7em',
|
||||||
|
width: '0.7em',
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
subheader={
|
||||||
|
showPGP ? (
|
||||||
|
<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 ? theme.typography.fontSize * 0.78 : null,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageCard;
|
@ -1,40 +1,16 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import EncryptedSocketChat from './EncryptedSocketChat';
|
||||||
import {
|
import EncryptedTrutleChat from './EncryptedTrutleChat';
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
TextField,
|
|
||||||
Grid,
|
|
||||||
Container,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
Paper,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { encryptMessage, decryptMessage } from '../../../pgp';
|
|
||||||
import { saveAsJson } from '../../../utils';
|
|
||||||
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 {
|
interface Props {
|
||||||
|
turtleMode: boolean;
|
||||||
orderId: number;
|
orderId: number;
|
||||||
|
takerNick: string;
|
||||||
|
makerNick: string;
|
||||||
userNick: string;
|
userNick: string;
|
||||||
|
chatOffset: number;
|
||||||
}
|
}
|
||||||
|
export interface EncryptedChatMessage {
|
||||||
interface EncryptedChatMessage {
|
|
||||||
userNick: string;
|
userNick: string;
|
||||||
validSignature: boolean;
|
validSignature: boolean;
|
||||||
plainTextMessage: string;
|
plainTextMessage: string;
|
||||||
@ -42,481 +18,39 @@ interface EncryptedChatMessage {
|
|||||||
time: string;
|
time: string;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
export interface ServerMessage {
|
||||||
|
message: string;
|
||||||
|
time: string;
|
||||||
|
index: number;
|
||||||
|
nick: string;
|
||||||
|
}
|
||||||
|
|
||||||
const EncryptedChat: React.FC<Props> = ({ orderId, userNick }: Props): JSX.Element => {
|
const EncryptedChat: React.FC<Props> = ({
|
||||||
const { t } = useTranslation();
|
turtleMode,
|
||||||
const theme = useTheme();
|
orderId,
|
||||||
|
takerNick,
|
||||||
const audio = new Audio(`/static/assets/sounds/chat-open.mp3`);
|
userNick,
|
||||||
const [connected, setConnected] = useState<boolean>(false);
|
chatOffset,
|
||||||
const [peerConnected, setPeerConnected] = useState<boolean>(false);
|
}: Props): JSX.Element => {
|
||||||
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 [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(() => {
|
return turtleMode ? (
|
||||||
if (!connected) {
|
<EncryptedTrutleChat
|
||||||
connectWebsocket();
|
messages={messages}
|
||||||
}
|
setMessages={setMessages}
|
||||||
}, [connected]);
|
orderId={orderId}
|
||||||
|
takerNick={takerNick}
|
||||||
useEffect(() => {
|
userNick={userNick}
|
||||||
if (messages.length > messageCount) {
|
chatOffset={chatOffset}
|
||||||
audio.play();
|
/>
|
||||||
setMessageCount(messages.length);
|
) : (
|
||||||
}
|
<EncryptedSocketChat
|
||||||
}, [messages, messageCount]);
|
messages={messages}
|
||||||
|
setMessages={setMessages}
|
||||||
useEffect(() => {
|
orderId={orderId}
|
||||||
if (serverMessages) {
|
takerNick={takerNick}
|
||||||
serverMessages.forEach(onMessage);
|
userNick={userNick}
|
||||||
}
|
/>
|
||||||
}, [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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Divider,
|
Divider,
|
||||||
|
Switch,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
@ -37,6 +38,7 @@ import { apiClient } from '../../services/api';
|
|||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
import PercentIcon from '@mui/icons-material/Percent';
|
import PercentIcon from '@mui/icons-material/Percent';
|
||||||
|
import SelfImprovement from '@mui/icons-material/SelfImprovement';
|
||||||
import BookIcon from '@mui/icons-material/Book';
|
import BookIcon from '@mui/icons-material/Book';
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||||
@ -47,6 +49,7 @@ import PlayCircleIcon from '@mui/icons-material/PlayCircle';
|
|||||||
import BoltIcon from '@mui/icons-material/Bolt';
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
import LinkIcon from '@mui/icons-material/Link';
|
import LinkIcon from '@mui/icons-material/Link';
|
||||||
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
||||||
|
import WifiTetheringErrorIcon from '@mui/icons-material/WifiTetheringError';
|
||||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||||
import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
|
import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
@ -72,6 +75,7 @@ class TradeBox extends Component {
|
|||||||
badInvoice: false,
|
badInvoice: false,
|
||||||
badAddress: false,
|
badAddress: false,
|
||||||
badStatement: false,
|
badStatement: false,
|
||||||
|
turtle_mode: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1438,6 +1442,29 @@ class TradeBox extends Component {
|
|||||||
</b>{' '}
|
</b>{' '}
|
||||||
{' ' + this.stepXofY()}
|
{' ' + this.stepXofY()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Grid item>
|
||||||
|
<Tooltip
|
||||||
|
enterTouchDelay={0}
|
||||||
|
placement='top'
|
||||||
|
title={t('Activate turtle mode (Use it when the connection is slow)')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '4em',
|
||||||
|
height: '1.1em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
size='small'
|
||||||
|
disabled={false}
|
||||||
|
checked={this.state.turtle_mode}
|
||||||
|
onChange={() => this.setState({ turtle_mode: !this.state.turtle_mode })}
|
||||||
|
/>
|
||||||
|
<WifiTetheringErrorIcon sx={{ color: 'text.secondary' }} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align='center'>
|
<Grid item xs={12} align='center'>
|
||||||
{this.props.data.is_seller ? (
|
{this.props.data.is_seller ? (
|
||||||
@ -1469,7 +1496,15 @@ class TradeBox extends Component {
|
|||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<EncryptedChat orderId={this.props.data.id} userNick={this.props.data.ur_nick} />
|
<EncryptedChat
|
||||||
|
turtleMode={this.state.turtle_mode}
|
||||||
|
chatOffset={this.props.data.chat_offset}
|
||||||
|
orderId={this.props.data.id}
|
||||||
|
takerNick={this.props.data.taker_nick}
|
||||||
|
makerNick={this.props.data.maker_nick}
|
||||||
|
userNick={this.props.data.ur_nick}
|
||||||
|
chat={this.props.data.chat}
|
||||||
|
/>
|
||||||
<Grid item xs={12} align='center'>
|
<Grid item xs={12} align='center'>
|
||||||
{showDisputeButton ? this.showOpenDisputeButton() : ''}
|
{showDisputeButton ? this.showOpenDisputeButton() : ''}
|
||||||
{showSendButton ? this.showFiatSentButton() : ''}
|
{showSendButton ? this.showFiatSentButton() : ''}
|
||||||
|
@ -357,6 +357,7 @@
|
|||||||
"Messages": "Messages",
|
"Messages": "Messages",
|
||||||
"Verified signature by {{nickname}}": "Verified signature by {{nickname}}",
|
"Verified signature by {{nickname}}": "Verified signature by {{nickname}}",
|
||||||
"Cannot verify signature of {{nickname}}": "Cannot verify signature of {{nickname}}",
|
"Cannot verify signature of {{nickname}}": "Cannot verify signature of {{nickname}}",
|
||||||
|
"Activate turtle mode (Use it when the connection is slow)": "Activate turtle mode (Use it when the connection is slow)",
|
||||||
|
|
||||||
"CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline",
|
"CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline",
|
||||||
"Contract Box": "Contract Box",
|
"Contract Box": "Contract Box",
|
||||||
|
@ -345,6 +345,7 @@
|
|||||||
"Messages": "Mensajes",
|
"Messages": "Mensajes",
|
||||||
"Verified signature by {{nickname}}": "Firma de {{nickname}} verificada",
|
"Verified signature by {{nickname}}": "Firma de {{nickname}} verificada",
|
||||||
"Cannot verify signature of {{nickname}}": "No se pudo verificar la firma de {{nickname}}",
|
"Cannot verify signature of {{nickname}}": "No se pudo verificar la firma de {{nickname}}",
|
||||||
|
"Activate turtle mode (Use it when the connection is slow)": "Activar modo tortuga (Úsalo cuando tu conexión es lenta)",
|
||||||
|
|
||||||
"CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline",
|
"CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline",
|
||||||
"Contract Box": "Contrato",
|
"Contract Box": "Contrato",
|
||||||
|
@ -329,6 +329,7 @@
|
|||||||
"Messages": "Сообщения",
|
"Messages": "Сообщения",
|
||||||
"Verified signature by {{nickname}}": "Проверенная подпись пользователя {{nickname}}",
|
"Verified signature by {{nickname}}": "Проверенная подпись пользователя {{nickname}}",
|
||||||
"Cannot verify signature of {{nickname}}": "Не удается проверить подпись {{nickname}}",
|
"Cannot verify signature of {{nickname}}": "Не удается проверить подпись {{nickname}}",
|
||||||
|
"Activate turtle mode (Use it when the connection is slow)": "Включить режим \"черепахи\" (Используйте при низкой скорости интернета)",
|
||||||
|
|
||||||
"CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline",
|
"CONTRACT BOX - TradeBox.js": "The Contract Box that guides users trough the whole trade pipeline",
|
||||||
"Contract Box": "Окно контракта",
|
"Contract Box": "Окно контракта",
|
||||||
|
Loading…
Reference in New Issue
Block a user