Browse Source

Add transactionsMonitor, Add progressBar, Minor improvements and fixes

develop
Apostolos Fanakis 7 years ago
parent
commit
2d48cc3ab9
  1. 9
      src/assets/css/App.css
  2. 1
      src/assets/css/index.css
  3. 108
      src/assets/css/progress-bar.css
  4. 4
      src/assets/css/start-topic-container.css
  5. 5
      src/assets/css/topic-container.css
  6. 196
      src/components/NewPost.js
  7. 2
      src/components/PostList.js
  8. 77
      src/components/ProfileInformation.js
  9. 61
      src/components/WithBlockchainData.js
  10. 26
      src/containers/BoardContainer.js
  11. 13
      src/containers/SignUpContainer.js
  12. 183
      src/containers/StartTopicContainer.js
  13. 94
      src/containers/TopicContainer.js
  14. 172
      src/containers/TransactionsMonitorContainer.js
  15. 33
      src/containers/UsernameFormContainer.js
  16. 19
      src/layouts/CoreLayout/CoreLayout.js
  17. 54
      src/redux/actions/transactionsMonitorActions.js
  18. 12
      src/redux/actions/userInterfaceActions.js
  19. 5
      src/redux/reducer/reducer.js
  20. 39
      src/redux/reducer/transactionsMonitorReducer.js
  21. 22
      src/redux/reducer/userInterfaceReducer.js

9
src/assets/css/App.css

@ -6,10 +6,6 @@ html, body {
height: 100%;
}
body {
font-family: 'Roboto', sans-serif;
}
strong {
font-weight: bold !important;
}
@ -57,6 +53,11 @@ strong {
right: 0;
}
.sidebar-message {
margin: 0px 5px 12px 12px;
padding: 0px;
}
.view-container {
width: 100%;
height: 100%;

1
src/assets/css/index.css

@ -1,5 +1,4 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}

108
src/assets/css/progress-bar.css

@ -0,0 +1,108 @@
/* Progress Bar */
.progress-bar-container {
position: absolute;
top: 54px;
left: 0px;
width: 100%;
}
.progress {
position: relative;
height: 4px;
display: block;
width: 100%;
background-color: #acece6;
border-radius: 2px;
background-clip: padding-box;
margin: 0.5rem 0 1rem 0;
overflow: hidden;
}
.progress .indeterminate {
background-color: #00b5ad;
}
.progress .indeterminate:before {
content: '';
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
}
.progress .indeterminate:after {
content: '';
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
-webkit-animation-delay: 1.15s;
animation-delay: 1.15s;
}
@-webkit-keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@-webkit-keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}
@keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}

4
src/assets/css/start-topic-container.css

@ -3,8 +3,4 @@
.topic-form {
width: 100%;
margin: 20px 0px;
}
.markdown-preview {
padding: 0px 20px;
}

5
src/assets/css/topic-container.css

