import React from 'react'; import Arweave from 'arweave/web'; // Arweave libraries import {notify} from 'react-notify-toast'; // Notifications import { withRouter } from 'react-router-dom'; // React-router-dom navigation import { get_public_key, encrypt_mail } from '../../utils/crypto'; // Mail encryption import {getPSTAllocation} from '../../utils/pst'; // Setup PST randomization import ReactTooltip from "react-tooltip"; // PST fee description import './index.css'; // React draft import { Editor } from 'react-draft-wysiwyg'; import { EditorState, convertToRaw, convertFromRaw } from 'draft-js'; import { markdownToDraft } from 'markdown-draft-js'; import draftToMarkdown from 'draftjs-to-markdown'; import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'; class Compose extends React.Component { constructor() { super(); this.state= { recipient: '', // Initialize empty recipient subject: '', // Initialize empty subject numTokens: 0, // Initialize AR tokens to send to 0 transactionLoading: false, // Set loading status to false editorState: EditorState.createEmpty() // Initialize react-draft } } // Input field change handlers onEditorStateChange = editorState => { this.setState({editorState}); }; handleRecipientChange = event => { this.setState({recipient: event.target.value}); }; handleSubjectChange = event => { this.setState({subject: event.target.value}); }; handleNumTokensChange = event => { this.setState({numTokens: event.target.value}); }; // Save as draft function save = () => { // Generate random temporary id let randomID = (Math.random()*1e32).toString(36).substring(0, 10); // Collect markdown from editor let markdown = draftToMarkdown(convertToRaw(this.state.editorState.getCurrentContent())); // Make a complete mail_item let mail_item = { "id": randomID, "to": this.state.recipient, "subject": this.state.subject, "body": markdown, "amount": this.state.numTokens, "timestamp": Date.now() }; // Collect items from drafts in sessionStorage; let sessionDrafts = sessionStorage.getItem('drafts'); let drafts; // If drafts in sessionStorage exists if (sessionDrafts !== null) { // Parse what is in it drafts = JSON.parse(sessionDrafts); } else { // Initialize empty drafts drafts = []; } drafts.push(mail_item); // Push new draft to array sessionStorage.setItem('drafts', JSON.stringify(drafts)); // Set new array to drafts item in sessionStorage this.props.toggleSelf(); // Close modal this.props.history.push(`/drafts/${randomID}`); // Redirect to draft in drafts page }; // Send mail function send = async () => { // Set loading status to true this.setState({transactionLoading: true}); let arweave = Arweave.init(); let wallet = JSON.parse(sessionStorage.getItem('keyfile')); // Collect wallet from sessionStorage let tokens = arweave.ar.arToWinston(this.state.numTokens); // Collect number of tokens to send let pub_key = await get_public_key(this.state.recipient); // Collect recipient public key let pub_key_holder = await arweave.wallets.jwkToAddress(wallet); // Collect sender public key // If public key returns as undefined, thus, address has not sent a transaction to network: if (pub_key === undefined) { // Throw a toast notification error notify.show("Error: Recipient has to send a transaction to the network, first!", "error"); // Stop loading status this.setState({transactionLoading: false}); // Stop further execution return } // If public key is also recipient, a.k.a self-mail if (pub_key_holder === this.state.recipient) { // Throw a toast notification error notify.show("Error: Cannot send mail to yourself", "error"); // Stop loading status this.setState({transactionLoading: false}); // Stop further execution return } // Collect content from react-draft let content = await encrypt_mail(draftToMarkdown(convertToRaw(this.state.editorState.getCurrentContent())), this.state.subject, pub_key); // Generate arweave transactions let tx = await arweave.createTransaction({ target: this.state.recipient, // Recipient from input data: arweave.utils.concatBuffers([content]), // Tx data quantity: tokens }, wallet); // Adhere to Weavemail protocol specifications: tx.addTag('App-Name', 'permamail'); // Add permamail tag tx.addTag('App-Version', '0.0.2'); // Add version tag tx.addTag('Unix-Time', Math.round((new Date()).getTime() / 1000)); // Add Unix timestamp await arweave.transactions.sign(tx, wallet); // Sign transaction let tx_id = tx.id; // Get transaction id from signed transaction // Check if sending wallet has enough AR to cover transaction fees let jwk_wallet = await arweave.wallets.jwkToAddress(wallet); let wallet_balance = await arweave.wallets.getBalance(jwk_wallet); // Collect balance let balance_in_ar = await arweave.ar.winstonToAr(wallet_balance); // Convert winston to AR if (balance_in_ar < 0.10000001 + parseFloat(this.state.numTokens)) { // Throw a toast notification error notify.show("Error: Insufficient balance to send mail", "error"); // Stop loading status this.setState({transactionLoading: false}); // Stop further execution return } // PST Fee handling let pstRecipient = await getPSTAllocation(); // Get randomized token holder address let pstTx = await arweave.createTransaction({ target: pstRecipient, // Fee recipient quantity: arweave.ar.arToWinston(0.1) // 0.1 AR fee }, wallet); await arweave.transactions.sign(pstTx, wallet); // Sign transaction await arweave.transactions.post(pstTx); await arweave.transactions.post(tx); // Post transaction this.setState({transactionLoading: false}); // Set loading status to false notify.show(`Success: Transaction sent, id: ${tx_id}.`, 'success'); // Show successful toast notification with tx id // Add new pending notification to sessionStorage // If notifications array is present in sessionStorage if (sessionStorage.getItem('notifications') !== null) { let notifications = JSON.parse(sessionStorage.getItem('notifications')); // Collect notifications item and parse notifications.push({id: tx_id, timestamp: Date.now(), pending: true}); // Append to notifications sessionStorage.setItem('notifications', JSON.stringify(notifications)); // Update notifications in sessionStorage } else { let notifications = []; notifications.push({id: tx_id, timestamp: Date.now(), pending: true}); // Append to notifications sessionStorage.setItem('notifications', JSON.stringify(notifications)); // Set notifications in sessionStorage } this.props.toggleSelf(); // Close modal }; // Fill existing data from props (used for edit/reply button modal opening) fillExistingData = () => { // If existingData is not null (i.e, there is data to fill): if (this.props.existingData !== null) { // If data is from reply button (i.e inbox page) if (this.props.existingData[0] === 'reply') { // Fill data let subject = this.props.existingData[2].startsWith('RE: ') ? this.props.existingData[2] : 'RE: ' + this.props.existingData[2]; this.setState({recipient: this.props.existingData[1], subject: subject}); } else if (this.props.existingData[0] === 'edit') { // If data is from edit button (i.e drafts page) // Fill data this.setState({ recipient: this.props.existingData[1].to, subject: this.props.existingData[1].subject, numTokens: this.props.existingData[1].amount, editorState: EditorState.createWithContent(convertFromRaw(markdownToDraft(this.props.existingData[1].body))) }) } } }; componentDidMount() { this.fillExistingData(); // Run fillExistingData on load } render() { return ( <> <ReactTooltip id="pst-tooltip" place="top" type="dark" effect="float"> <span>Transaction fees payout to weveToken PST holders and support Weve development.</span> </ReactTooltip> <div className="compose-modal"> <div> <h2>Compose mail</h2> <span>Send a weve mail.</span> </div> <div> <div> <span>Mail recipient</span> <input type="text" value={this.state.recipient} onChange={this.handleRecipientChange} placeholder="cVp3bbGwp9EfSAMPLE9ej3nssi303nn300ns03i"/> </div> <div> <span>Mail subject</span> <input value={this.state.subject} onChange={this.handleSubjectChange} type="text" /> </div> <div> <span>Mail body</span> <Editor editorState={this.state.editorState} toolbar={toolbarOptions} toolbarClassName="editor-toolbar" wrapperClassName="editor-wrapper" editorClassName="editor" onEditorStateChange={this.onEditorStateChange} /> </div> <div> <span>AR tokens to send</span> <input value={this.state.numTokens} onChange={this.handleNumTokensChange} type="number" /> </div> <div> <span data-tip data-for="pst-tooltip">Transaction fee:</span> <span>0.1 AR</span> </div> </div> <div> <button onClick={this.save}><i className="fa fa-floppy-o"></i>Save as draft</button> {this.state.recipient === '' ? ( <button onClick={this.send} className="disabled-send" disabled>Enter recipient</button> ) : ( <button onClick={this.send}><i className={this.state.transactionLoading ? "fa fa-spinner fa-spin" : "fa fa-send-o"}></i>Send</button> )} </div> </div> </> ); } } export default withRouter(Compose); const toolbarOptions = { options: ['inline', 'list', 'link', 'image'], inline: { options: ['bold', 'italic', 'underline', 'strikethrough'], }, list: { options: ['unordered', 'ordered'], }, image: { uploadEnabled: false, } }