2022-05-24 00:31:34 +00:00
import React , { Component } from 'react' ;
import { withTranslation } from "react-i18next" ;
2022-05-24 13:33:55 +00:00
import { Button , IconButton , Badge , Tooltip , TextField , Grid , Container , Card , CardHeader , Paper , Avatar , Typography } from "@mui/material" ;
2022-05-24 00:31:34 +00:00
import ReconnectingWebSocket from 'reconnecting-websocket' ;
import { encryptMessage , decryptMessage } from "../utils/pgp" ;
import { getCookie } from "../utils/cookies" ;
2022-05-24 13:33:55 +00:00
import { saveAsJson } from "../utils/saveFile" ;
2022-05-24 12:16:50 +00:00
import { AuditPGPDialog } from "./Dialogs"
// Icons
import CheckIcon from '@mui/icons-material/Check' ;
import CloseIcon from '@mui/icons-material/Close' ;
2022-05-24 13:33:55 +00:00
import ContentCopy from "@mui/icons-material/ContentCopy" ;
2022-05-24 12:16:50 +00:00
import VisibilityIcon from '@mui/icons-material/Visibility' ;
2022-05-24 21:36:21 +00:00
import CircularProgress from '@mui/material/CircularProgress' ;
2022-05-24 12:16:50 +00:00
import KeyIcon from '@mui/icons-material/Key' ;
import { ExportIcon } from './Icons' ;
2022-05-24 00:31:34 +00:00
class Chat extends Component {
constructor ( props ) {
super ( props ) ;
}
state = {
own _pub _key : getCookie ( 'pub_key' ) . split ( '\\' ) . join ( '\n' ) ,
own _enc _priv _key : getCookie ( 'enc_priv_key' ) . split ( '\\' ) . join ( '\n' ) ,
peer _pub _key : null ,
token : getCookie ( 'robot_token' ) ,
messages : [ ] ,
value : '' ,
connected : false ,
peer _connected : false ,
audit : false ,
2022-05-24 21:36:21 +00:00
showPGP : new Array ,
waitingEcho : false ,
lastSent : '---BLANK---' ,
2022-05-28 14:59:32 +00:00
latestIndex : 0 ,
2022-06-04 17:29:21 +00:00
scrollNow : false ,
2022-05-24 00:31:34 +00:00
} ;
rws = new ReconnectingWebSocket ( 'ws://' + window . location . host + '/ws/chat/' + this . props . orderId + '/' ) ;
2022-05-24 21:36:21 +00:00
2022-05-24 00:31:34 +00:00
componentDidMount ( ) {
this . rws . addEventListener ( 'open' , ( ) => {
console . log ( 'Connected!' ) ;
this . setState ( { connected : true } ) ;
this . rws . send ( JSON . stringify ( {
type : "message" ,
message : this . state . own _pub _key ,
nick : this . props . ur _nick ,
} ) ) ;
} ) ;
this . rws . addEventListener ( 'message' , ( message ) => {
const dataFromServer = JSON . parse ( message . data ) ;
console . log ( 'Got reply!' , dataFromServer . type ) ;
2022-05-28 14:59:32 +00:00
console . log ( 'PGP message index' , dataFromServer . index , ' latestIndex ' , this . state . latestIndex ) ;
2022-05-24 00:31:34 +00:00
if ( dataFromServer ) {
console . log ( dataFromServer )
2022-06-04 17:29:21 +00:00
this . setState ( { peer _connected : dataFromServer . peer _connected } )
2022-05-24 00:31:34 +00:00
// If we receive our own key on a message
2022-05-28 14:59:32 +00:00
if ( dataFromServer . message == this . state . own _pub _key ) { console . log ( "OWN PUB KEY RECEIVED!!" ) }
2022-05-24 00:31:34 +00:00
// If we receive a public key other than ours (our peer key!)
2022-05-28 14:59:32 +00:00
if ( dataFromServer . message . substring ( 0 , 36 ) == ` -----BEGIN PGP PUBLIC KEY BLOCK----- ` && dataFromServer . message != this . state . own _pub _key ) {
2022-05-24 12:16:50 +00:00
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" )
}
2022-05-28 17:05:04 +00:00
console . log ( "PEER PUBKEY RECEIVED!!" )
2022-05-24 00:31:34 +00:00
this . setState ( { peer _pub _key : dataFromServer . message } )
2022-05-28 21:50:18 +00:00
// After receiving the peer pubkey we ask the server for the historic messages if any
this . rws . send ( JSON . stringify ( {
type : "message" ,
message : ` -----SERVE HISTORY----- ` ,
nick : this . props . ur _nick ,
} ) )
2022-05-24 00:31:34 +00:00
} else
// If we receive an encrypted message
2022-05-28 14:59:32 +00:00
if ( dataFromServer . message . substring ( 0 , 27 ) == ` -----BEGIN PGP MESSAGE----- ` && dataFromServer . index > this . state . latestIndex ) {
2022-05-24 00:31:34 +00:00
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 ) =>
( {
2022-06-04 17:29:21 +00:00
scrollNow : true ,
2022-05-24 21:36:21 +00:00
waitingEcho : this . state . waitingEcho == true ? ( decryptedData . decryptedMessage == this . state . lastSent ? false : true ) : false ,
lastSent : decryptedData . decryptedMessage == this . state . lastSent ? '----BLANK----' : this . state . lastSent ,
2022-05-28 14:59:32 +00:00
latestIndex : dataFromServer . index > this . state . latestIndex ? dataFromServer . index : this . state . latestIndex ,
2022-05-24 00:31:34 +00:00
messages : [ ... state . messages ,
2022-05-28 14:59:32 +00:00
{
index : dataFromServer . index ,
2022-05-24 00:31:34 +00:00
encryptedMessage : dataFromServer . message . split ( '\\' ) . join ( '\n' ) ,
plainTextMessage : decryptedData . decryptedMessage ,
validSignature : decryptedData . validSignature ,
userNick : dataFromServer . user _nick ,
time : dataFromServer . time
2022-05-28 21:50:18 +00:00
} ] . sort ( function ( a , b ) {
// order the message array by their index (increasing)
return a . index - b . index
} ) ,
2022-05-24 00:31:34 +00:00
} )
) ) ;
2022-06-04 21:26:53 +00:00
} else
// We allow plaintext communication. The user must write # to start
// If we receive an plaintext message
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 ( ) ,
} ] } ) ) ;
}
2022-05-24 00:31:34 +00:00
}
} ) ;
this . rws . addEventListener ( 'close' , ( ) => {
console . log ( 'Socket is closed. Reconnect will be attempted' ) ;
this . setState ( { connected : false } ) ;
} ) ;
this . rws . addEventListener ( 'error' , ( ) => {
console . error ( 'Socket encountered error: Closing socket' ) ;
} ) ;
}
componentDidUpdate ( ) {
2022-06-04 21:26:53 +00:00
// Only fire the scroll when the reason for Update is a new message
2022-06-04 17:29:21 +00:00
if ( this . state . scrollNow ) {
this . scrollToBottom ( ) ;
this . setState ( { scrollNow : false } )
}
2022-05-24 00:31:34 +00:00
}
scrollToBottom = ( ) => {
this . messagesEnd . scrollIntoView ( { behavior : "smooth" } ) ;
}
onButtonClicked = ( e ) => {
2022-06-04 21:26:53 +00:00
if ( this . state . value . substring ( 0 , 1 ) == '#' ) {
this . rws . send ( JSON . stringify ( {
type : "message" ,
message : this . state . value ,
nick : this . props . ur _nick ,
} ) ) ;
this . setState ( { value : "" } ) ;
}
else if ( this . state . value != '' ) {
this . setState ( { value : "" , waitingEcho : true , lastSent : this . state . value } )
2022-05-24 00:31:34 +00:00
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 ) =>
2022-06-04 21:26:53 +00:00
console . log ( "Sending Encrypted MESSAGE" , encryptedMessage ) &
2022-05-24 00:31:34 +00:00
this . rws . send ( JSON . stringify ( {
type : "message" ,
message : encryptedMessage . split ( '\n' ) . join ( '\\' ) ,
nick : this . props . ur _nick ,
} )
2022-06-04 21:26:53 +00:00
)
2022-05-24 00:31:34 +00:00
) ;
}
e . preventDefault ( ) ;
}
2022-05-24 12:16:50 +00:00
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 ,
} )
}
2022-05-24 21:36:21 +00:00
messageCard = ( props ) => {
const { t } = this . props ;
return (
< Card elevation = { 5 } align = "left" >
< CardHeader sx = { { color : '#333333' } }
avatar = {
< Badge variant = "dot" overlap = "circular" badgeContent = "" color = { props . userConnected ? "success" : "error" } >
< Avatar className = "flippedSmallAvatar"
alt = { props . message . userNick }
src = { window . location . origin + '/static/assets/avatars/' + props . message . userNick + '.png' }
/ >
< / B a d g e >
}
style = { { backgroundColor : props . cardColor } }
title = {
2022-06-05 01:10:13 +00:00
< 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 } ) } >
2022-06-04 21:26:53 +00:00
< div style = { { display : 'flex' , alignItems : 'center' , flexWrap : 'wrap' , position : 'relative' , left : - 5 , width : 240 } } >
2022-06-05 01:10:13 +00:00
< div style = { { width : 168 , display : 'flex' , alignItems : 'center' , flexWrap : 'wrap' } } >
2022-05-24 21:36:21 +00:00
{ props . message . userNick }
{ props . message . validSignature ?
< CheckIcon sx = { { height : 16 } } color = "success" / >
:
< CloseIcon sx = { { height : 16 } } color = "error" / >
}
< / d i v >
< 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" } } / >
< / I c o n B u t t o n >
< / d i v >
< div style = { { width : 20 } } >
< Tooltip disableHoverListener enterTouchDelay = { 0 } title = { t ( "Copied!" ) } >
< IconButton sx = { { height : 18 , width : 18 } }
onClick = { ( ) => navigator . clipboard . writeText ( this . state . showPGP [ props . index ] ? props . message . encryptedMessage : props . message . plainTextMessage ) } >
< ContentCopy sx = { { height : 16 , width : 16 , color : '#333333' } } / >
< / I c o n B u t t o n >
< / T o o l t i p >
< / d i v >
< / d i v >
< / T o o l t i p >
}
2022-05-25 13:13:39 +00:00
subheader = { this . state . showPGP [ props . index ] ? < a > { props . message . time } < br / > { "Valid signature: " + props . message . validSignature } < br / > { props . message . encryptedMessage } < / a > : p r o p s . m e s s a g e . p l a i n T e x t M e s s a g e }
2022-06-06 18:00:50 +00:00
subheaderTypographyProps = { { sx : { wordWrap : "break-word" , width : '200px' , color : '#444444' , fontSize : this . state . showPGP [ props . index ] ? 11 : null } } }
2022-05-24 21:36:21 +00:00
/ >
< / C a r d >
)
}
2022-05-24 00:31:34 +00:00
render ( ) {
const { t } = this . props ;
return (
2022-06-04 17:29:21 +00:00
< Container component = "main" >
2022-05-24 00:31:34 +00:00
< 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' } } >
2022-05-24 21:36:21 +00:00
< Typography variant = 'caption' sx = { { color : '#333333' } } >
2022-05-24 00:31:34 +00:00
{ t ( "You" ) + ": " } { this . state . connected ? t ( "connected" ) : t ( "disconnected" ) }
< / T y p o g r a p h y >
< / P a p e r >
< / G r i d >
< Grid item xs = { 0.4 } / >
< Grid item xs = { 5.5 } >
< Paper elevation = { 1 } style = { this . state . peer _connected ? { backgroundColor : '#e8ffe6' } : { backgroundColor : '#FFF1C5' } } >
2022-05-24 21:36:21 +00:00
< Typography variant = 'caption' sx = { { color : '#333333' } } >
2022-05-24 00:31:34 +00:00
{ t ( "Peer" ) + ": " } { this . state . peer _connected ? t ( "connected" ) : t ( "disconnected" ) }
< / T y p o g r a p h y >
< / P a p e r >
< / G r i d >
< Grid item xs = { 0.3 } / >
< / G r i d >
2022-06-04 21:26:53 +00:00
< 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' } } >
2022-06-04 17:29:21 +00:00
{ 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 } / >
2022-05-24 21:36:21 +00:00
:
2022-06-04 17:29:21 +00:00
< this . messageCard message = { message } index = { index } cardColor = { '#fafafa' } userConnected = { this . state . peer _connected } / >
}
< / l i > ) }
< div style = { { float : "left" , clear : "both" } } ref = { ( el ) => { this . messagesEnd = el ; } } > < / d i v >
< / P a p e r >
< 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 ;
} }
2022-06-04 21:26:53 +00:00
sx = { { width : 219 } }
2022-06-04 17:29:21 +00:00
/ >
< / G r i d >
< 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 } } / > < / d i v >
< div style = { { width : 18 } } > < CircularProgress size = { 16 } thickness = { 5 } / > < / d i v >
< / d i v >
:
t ( "Send" )
}
< / B u t t o n >
< / G r i d >
2022-05-24 00:31:34 +00:00
< / G r i d >
2022-06-04 17:29:21 +00:00
< / f o r m >
< / d i v >
2022-05-24 12:16:50 +00:00
< div style = { { height : 4 } } / >
2022-06-04 17:29:21 +00:00
2022-05-24 12:16:50 +00:00
< 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 } ) }
/ >
2022-05-24 12:52:33 +00:00
2022-05-24 12:16:50 +00:00
< Grid item xs = { 6 } >
2022-05-24 12:52:33 +00:00
< 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" ) } < / B u t t o n >
< / T o o l t i p >
2022-05-24 12:16:50 +00:00
< / G r i d >
< Grid item xs = { 6 } >
2022-05-24 13:33:55 +00:00
< Tooltip placement = "bottom" enterTouchDelay = { 0 } enterDelay = { 500 } enterNextDelay = { 2000 } title = { t ( "Save full log as a JSON file (messages and credentials)" ) } >
2022-05-25 13:13:39 +00:00
< 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 } } / > < / d i v > { t ( " E x p o r t " ) } < / B u t t o n >
2022-05-24 12:52:33 +00:00
< / T o o l t i p >
2022-05-24 12:16:50 +00:00
< / G r i d >
2022-05-24 00:31:34 +00:00
< / G r i d >
2022-05-24 12:16:50 +00:00
2022-05-24 00:31:34 +00:00
< / C o n t a i n e r >
)
}
}
export default withTranslation ( ) ( Chat ) ;