@ -1,5 +1,10 @@
/* POSTS LIST SCREEN */
.posts-list-spacer {
margin-bottom: 85px;
height: 0px;
}
.post {
width: 100%;
background-color: #FFFFFF;

196
src/components/NewPost.js

@ -1,7 +1,5 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import uuidv4 from 'uuid/v4';
import { drizzleConnect } from 'drizzle-react';
import { Grid, Form, TextArea, Button, Icon, Divider } from 'semantic-ui-react'
@ -9,8 +7,7 @@ import TimeAgo from 'react-timeago';
import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown';
const contract = "Forum";
const contractMethod = "createPost";
import { createPost } from '../redux/actions/transactionsMonitorActions';
class NewPost extends Component {
constructor(props, context) {
@ -23,20 +20,13 @@ class NewPost extends Component {
this.newPostOuterRef = React.createRef();
this.transactionProgressText = [];
this.drizzle = context.drizzle;
this.state = {
postSubjectInput: this.props.subject ? this.props.subject : "",
postContentInput: '',
postSubjectInputEmptySubmit: false,
postContentInputEmptySubmit: false,
previewEnabled: false,
previewDate: "",
creatingPost: false,
transactionState: null,
savingToOrbitDB: null,
transactionOutputTimerActive: false
previewDate: ""
};
}
@ -49,13 +39,14 @@ class NewPost extends Component {
return;
}
this.stackId = this.drizzle.contracts[contract].methods[contractMethod].cacheSend(this.props.topicID);
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push("Waiting for transaction acceptance...");
this.setState({
'creatingPost': true,
'transactionState': "ACCEPTANCE_PENDING"
});
this.props.store.dispatch(
createPost(this.props.topicID, ((returnData) => {
this.topicIDFetched = returnData.topicID;
this.postIDFetched = returnData.postID;
this.pushToDatabase();
}))
);
this.props.onPostCreated();
}
async pushToDatabase() {
@ -63,7 +54,6 @@ class NewPost extends Component {
subject: this.state.postSubjectInput,
content: this.state.postContentInput
});
this.setState({'savingToOrbitDB': "SUCCESS"});
}
handleInputChange(event) {
@ -90,14 +80,6 @@ class NewPost extends Component {
render() {
return (
<div className="post" ref={this.newPostOuterRef}>
{this.state.creatingPost && <div id="overlay">
<div id="overlay-content">
<p><i className="fas fa-spinner fa-3x fa-spin"></i></p>
<br/>
{this.transactionProgressText}
</div>
</div>
}
<Divider horizontal>
<span className="grey-text">#{this.props.postIndex}</span>
</Divider>
@ -184,156 +166,6 @@ class NewPost extends Component {
);
}
componentWillReceiveProps(){
if(this.state.creatingPost && !this.state.transactionOutputTimerActive){
/* User submitted a new Post */
if (this.state.transactionState === "ACCEPTANCE_PENDING" &&
this.props.transactionStack[this.stackId]) {
/* User confirmed the transaction */
//Gets transaciton's hash
this.txHash = this.props.transactionStack[this.stackId];
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push("Transaction in progress: txHash = " + this.txHash);
this.setState({'transactionState': "IN_PROGRESS"});
}
else if (this.state.transactionState === "IN_PROGRESS") {
if (this.props.transactions[this.txHash].status === "success"){
/* Transaction completed successfully */
//Gets post's id returned by contract
let postData = this.props.transactions[this.txHash].receipt.events.PostCreated
.returnValues;
this.topicIDFetched = postData.topicID;
this.postIDFetched = postData.postID;
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'green'}}>
<strong>
Transaction completed successfully.
</strong>
</span>);
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'green'}}>
<strong>
TopicID = {this.topicIDFetched}, PostID = {this.postIDFetched}
</strong>
</span>);
this.setState({'transactionState': "SUCCESS"});
} else if (this.props.transactions[this.txHash].status === "error"){
/* Transaction failed to complete */
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'red'}}>
<strong>
Transaction failed to complete with error:
</strong>
</span>);
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'red'}}>
<strong>
{this.props.transactions[this.txHash].error}
</strong>
</span>);
this.setState({
'transactionState': "ERROR",
'transactionOutputTimerActive': true
});
this.transactionOutputTimer = setTimeout(() => {
this.transactionProgressText = [];
this.setState({
'creatingPost': false,
'transactionState': null,
'savingToOrbitDB': null,
'transactionOutputTimerActive': false
});
this.props.onPostCreated();
}, 5000);
}
}
else if (this.state.transactionState === "SUCCESS") {
/* Transaction completed successfully */
//Tries to store data in OrbitDB
this.pushToDatabase();
if (this.state.savingToOrbitDB === "SUCCESS"){
/* Data successfully saved in OrbitDB */
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'green'}}>
<strong>
Post successfully saved in OrbitDB.
</strong>
</span>);
this.setState({'transactionOutputTimerActive': true});
this.transactionOutputTimer = setTimeout(() => {
this.transactionProgressText = [];
this.setState({
'creatingPost': false,
'transactionState': null,
'savingToOrbitDB': null,
'transactionOutputTimerActive': false
});
this.props.onPostCreated();
}, 5000);
}
else if (this.state.savingToOrbitDB === "ERROR"){
/* Failed to save data in OrbitDB */
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'red'}}>
<strong>
An error occurred while trying to save post in OrbitDB.
</strong>
</span>);
this.setState({'transactionOutputTimerActive': true});
this.transactionOutputTimer = setTimeout(() => {
this.transactionProgressText = [];
this.setState({
'creatingPost': false,
'transactionState': null,
'savingToOrbitDB': null,
'transactionOutputTimerActive': false
});
this.props.onPostCreated();
}, 5000);
}
}
else if (this.state.transactionState === "ACCEPTANCE_PENDING" &&
this.props.transactions.undefined !== undefined &&
this.props.transactions.undefined.status === "error"){
/* User probably canceled the transaction */
//TODO user can't post after this!
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'orange'}}>
<strong>
Transaction canceled.
</strong>
</span>);
this.setState({'transactionState': "SUCCESS"});
this.setState({'transactionOutputTimerActive': true});
this.transactionOutputTimer = setTimeout(() => {
this.transactionProgressText = [];
this.setState({
'creatingPost': false,
'transactionState': null,
'savingToOrbitDB': null,
'transactionOutputTimerActive': false
});
this.props.onPostCreated();
}, 5000);
}
}
}
componentDidUpdate(prevProps, prevState){
if (!this.state.previewEnabled && prevState.previewEnabled){
this.newPostOuterRef.current.scrollIntoView(true);
@ -345,14 +177,8 @@ class NewPost extends Component {
}
}
NewPost.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
transactions: state.transactions,
transactionStack: state.transactionStack,
orbitDB: state.orbitDB,
user: state.user
}

2
src/components/PostList.js

@ -23,7 +23,7 @@ const PostList = (props) => {
});
return (
<div className="posts-list">
<div>
{props.recentToTheTop
?posts.slice(0).reverse()
:posts

77
src/components/ProfileInformation.js

@ -17,46 +17,47 @@ const ProfileInformation = (props) => {
transaction = props.blockchainData
.find(transaction => transaction.callInfo.method === "getOrbitDBId")
let orbitDBId = transaction ? transaction.returnData : "";
return (
<div className="user-info">
{props.avatarUrl && <UserAvatar
size="40"
className="inline user-avatar"
src={props.avatarUrl}
name={username}/>}
<table className="highlight centered responsive-table">
<tbody>
<tr>
<td><strong>Username:</strong></td>
<td>{username}</td>
</tr>
<tr>
<td><strong>Account address:</strong></td>
<td>{props.address}</td>
</tr>
<tr>
<td><strong>OrbitDB:</strong></td>
<td>{orbitDBId}</td>
</tr>
<tr>
<td><strong>Number of topics created:</strong></td>
<td>{props.numberOfTopics}</td>
</tr>
return (
<div className="user-info">
{props.avatarUrl && <UserAvatar
size="40"
className="inline user-avatar"
src={props.avatarUrl}
name={username}/>}
<table className="highlight centered responsive-table">
<tbody>
<tr>
<td><strong>Username:</strong></td>
<td>{username}</td>
</tr>
<tr>
<td><strong>Account address:</strong></td>
<td>{props.address}</td>
</tr>
<tr>
<td><strong>OrbitDB:</strong></td>
<td>{orbitDBId}</td>
</tr>
<tr>
<td><strong>Number of topics created:</strong></td>
<td>{props.numberOfTopics}</td>
</tr>
<tr>
<td><strong>Number of posts:</strong></td>
<td>{props.numberOfPosts}</td>
</tr>
{dateOfRegister &&
<tr>
<td><strong>Number of posts:</strong></td>
<td>{props.numberOfPosts}</td>
<td><strong>Member since:</strong></td>
<td>{epochTimeConverter(dateOfRegister)}</td>
</tr>
{props.dateOfRegister &&
<tr>
<td><strong>Member since:</strong></td>
<td>{epochTimeConverter(dateOfRegister)}</td>
</tr>
}
</tbody>
</table>
{props.self && <UsernameFormContainer/>}
</div>
);
}
</tbody>
</table>
{props.self && <UsernameFormContainer/>}
</div>
);
};
export default ProfileInformation;

61
src/components/WithBlockchainData.js

@ -12,11 +12,9 @@ class WithBlockchainData extends Component {
this.forwardedProps = rest;
}
this.checkContractUpdates = this.checkContractUpdates.bind(this);
this.drizzle = context.drizzle;
this.dataKeys = [];
this.blockchainData = this.callsInfo.map((call) => {
let blockchainData = this.callsInfo.map((call) => {
return ({
callInfo: call,
status: "initialized",
@ -24,52 +22,53 @@ class WithBlockchainData extends Component {
});
});
//Initial call
for (var i = 0; i < this.callsInfo.length; ++i){
this.dataKeys[i] = this.drizzle
.contracts[this.callsInfo[i].contract]
.methods[this.callsInfo[i].method]
.cacheCall(...(this.callsInfo[i].params));
this.blockchainData[i].status = "pending";
blockchainData[i].status = "pending";
}
this.state = {
transactionsState: new Array(this.callsInfo.length).fill("pending")
callState: new Array(this.callsInfo.length).fill("pending"),
blockchainData: blockchainData
}
}
render() {
let {component, callsInfo, ...rest } = this.props;
let {component, callsInfo, ...rest } = this.props; //Update rest arguments
return (
<this.component blockchainData={this.blockchainData} {...rest}/>
<this.component blockchainData={this.state.blockchainData} {...rest}/>
);
}
componentDidMount() {
this.intervalChecker = setInterval(this.checkContractUpdates, 10); //HOWMUCHMUCHACHO???
}
componentWillUnmount() {
clearInterval(this.intervalChecker);
}
checkContractUpdates() {
componentDidUpdate(){
for (var i = 0; i < this.callsInfo.length; ++i){
let currentDrizzleState = this.drizzle.store.getState();
if (this.state.transactionsState[i] === "pending") {
let dataFetched = (currentDrizzleState
.contracts[this.callsInfo[i].contract][this.callsInfo[i].method][this.dataKeys[i]]);
if (dataFetched){
this.blockchainData[i].returnData = dataFetched.value;
this.blockchainData[i].status = "success";
this.setState((prevState) => ({
transactionsState: [
...prevState.transactionsState.slice(0, i),
"success",
...prevState.transactionsState.slice(i)
]
}));
}
} //TODO cover errors!!
let dataFetched = (currentDrizzleState
.contracts[this.callsInfo[i].contract][this.callsInfo[i].method][this.dataKeys[i]]);
if (dataFetched && dataFetched.value !== this.state.blockchainData[i].returnData){
/* There are new data in the blockchain*/
//Immutable update
let newBlockchainData = this.state.blockchainData.map((callData, index) => {
if (index !== i) return callData;
return {
...callData,
returnData: dataFetched.value,
status: "success"
}
})
let newStates = this.state.callState.slice();
newStates[i] = "success"
this.setState({
callState: newStates,
blockchainData: newBlockchainData
});
}
}
}
}

26
src/containers/BoardContainer.js

@ -7,13 +7,20 @@ import { Header } from 'semantic-ui-react';
import WithBlockchainData from '../components/WithBlockchainData';
import TopicList from '../components/TopicList';
import FloatingButton from '../components/FloatingButton';
import LoadingSpinner from '../components/LoadingSpinner';
import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions';
class Board extends Component {
constructor(props) {
super(props);
this.props.store.dispatch(showProgressBar());
this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this);
this.state = {
pageLoaded: false
}
}
handleCreateTopicClick() {
@ -22,17 +29,14 @@ class Board extends Component {
render() {
var boardContents;
if (!this.props.blockchainData[0].returnData) {
boardContents = (
<LoadingSpinner/>
);
} else if (this.props.blockchainData[0].returnData !== '0'){
if (this.props.blockchainData[0].returnData !== '0'){
this.topicIDs = [];
for (var i = 0; i < this.props.blockchainData[0].returnData; i++) {
this.topicIDs.push(i);
}
boardContents = ([
<TopicList topicIDs={this.topicIDs} key="topicList"/>,
<div className="bottom-overlay-pad" key="pad"></div>,
this.props.user.hasSignedUp &&
<FloatingButton onClick={this.handleCreateTopicClick}
key="createTopicButton"/>
@ -45,7 +49,7 @@ class Board extends Component {
There are no topics yet!
</Header>
<Header color='teal' textAlign='center' as='h4'>
Sign up to be the first post.
Sign up to be the first to post.
</Header>
</div>
);
@ -68,10 +72,16 @@ class Board extends Component {
return (
<div className="fill">
{boardContents}
<div className="bottom-overlay-pad"></div>
</div>
);
}
componentDidUpdate(){
if (!this.state.pageLoaded && this.props.blockchainData[0].returnData){
this.props.store.dispatch(hideProgressBar());
this.setState({ pageLoaded: true });
}
}
}
Board.contextTypes = {

13
src/containers/SignUpContainer.js

@ -5,6 +5,16 @@ import UsernameFormContainer from './UsernameFormContainer';
import { Header } from 'semantic-ui-react';
class SignUp extends Component {
constructor(props){
super(props);
this.signedUp = this.signedUp.bind(this);
}
signedUp(){
this.props.router.push("/home");
}
render() {
return (
this.props.user.hasSignedUp
@ -22,14 +32,13 @@ class SignUp extends Component {
<p className="no-margin">
<strong>Account address:</strong> {this.props.user.address}
</p>
<UsernameFormContainer/>
<UsernameFormContainer signedUp={this.signedUp}/>
</div>
</div>)
);
}
}
// May still need this even with data function to refresh component on updates for this contract.
const mapStateToProps = state => {
return {
user: state.user

183
src/containers/StartTopicContainer.js

@ -1,14 +1,11 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import uuidv4 from 'uuid/v4';
import { Form, TextArea, Button, Icon } from 'semantic-ui-react'
import NewTopicPreview from '../components/NewTopicPreview'
const contract = "Forum";
const contractMethod = "createTopic";
import { createTopic } from '../redux/actions/transactionsMonitorActions';
class StartTopic extends Component {
constructor(props, context) {
@ -19,9 +16,6 @@ class StartTopic extends Component {
this.validateAndPost = this.validateAndPost.bind(this);
this.pushToDatabase = this.pushToDatabase.bind(this);
this.transactionProgressText = [];
this.drizzle = context.drizzle;
this.state = {
topicSubjectInput: '',
topicMessageInput: '',
@ -29,10 +23,7 @@ class StartTopic extends Component {
topicMessageInputEmptySubmit: false,
previewEnabled: false,
previewDate: "",
creatingTopic: false,
transactionState: null,
savingToOrbitDB: null,
transactionOutputTimerActive: false
creatingTopic: false
};
}
@ -42,16 +33,19 @@ class StartTopic extends Component {
topicSubjectInputEmptySubmit: this.state.topicSubjectInput === '',
topicMessageInputEmptySubmit: this.state.topicMessageInput === ''
});
return;
}
this.stackId = this.drizzle.contracts[contract].methods[contractMethod].cacheSend();
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push("Waiting for transaction acceptance...");
this.props.store.dispatch(
createTopic(((returnData) => {
this.topicIDFetched = returnData.topicID;
this.postIDFetched = returnData.postID;
this.pushToDatabase();
this.props.router.push("/topic/" + this.topicIDFetched);
}))
);
this.setState({
'creatingTopic': true,
'transactionState': "ACCEPTANCE_PENDING"
'creatingTopic': true
});
}
@ -64,7 +58,6 @@ class StartTopic extends Component {
subject: this.state.topicSubjectInput,
content: this.state.topicMessageInput
});
this.setState({'savingToOrbitDB': "SUCCESS"});
}
handleInputChange(event) {
@ -97,13 +90,13 @@ class StartTopic extends Component {
var previewEditText = this.state.previewEnabled ? "Edit" : "Preview";
return (
<div>
{this.state.creatingTopic && <div id="overlay">
{/*this.state.creatingTopic && <div id="overlay">
<div id="overlay-content">
<p><i className="fas fa-spinner fa-3x fa-spin"></i></p>
<br/>
{this.transactionProgressText}
</div>
</div>
</div>*/
}
{this.state.previewEnabled &&
<NewTopicPreview
@ -151,157 +144,9 @@ class StartTopic extends Component {
</div>
);
}
componentWillReceiveProps(){
if(this.state.creatingTopic && !this.state.transactionOutputTimerActive){
/* User submitted a new Topic */
if (this.state.transactionState === "ACCEPTANCE_PENDING" &&
this.props.transactionStack[this.stackId]) {
/* User confirmed the transaction */
//Gets transaciton's hash
this.txHash = this.props.transactionStack[this.stackId];
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push("Transaction in progress: txHash = " + this.txHash);
this.setState({'transactionState': "IN_PROGRESS"});
}
else if (this.state.transactionState === "IN_PROGRESS") {
if (this.props.transactions[this.txHash].status === "success"){
/* Transaction completed successfully */
//Gets topic's id returned by contract
let topicData = this.props.transactions[this.txHash].receipt.events.TopicCreated
.returnValues;
this.topicIDFetched = topicData.topicID;
this.postIDFetched = topicData.postID;
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'green'}}>
<strong>
Transaction completed successfully.
</strong>
</span>);
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'green'}}>
<strong>
TopicID = {this.topicIDFetched}, PostID = {this.postIDFetched}
</strong>
</span>);
this.setState({'transactionState': "SUCCESS"});
} else if (this.props.transactions[this.txHash].status === "error"){
/* Transaction failed to complete */
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'red'}}>
<strong>
Transaction failed to complete with error:
</strong>
</span>);
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'red'}}>
<strong>
{this.props.transactions[this.txHash].error}
</strong>
</span>);
this.setState({
'transactionState': "ERROR",
'transactionOutputTimerActive': true
});
this.transactionOutputTimer = setTimeout(() => {
this.transactionProgressText = [];
this.setState({
'creatingTopic': false,
'transactionState': null,
'savingToOrbitDB': null,
'transactionOutputTimerActive': false
});
}, 5000);
}
}
else if (this.state.transactionState === "SUCCESS") {
/* Transaction completed successfully */
//Tries to store data in OrbitDB
this.pushToDatabase();
if (this.state.savingToOrbitDB === "SUCCESS"){
/* Data successfully saved in OrbitDB */
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'green'}}>
<strong>
Topic successfully saved in OrbitDB.
</strong>
</span>);
this.setState({'transactionOutputTimerActive': true});
this.transactionOutputTimer = setTimeout(() => {
this.transactionProgressText = [];
this.setState({
'creatingTopic': false,
'transactionState': null,
'savingToOrbitDB': null,
'transactionOutputTimerActive': false
});
this.props.router.push("/topic/" + this.topicIDFetched);
}, 5000);
}
else if (this.state.savingToOrbitDB === "ERROR"){
/* Failed to save data in OrbitDB */
//Updates output and state
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'red'}}>
<strong>
An error occurred while trying to save post in OrbitDB.
</strong>
</span>);
this.setState({'transactionOutputTimerActive': true});
this.transactionOutputTimer = setTimeout(() => {
this.transactionProgressText = [];
this.setState({
'creatingTopic': false,
'transactionState': null,
'savingToOrbitDB': null,
'transactionOutputTimerActive': false
});
}, 5000);
}
}
else if (this.state.transactionState === "ACCEPTANCE_PENDING" &&
this.props.transactions.undefined !== undefined &&
this.props.transactions.undefined.status === "error"){
/* User probably canceled the transaction */
//TODO user can't post after this!
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'orange'}}>
<strong>
Transaction canceled.
</strong>
</span>);
this.setState({'transactionState': "SUCCESS"});
this.setState({'transactionOutputTimerActive': true});
this.transactionOutputTimer = setTimeout(() => {
this.transactionProgressText = [];
this.setState({
'creatingTopic': false,
'transactionState': null,
'savingToOrbitDB': null,
'transactionOutputTimerActive': false
});
}, 5000);
}
}
}
}
StartTopic.contextTypes = {
drizzle: PropTypes.object,
router: PropTypes.object
};

94
src/containers/TopicContainer.js

@ -1,47 +1,44 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { drizzleConnect } from 'drizzle-react';
import WithBlockchainData from '../components/WithBlockchainData';
import PostList from '../components/PostList';
import NewPost from '../components/NewPost';
import FloatingButton from '../components/FloatingButton';
import LoadingSpinner from '../components/LoadingSpinner';
import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions';
class Topic extends Component {
constructor(props, context) {
constructor(props) {
super(props);
this.props.store.dispatch(showProgressBar());
this.fetchTopicSubject = this.fetchTopicSubject.bind(this);
this.handleClick = this.handleClick.bind(this);
this.togglePostingState = this.togglePostingState.bind(this);
this.postCreated = this.postCreated.bind(this);
this.drizzle = context.drizzle;
this.state = {
topicID: this.props.params.topicId,
topicSubject: this.props.params.topicSubject ? this.props.params.topicSubject : null,
topicSubject: null,
postFocus: this.props.params.postId && /^[0-9]+$/.test(this.props.params.postId)
? this.props.params.postId
: null,
getPostsTransactionState: null,
fetchTopicSubjectStatus: null,
posting: false
};
}
async fetchTopicSubject(orbitDBAddress) {
/*const fullAddress = this.topicsData[this.state.topicID][1];
const store = await this.props.orbitDB.orbitdb.keyvalue(JSON.stringify(fullAddress));
await store.load();
var som = store.get(JSON.stringify(this.state.topicID));
this.topicsSubjects[this.state.topicID] = som['subject'];
this.topicsSubjectsFetchStatus[this.state.topicID] = "fetched";*/
var som =this.props.orbitDB.topicsDB.get(this.state.topicID);
this.setState({'topicSubject': som['subject']});
var orbitData =this.props.orbitDB.topicsDB.get(this.state.topicID);
this.props.store.dispatch(hideProgressBar());
this.setState({
'topicSubject': orbitData['subject'],
fetchTopicSubjectStatus: "fetched"
});
}
handleClick(event) {
togglePostingState(event) {
if (event){
event.preventDefault();
}
@ -52,31 +49,28 @@ class Topic extends Component {
postCreated(){
this.setState(prevState => ({
getPostsTransactionState: null,
posting: false
}));
//TODO reload topic
}
render() {
var topicContents;
if (this.state.getPostsTransactionState !== "SUCCESS") {
topicContents = (
<LoadingSpinner/>
);
} else {
if (this.props.blockchainData[0].status === "success") {
topicContents = (
(<div style={{marginBottom: '100px'}}>
{this.postList}
(<div>
<PostList postIDs={this.props.blockchainData[0].returnData[4]}/>
{this.state.posting &&
<NewPost topicID={this.state.topicID}
subject={this.state.topicSubject}
postIndex={this.props.blockchainData[0].returnData[4].length}
onCancelClick={() => {this.handleClick()}}
onCancelClick={() => {this.togglePostingState()}}
onPostCreated={() => {this.postCreated()}}
/>
}
<div className="posts-list-spacer"></div>
{this.props.user.hasSignedUp && !this.state.posting &&
<FloatingButton onClick={this.handleClick}/>
<FloatingButton onClick={this.togglePostingState}/>
}
</div>)
)
@ -92,32 +86,16 @@ class Topic extends Component {
);
}
componentWillReceiveProps() {
componentDidUpdate() {
if (this.props.blockchainData[0].status === "success") {
if (this.state.getPostsTransactionState !== "SUCCESS"){
this.postList = <WithBlockchainData
component={PostList}
callsInfo={this.props.blockchainData[0].returnData[4].map((postID) => {
return {
contract: 'Forum',
method: 'getPost',
params: [postID]
}
})}
postIDs={this.props.blockchainData[0].returnData[4]}
/>
this.setState({'getPostsTransactionState': "SUCCESS"});
if (this.state.fetchTopicSubjectStatus === null){
this.setState({ fetchTopicSubjectStatus: "fetching"})
this.fetchTopicSubject(this.props.blockchainData[0].returnData[0]);
}
}
}
}
Topic.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
user: state.user,
@ -132,20 +110,20 @@ class TopicContainer extends Component {
if (!/^[0-9]+$/.test(props.params.topicId)){ //Topic ID should be a positive integer
this.props.router.push("/404");
}
this.topic = <WithBlockchainData
component={drizzleConnect(Topic, mapStateToProps)}
callsInfo={[{
}
render() {
return(
<WithBlockchainData
component={drizzleConnect(Topic, mapStateToProps)}
callsInfo={[{
contract: 'Forum',
method: 'getTopic',
params: [this.props.params.topicId]
}]}
params={this.props.params}
/>;
}
render() {
return(this.topic);
params={this.props.params}
/>
);
}
}

172
src/containers/TransactionsMonitorContainer.js

@ -0,0 +1,172 @@
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import { Message } from 'semantic-ui-react';
import { updateTransaction } from '../redux/actions/transactionsMonitorActions';
class RightSideBar extends Component {
constructor(props, context) {
super(props);
this.handleMessageDismiss = this.handleMessageDismiss.bind(this);
this.drizzle = context.drizzle;
this.transactionsStackIds = [];
this.transactionsTxHashes = [];
this.state = {
transactionsCompletionTime: [],
isTransactionMessageActive: []
}
}
handleMessageDismiss(messageIndex) {
let isTransactionMessageActiveShallowCopy = this.state.isTransactionMessageActive.slice();
isTransactionMessageActiveShallowCopy[messageIndex] = false;
this.setState({
isTransactionMessageActive: isTransactionMessageActiveShallowCopy
});
}
render() {
let transactionMessages = this.props.transactionsQueue.map((transaction, index) => {
if (!this.state.isTransactionMessageActive[index]){
return null;
}
let color = 'black';
let message = [];
while(true) {
if (transaction.status === 'initialized') break;
message.push("New transaction has been queued and is waiting your confirmation.");
if (transaction.status === 'acceptance_pending') break;
message.push(<br key="confirmed"/>);
message.push("- transaction confirmed");
if (transaction.status === 'mining_pending') break;
message.push(<br key="mined"/>);
message.push("- transaction mined");
if (transaction.status === 'success') {
color = 'green';
message.push(<br key="success"/>);
message.push("- transaction completed successfully");
break;
}
if (transaction.status === 'error') {
color = 'red';
message.push(<br key="fail"/>);
message.push("Transaction failed to complete!");
break;
}
}
return (
<div className="sidebar-message" key={index}>
<Message color={color} onDismiss={() => {this.handleMessageDismiss(index)}}>
{message}
</Message>
</div>
);
});
return (transactionMessages);
}
componentDidUpdate(){
for (var index = 0; index < this.props.transactionsQueue.length; ++index) {
let transaction = this.props.transactionsQueue[index];
if (transaction.status === 'initialized' &&
this.transactionsStackIds[index] === undefined){
/* User submitted a new transaction */
let isTransactionMessageActiveShallowCopy = this.state
.isTransactionMessageActive.slice();
isTransactionMessageActiveShallowCopy[index] = true;
this.setState({
isTransactionMessageActive: isTransactionMessageActiveShallowCopy
});
this.transactionsStackIds[index] = (this.drizzle
.contracts[transaction.contract]
.methods[transaction.method]
.cacheSend(...(transaction.params)));
this.props.store.dispatch(updateTransaction(index, {
status: 'acceptance_pending'
}));
} else if (transaction.status === 'acceptance_pending'){
if (this.props.transactionStack[this.transactionsStackIds[index]]){
/* User confirmed the transaction */
//Gets transaciton's hash
this.transactionsTxHashes[index] = (this.props
.transactionStack[this.transactionsStackIds[index]]);
this.props.store.dispatch(updateTransaction(index, {
status: 'mining_pending'
}));
}
} else if (transaction.status === 'mining_pending'){
if (this.props.transactions[this.transactionsTxHashes[index]]
.status === "success"){
/* Transaction completed successfully */
//Gets returned data by contract
let data = this.props.transactions[this.transactionsTxHashes[index]]
.receipt.events[transaction.event].returnValues;
this.props.store.dispatch(updateTransaction(index, {
status: 'success',
returnData: data
}));
let transactionsCompletionTimeShallowCopy = this.state
.transactionsCompletionTime.slice();
transactionsCompletionTimeShallowCopy[index] = new Date().getTime();
this.setState({
transactionsCompletionTime: transactionsCompletionTimeShallowCopy
});
if (this.props.transactionsQueue[index].callback){
this.props.transactionsQueue[index].callback(data);
}
} else if (this.props.transactions[this.transactionsTxHashes[index]]
.status === "error"){
/* Transaction failed to complete */
this.props.store.dispatch(updateTransaction(index, {
status: 'error'
}));
let transactionsCompletionTimeShallowCopy = this.state
.transactionsCompletionTime.slice();
transactionsCompletionTimeShallowCopy[index] = new Date().getTime();
this.setState({
transactionsCompletionTime: transactionsCompletionTimeShallowCopy
});
if (this.props.transactionsQueue[index].callback){
this.props.transactionsQueue[index].callback(null);
}
}
}
}
}
}
RightSideBar.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
transactionsQueue: state.transactionsQueue.transactions,
transactions: state.transactions,
transactionStack: state.transactionStack
}
};
const RightSideBarContainer = drizzleConnect(RightSideBar, mapStateToProps);
export default RightSideBarContainer;

33
src/containers/UsernameFormContainer.js

@ -3,11 +3,12 @@ import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import { Button, Message, Form, Dimmer, Loader, Header } from 'semantic-ui-react';
import { createDatabases } from './../util/orbit';
import { updateUsername } from '../redux/actions/transactionsMonitorActions';
const contract = "Forum";
const signUpMethod = "signUp";
const updateUsernameMethod ="updateUsername";
class UsernameFormContainer extends Component {
constructor(props, context) {
@ -22,7 +23,7 @@ class UsernameFormContainer extends Component {
this.state = {
usernameInput: '',
error: false,
completingAction: false
signingUp: false
};
}
@ -39,14 +40,26 @@ class UsernameFormContainer extends Component {
}
async completeAction() {
this.setState({ completingAction: true });
if(this.props.user.hasSignedUp)
this.contracts[contract].methods[updateUsernameMethod].cacheSend(...[this.state.usernameInput]);
else
{
if(this.props.user.hasSignedUp){
this.props.store.dispatch(updateUsername(...[this.state.usernameInput], null));
} else {
this.setState({ signingUp: true });
const orbitdbInfo = await createDatabases();
this.contracts[contract].methods[signUpMethod].cacheSend(...[this.state.usernameInput, orbitdbInfo.id,
orbitdbInfo.topicsDB, orbitdbInfo.postsDB, orbitdbInfo.publicKey, orbitdbInfo.privateKey]);
this.contracts[contract].methods[signUpMethod]
.cacheSend(...[this.state.usernameInput,
orbitdbInfo.id,
orbitdbInfo.topicsDB,
orbitdbInfo.postsDB,
orbitdbInfo.publicKey,
orbitdbInfo.privateKey
]);
}
this.setState({ usernameInput: '' });
}
componentWillReceiveProps(nextProps){
if (this.state.signingUp && nextProps.user.hasSignedUp){
this.props.signedUp();
}
}
@ -77,7 +90,7 @@ class UsernameFormContainer extends Component {
/>
<Button type='submit'>{buttonText}</Button>
</Form>
<Dimmer active={this.state.completingAction} page>
<Dimmer active={this.state.signingUp} page>
<Header as='h2' inverted>
<Loader size='large'>Magic elfs are processing your nobel request.</Loader>
</Header>

19
src/layouts/CoreLayout/CoreLayout.js

@ -1,5 +1,8 @@
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import NavBar from '../../components/NavBar';
import TransactionsMonitorContainer from '../../containers/TransactionsMonitorContainer';
// Styles
import '../../assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js';
@ -11,12 +14,19 @@ import '../../assets/css/board-container.css';
import '../../assets/css/start-topic-container.css';
import '../../assets/css/topic-container.css';
import '../../assets/css/profile-container.css';
import '../../assets/css/progress-bar.css';
class CoreLayout extends Component {
render() {
return (
<div className="App">
<NavBar/>
<div className="progress-bar-container"
style={{display: this.props.isProgressBarVisible ? "block" : "none"}}>
<div className="progress">
<div className="indeterminate"></div>
</div>
</div>
<div className="page-container">
<aside className="left-side-panel">
</aside>
@ -26,6 +36,7 @@ class CoreLayout extends Component {
</div>
</div>
<aside className="right-side-panel">
<TransactionsMonitorContainer/>
</aside>
</div>
</div>
@ -33,4 +44,10 @@ class CoreLayout extends Component {
}
}
export default CoreLayout;
const mapStateToProps = state => {
return {
isProgressBarVisible: state.interface.displayProgressBar
}
};
export default drizzleConnect(CoreLayout, mapStateToProps)

54
src/redux/actions/transactionsMonitorActions.js

@ -0,0 +1,54 @@
//Action creators
export const INIT_TRANSACTION = 'INIT_TRANSACTION';
export const UPDATE_TRANSACTION = 'UPDATE_TRANSACTION';
export function updateUsername(newUsername, callback){
return {
type: INIT_TRANSACTION,
transactionDescriptor:
{
contract: 'Forum',
method: 'updateUsername',
params: [newUsername],
event: 'UsernameUpdated',
},
callback: callback
};
}
export function createTopic(callback){
return {
type: INIT_TRANSACTION,
transactionDescriptor:
{
contract: 'Forum',
method: 'createTopic',
params: [],
event: 'TopicCreated',
},
callback: callback
};
}
export function createPost(topicID, callback){
return {
type: INIT_TRANSACTION,
transactionDescriptor:
{
contract: 'Forum',
method: 'createPost',
params: [topicID],
event: 'PostCreated',
},
callback: callback
};
}
export function updateTransaction(transactionIndex, updateDescriptor){
return {
type: UPDATE_TRANSACTION,
index: transactionIndex,
transactionUpdates: updateDescriptor
};
}

12
src/redux/actions/userInterfaceActions.js

@ -0,0 +1,12 @@
//Action creators
export const SHOW_PROGRESS_BAR = 'SHOW_PROGRESS_BAR';
export const HIDE_PROGRESS_BAR = 'HIDE_PROGRESS_BAR';
export function showProgressBar(){
return { type: 'SHOW_PROGRESS_BAR'};
}
export function hideProgressBar(){
return { type: 'HIDE_PROGRESS_BAR'};
}

5
src/redux/reducer/reducer.js

@ -4,12 +4,17 @@ import { drizzleReducers } from 'drizzle';
import userReducer from "./userReducer";
import contractReducer from "./contractReducer";
import orbitReducer from "../../util/orbitReducer";
import userInterfaceReducer from "./userInterfaceReducer";
import transactionsMonitorReducer from "./transactionsMonitorReducer";
const reducer = combineReducers({
routing: routerReducer,
user: userReducer,
orbitDB: orbitReducer,
forumContract: contractReducer,
interface: userInterfaceReducer,
transactionsQueue: transactionsMonitorReducer,
...drizzleReducers
});

39
src/redux/reducer/transactionsMonitorReducer.js

@ -0,0 +1,39 @@
import { INIT_TRANSACTION, UPDATE_TRANSACTION } from '../actions/transactionsMonitorActions';
const initialState = {
transactions: []
};
const transactionsReducer = (state = initialState, action) => {
switch (action.type) {
case INIT_TRANSACTION:
let transactionsShallowCopy = state.transactions.slice();
transactionsShallowCopy.push({
status: 'initialized',
contract: action.transactionDescriptor.contract,
method: action.transactionDescriptor.method,
params: action.transactionDescriptor.params,
event: action.transactionDescriptor.event,
returnData: null,
callback: action.callback
});
return {
transactions: transactionsShallowCopy
};
case UPDATE_TRANSACTION:
return { transactions: state.transactions.map( (transaction, index) => {
if (index !== action.index){
return transaction;
}
return {
...transaction,
...action.transactionUpdates
}
})};
default:
return state;
}
};
export default transactionsReducer;

22
src/redux/reducer/userInterfaceReducer.js

@ -0,0 +1,22 @@
import { SHOW_PROGRESS_BAR, HIDE_PROGRESS_BAR } from '../actions/userInterfaceActions';
const initialState = {
displayProgressBar: false
};
const userInterfaceReducer = (state = initialState, action) => {
switch (action.type) {
case SHOW_PROGRESS_BAR:
return {
displayProgressBar: true
};
case HIDE_PROGRESS_BAR:
return {
displayProgressBar: false
};
default:
return state;
}
};
export default userInterfaceReducer;
Loading…
Cancel
Save