Browse Source

Add contract and OrbitDB interaction in StartTopic, BoardContainer and TopicList

develop
Apostolos Fanakis 7 years ago
parent
commit
dfcb139e3c
  1. 33
      contracts/Forum.sol
  2. 33
      src/assets/css/App.css
  3. 4
      src/components/FloatingButton.js
  4. 124
      src/components/StartTopic.js
  5. 28
      src/components/Topic.js
  6. 159
      src/components/TopicList.js
  7. 61
      src/containers/BoardContainer.js
  8. 7
      src/util/drizzleOptions.js
  9. 53
      src/util/orbit.js
  10. 12
      src/util/orbitReducer.js

33
contracts/Forum.sol

@ -55,7 +55,7 @@ contract Forum {
return false;
}
//---------OrbitDB---------
//----------------------------------------OrbitDB----------------------------------------
struct OrbitDB {
string id; // TODO: set an upper bound instead of arbitrary string
string topicsDB; //TODO: not sure yet which of these are actually needed
@ -123,11 +123,17 @@ contract Forum {
mapping (uint => Topic) topics;
mapping (uint => Post) posts;
event TopicCreated(uint topicID);
event PostCreated(uint postID, uint topicID);
event NumberOfTopicsReceived(uint numTopics);
event TopicReceived(string orbitTopicsDB, address author, string username, uint timestamp, uint[] postIDs);
function createTopic() public returns (uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create topics
uint topicID = numTopics++;
topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0));
users[msg.sender].topicIDs.push(topicID);
emit TopicCreated(topicID);
return topicID;
}
@ -138,10 +144,33 @@ contract Forum {
posts[postID] = Post(postID, msg.sender, block.timestamp);
topics[topicID].postIDs.push(postID);
users[msg.sender].postIDs.push(postID);
emit PostCreated(postID, topicID);
return postID;
}
function getTopicPosts (uint topicID) public view returns (uint[]) {
function getNumberOfTopics() public view returns (uint) {
emit NumberOfTopicsReceived(numTopics);
return numTopics;
}
function getTopic(uint topicID) public view returns (string, address, string, uint, uint[]) {
//require(hasUserSignedUp(msg.sender)); needed?
require(topicID<numTopics);
emit TopicReceived(getOrbitTopicsDB(topics[topicID].author),
topics[topicID].author,
users[topics[topicID].author].username,
topics[topicID].timestamp,
topics[topicID].postIDs);
return (
getOrbitTopicsDB(topics[topicID].author),
topics[topicID].author,
users[topics[topicID].author].username,
topics[topicID].timestamp,
topics[topicID].postIDs
);
}
function getTopicPosts(uint topicID) public view returns (uint[]) {
require(topicID<numTopics); // Topic should exist
return topics[topicID].postIDs;
}

33
src/assets/css/App.css

@ -173,6 +173,10 @@ body,
height: 200px;
}
.form-input-required {
border-color: red !important;
}
.markdownPreview {
padding: 0px 20px;
}
@ -336,4 +340,33 @@ code {
a {
color:inherit;
text-decoration: none;
}
.center-in-parent {
width: 100%;
height: 100%;
text-align: center;
}
#overlay {
position: fixed;
display: block;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 2;
}
#overlay-content{
position: absolute;
text-align: center;
top: 50%;
left: 50%;
color: white;
transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
}

4
src/components/FloatingButton.js

@ -2,8 +2,8 @@ import React from 'react';
const FloatingButton = (props) => {
return (
<div className="pure-u-1-1">
<p className="no-margin floating-button" data-fa-transform="down-6" onClick={props.onClick}>
<div className="pure-u-1-1" onClick={props.onClick}>
<p className="no-margin floating-button" data-fa-transform="down-6">
<i className="fa fa-plus fa-2x"></i>
</p>
</div>

124
src/components/StartTopic.js

@ -1,10 +1,11 @@
import { drizzleConnect } from 'drizzle-react'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Post from './Post'
const contract = "Forum";
const startTopicMethod = "createTopic";
const contractMethod = "createTopic";
class StartTopic extends Component {
constructor(props, context) {
@ -12,28 +13,49 @@ class StartTopic extends Component {
this.handleInputChange = this.handleInputChange.bind(this);
this.handlePreviewToggle = this.handlePreviewToggle.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.validateAndPost = this.validateAndPost.bind(this);
this.pushToDatabase = this.pushToDatabase.bind(this);
this.transactionProgressText = [];
this.drizzle = context.drizzle;
this.drizzleState = this.drizzle.store.getState();
this.contracts = this.drizzle.contracts;
this.abi = this.contracts[contract].abi;
this.state = {
topicSubjectInput: '',
topicMessageInput: '',
topicSubjectInputEmptySubmit: false,
topicMessageInputEmptySubmit: false,
previewEnabled: false,
previewDate: ""
previewDate: "",
creatingTopic: false,
transactionState: null,
savingToOrbitDB: null
};
}
async handleSubmit() {
console.log("contracts:");
console.log(this.contracts);
console.log("DS contracts:");
console.log(this.drizzleState.contracts);
async validateAndPost() {
if (this.state.topicSubjectInput === '' || this.state.topicMessageInput === ''){
this.setState({
topicSubjectInputEmptySubmit: this.state.topicSubjectInput === '',
topicMessageInputEmptySubmit: this.state.topicMessageInput === ''
});
return;
}
this.stackId = this.drizzle.contracts[contract].methods[contractMethod].cacheSend();
this.transactionProgressText.push(<br/>);
this.transactionProgressText.push("Waiting for transaction acceptance...");
this.setState({
'creatingTopic': true,
'transactionState': "ACCEPTANCE_PENDING"
});
}
this.dataKey = this.drizzleState.contracts[contract].methods[startTopicMethod].cacheCall();
//TODO get return value and pass it to orbit
async pushToDatabase() {
await this.props.orbitDB.topicsDB.put(this.topicIDFetched, {
subject: this.state.topicSubjectInput,
content: this.state.topicMessageInput
});
this.setState({'savingToOrbitDB': "SUCCESS"});
}
handleInputChange(event) {
@ -49,23 +71,25 @@ class StartTopic extends Component {
getDate() {
const currentdate = new Date();
return ((currentdate.getMonth() + 1) + " "
return ((currentdate.getMonth() + 1) + " "
+ currentdate.getDate() + ", "
+ currentdate.getFullYear() + ", "
+ currentdate.getHours() + ":"
+ currentdate.getMinutes() + ":"
+ currentdate.getFullYear() + ", "
+ currentdate.getHours() + ":"
+ currentdate.getMinutes() + ":"
+ currentdate.getSeconds());
}
render() {
if(this.dataKey) {
/*console.log(this.drizzleState);*/
if (this.drizzleState.contracts[contract]) {
console.log(this.drizzleState.contracts[contract].storedData[this.dataKey].value);
}
}
return(
<div>
{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 className="pure-u-1-1 start-topic-back-button">
<p className="no-margin" onClick={this.props.onClick}>
<i className="fas fa-arrow-left fa-3x"></i>
@ -83,6 +107,7 @@ class StartTopic extends Component {
[
<input key={"topicSubjectInput"}
name={"topicSubjectInput"}
className={this.state.topicSubjectInputEmptySubmit && "form-input-required"}
type="text"
value={this.state.topicSubjectInput}
placeholder="Subject"
@ -90,6 +115,7 @@ class StartTopic extends Component {
onChange={this.handleInputChange} />,
<textarea key={"topicMessageInput"}
name={"topicMessageInput"}
className={this.state.topicMessageInputEmptySubmit && "form-input-required"}
value={this.state.topicMessageInput}
placeholder="Post"
id="topicMessageInput"
@ -98,7 +124,7 @@ class StartTopic extends Component {
<button key="submit"
className="pure-button"
type="button"
onClick={this.handleSubmit}>
onClick={this.validateAndPost}>
Post
</button>
<button className="pure-button margin-left-small"
@ -110,6 +136,54 @@ class StartTopic extends Component {
</div>
);
}
componentWillReceiveProps(){ //Maybe change it with this: https://redux.js.org/api-reference/store#subscribe
let currentDrizzleState = this.drizzle.store.getState();
if(this.state.creatingTopic){
if (this.state.transactionState === "ACCEPTANCE_PENDING" &&
currentDrizzleState.transactionStack[this.stackId]) {
this.txHash = currentDrizzleState.transactionStack[this.stackId];
this.transactionProgressText.push(<br/>);
this.transactionProgressText.push("Transaction in progress: txHash = " + this.txHash);
this.setState({'transactionState': "IN_PROGRESS"});
} else if (this.state.transactionState === "IN_PROGRESS") {
if (currentDrizzleState.transactions[this.txHash].status === "success"){
this.topicIDFetched = currentDrizzleState.transactions[this.txHash].receipt
.events.TopicCreated.returnValues.topicID;
this.transactionProgressText.push(<br/>);
this.transactionProgressText.push("Transaction completed successfully.");
this.transactionProgressText.push(<br/>);
this.transactionProgressText.push("TopicID = " + this.topicIDFetched);
this.setState({'transactionState': "SUCCESS"});
} else if (currentDrizzleState.transactions[this.txHash].status === "error"){
this.transactionProgressText.push(<br/>);
this.transactionProgressText.push("Transaction failed to complete.");
this.setState({'transactionState': "ERROR"});
}
} else if (this.state.transactionState === "SUCCESS") {
this.pushToDatabase();
if (this.state.savingToOrbitDB === "SUCCESS"){
this.transactionProgressText.push(<br/>);
this.transactionProgressText.push("Post successfully saved in OrbitDB.");
this.setState({creatingTopic: false});
} else if (this.state.savingToOrbitDB === "ERROR"){
this.transactionProgressText.push(<br/>);
this.transactionProgressText.push(<span style={{color: 'red'}}><strong>
An error occurred while trying to save post in OrbitDB.
</strong></span>);
this.setState({creatingTopic: false});
}
} else if (this.state.transactionState === "ERROR"){
this.transactionProgressText.push(<br/>);
this.transactionProgressText.push(<span style={{color: 'red'}}><strong>
An error occurred while trying to complete transaction.
</strong></span>);
this.setState({creatingTopic: false});
}
}
}
}
StartTopic.contextTypes = {
@ -118,7 +192,7 @@ StartTopic.contextTypes = {
const mapStateToProps = state => {
return {
contracts: state.contracts,
orbitDB: state.orbitDB,
user: state.user
}
};

28
src/components/Topic.js

@ -3,15 +3,27 @@ import TimeAgo from 'react-timeago';
const Topic = (props) => {
return (
<div className="topic card">
<p className="topic-subject"><strong>{props.topicSubject}</strong></p>
<hr/>
<div className="topic-meta">
<p className="no-margin">{props.topicStarter}</p>
<p className="no-margin">Number of replies: {props.numberOfReplies}</p>
<p className="topic-date">Started <TimeAgo date={props.date}/></p>
props.topic !== null
? <div className={"topic card"}>
<p className="topic-subject" style={{color: props.topic.topicSubject ? "" : "grey"}}>
<strong>{props.topic.topicSubject ? props.topic.topicSubject : "Subject"}</strong>
</p>
<hr/>
<div className="topic-meta">
<p className="no-margin">{props.topic.topicStarter}</p>
<p className="no-margin">Number of replies: {props.topic.numberOfReplies}</p>
<p className="topic-date">Started <TimeAgo date={props.topic.date}/></p>
</div>
</div>
: <div className={"topic card"} style={{color: 'grey'}}>
<p className="topic-subject"><strong>Subject</strong></p>
<hr/>
<div className="topic-meta">
<p className="no-margin">Username</p>
<p className="no-margin">Number of replies: </p>
<p className="topic-date">Started </p>
</div>
</div>
</div>
);
};

159
src/components/TopicList.js

@ -1,50 +1,119 @@
import React from 'react';
import Topic from './Topic';
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import { Link } from 'react-router';
import PropTypes from 'prop-types'
import Topic from './Topic';
const contract = "Forum";
const contractMethod = "getTopic";
class TopicList extends Component {
constructor(props, context) {
super(props);
this.fetchSubject = this.fetchSubject.bind(this);
this.correctTimeFormat = this.correctTimeFormat.bind(this);
this.drizzle = context.drizzle;
this.dataKeys = [];
this.topicsData = new Array(parseInt(this.props.numberOfTopics, 10)).fill(undefined);
this.topicsSubjects = [];
this.topicsSubjectsFetched = [];
for (var i = 0; i < this.props.numberOfTopics; ++i){
this.dataKeys[i] = this.drizzle.contracts[contract].methods[contractMethod].cacheCall(i);
}
this.state = {
};
}
async fetchSubject(topicID) {
/*const fullAddress = this.topicsData[topicID][1];
const store = await this.props.orbitDB.orbitdb.keyvalue(JSON.stringify(fullAddress));
await store.load();
var som = store.get(JSON.stringify(topicID));
this.topicsSubjects[topicID] = som['subject'];
this.topicsSubjectsFetched[topicID] = true;*/
var som =this.props.orbitDB.topicsDB.get(JSON.stringify(topicID));
this.topicsSubjects[topicID] = som['subject'];
this.topicsSubjectsFetched[topicID] = true;
}
correctTimeFormat(timestamp) {
var timestampDate = new Date(0);
timestampDate.setUTCSeconds(timestamp);
return ((timestampDate.getMonth() + 1) + " "
+ timestampDate.getDate() + ", "
+ timestampDate.getFullYear() + ", "
+ timestampDate.getHours() + ":"
+ timestampDate.getMinutes() + ":"
+ timestampDate.getSeconds())
}
render (){
const topics = this.topicsData.map((topic, index) => {
if (topic){
return (
<Link to={"/topic/" + index + "/" +
((this.topicsSubjects[index] !== undefined) ? this.topicsSubjects[index] : "")}
key={index}>
<Topic topic={{
topicSubject: ((this.topicsSubjects[index] !== undefined) && this.topicsSubjects[index]),
topicStarter: topic[2],
numberOfReplies: topic[4].length,
date: this.correctTimeFormat(topic[3])
}}
id={index}
key={index}
address={topic[1]}/>
</Link>
);
} else {
return (
<Link to={"/topic/" + index + "/"}
key={index}>
<Topic topic={null}
id={index}
key={index}/>
</Link>
);
}
});
return (
<div className="topics-list">
{topics}
</div>
);
}
componentWillReceiveProps() {
for (var i = 0; i < this.props.numberOfTopics; ++i){
if (this.topicsData[i] === undefined) {
let currentDrizzleState = this.drizzle.store.getState();
let dataFetched = (currentDrizzleState.contracts[contract][contractMethod])[this.dataKeys[i]];
if (dataFetched){
this.topicsData[i] = dataFetched.value;
}
} else if (!this.topicsSubjects[i] && !this.topicsSubjectsFetched[i]) {
this.fetchSubject(i);
}
}
}
};
TopicList.contextTypes = {
drizzle: PropTypes.object
};
const topics1 = [
{topicSubject: 'This is a topic about something 1',
topicStarter: 'username1',
numberOfReplies: 12,
date: 'May 20, 2018, 10:10:10',
id: 1,
address: 0x5fe3062B24033113fbf52b2b75882890D7d8CA54
},
{topicSubject: 'This is a topic about something 2',
topicStarter: 'username2',
numberOfReplies: 41,
date: 'May 20, 2018, 10:10:10',
id: 2,
address: 0x083c41ea13af6c2d5aaddf6e73142eb9a7b00183
},
{topicSubject: 'This is a topic about something 3',
topicStarter: 'username3',
numberOfReplies: 73,
date: 'May 20, 2018, 10:10:10',
id: 3,
address: 0x26d1ec50b4e62c1d1a40d16e7cacc6a6580757d5
const mapStateToProps = state => {
return {
user: state.user, //Needed!!
orbitDB: state.orbitDB,
}
];
const TopicList = (props) => {
const topics = topics1.map((topic) =>
<Link to={"/topic/" + topic.id + "/" + topic.topicSubject}
key={topic.id}>
<Topic topicSubject={topic.topicSubject}
topicStarter={topic.topicStarter}
numberOfReplies={topic.numberOfReplies}
date={topic.date}
id={topic.id}
key={topic.id}
address={topic.address}/>
</Link>
);
return (
<div className="topics-list">
{topics}
</div>
);
};
export default TopicList;
export default drizzleConnect(TopicList, mapStateToProps);

61
src/containers/BoardContainer.js

@ -1,48 +1,83 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import TopicList from '../components/TopicList';
import FloatingButton from '../components/FloatingButton';
import StartTopic from '../components/StartTopic';
const contract = "Forum";
const contractMethod = "getNumberOfTopics";
class Board extends Component {
constructor(props) {
constructor(props, context) {
super(props);
this.state = {
startingNewTopic: false
};
this.handleClick = this.handleClick.bind(this);
this.drizzle = context.drizzle;
this.dataKey = this.drizzle.contracts[contract].methods[contractMethod].cacheCall();
this.state = {
startingNewTopic: false,
transactionState: "IN_PROGRESS"
};
}
handleClick(event) {
event.preventDefault();
event.preventDefault();
this.setState(prevState => ({
startingNewTopic: !prevState.startingNewTopic
}));
}
render() {
var boardContents;
if (this.state.transactionState === "IN_PROGRESS") {
boardContents = (
<div className="center-in-parent">
<p>
<i className="fas fa-spinner fa-3x fa-spin"></i>
</p>
</div>
);
} else {
boardContents = <TopicList numberOfTopics={this.numberOfTopics}/>;
}
return (
this.state.startingNewTopic
?(<div>
<StartTopic onClick={this.handleClick}/>
</div>)
:(<div style={{marginBottom: '100px'}}>
<TopicList/>
{boardContents}
<FloatingButton onClick={this.handleClick}/>
</div>)
);
}
componentWillReceiveProps() {
if (!this.numberOfTopics) {
let currentDrizzleState = this.drizzle.store.getState()
let dataFetched = (currentDrizzleState.contracts[contract][contractMethod])[this.dataKey];
if (dataFetched){
this.numberOfTopics = dataFetched.value
this.setState({'transactionState': "SUCCESS"});
}
}
}
}
Board.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
accounts: state.accounts,
Forum: state.contracts.Forum,
user: state.user,
orbitDB: state.orbitDB,
drizzleStatus: state.drizzleStatus
}
return {
user: state.user,
orbitDB: state.orbitDB,
}
};
const BoardContainer = drizzleConnect(Board, mapStateToProps);

7
src/util/drizzleOptions.js

@ -11,7 +11,12 @@ const drizzleOptions = {
Forum
],
events: {
Forum: ['UserSignedUp', 'UsernameUpdated']
Forum: ['UserSignedUp',
'UsernameUpdated',
'TopicCreated',
'PostCreated',
'NumberOfTopicsReceived',
'TopicReceived']
},
polls: {
accounts: 3000,

53
src/util/orbit.js

@ -11,11 +11,29 @@ import store from './../redux/store';
const ipfsOptions = {
EXPERIMENTAL: {
pubsub: true
}, config: {
Addresses: {
Swarm: []
}
},
};
/*,
config: {
Addresses: {
Swarm: [
// Use IPFS dev signal server
// '/dns4/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star',
'/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star',
// Use local signal server
// '/ip4/0.0.0.0/tcp/9090/wss/p2p-webrtc-star',
]
}
}*/
// Create IPFS instance
const ipfs = new IPFS(ipfsOptions);
let orbitdb, topicsDB, postsDB;
ipfs.on('ready', async () => {
store.dispatch({type: "IPFS_INITIALIZED"});
@ -23,26 +41,41 @@ ipfs.on('ready', async () => {
async function createDatabases() {
const orbitdb = new OrbitDB(ipfs);
const topicsDB = await orbitdb.keyvalue('topics');
const postsDB = await orbitdb.keyvalue('posts');
store.dispatch({type: "DATABASES_CREATED", id: orbitdb.id});
orbitdb = new OrbitDB(ipfs);
topicsDB = await orbitdb.keyvalue('topics');
postsDB = await orbitdb.keyvalue('posts');
store.dispatch({
type: "DATABASES_CREATED",
orbitdb: orbitdb,
topicsDB: topicsDB,
postsDB: postsDB,
id: orbitdb.id
});
return {id: orbitdb.id, topicsDB: topicsDB.address.root, postsDB: postsDB.address.root,
publicKey: orbitdb.key.getPublic('hex'), privateKey:orbitdb.key.getPrivate('hex')};
}
async function loadDatabases(id,topicsDB, postsDB,publicKey,privateKey) { //TODO: does this work? does IPFS need reinitializng?
async function loadDatabases(id,mTopicsDB, mPostsDB,publicKey,privateKey) {
let directory = "./orbitdb";
let keystore = Keystore.create(path.join(directory, id, '/keystore'));
keystore._storage.setItem(id, JSON.stringify({
publicKey: publicKey,
privateKey: privateKey
}));
const orbitdb = new OrbitDB(ipfs,directory,{peerId:id, keystore:keystore});
await orbitdb.keyvalue('/orbitdb/' + topicsDB +'/topics');
await orbitdb.keyvalue('/orbitdb/' + postsDB +'/posts');
//todo: loadedDBs.load() (?)
store.dispatch({type: "DATABASES_LOADED", id: orbitdb.id});
orbitdb = new OrbitDB(ipfs,directory,{peerId:id, keystore:keystore});
topicsDB = await orbitdb.keyvalue('/orbitdb/' + mTopicsDB +'/topics');
postsDB = await orbitdb.keyvalue('/orbitdb/' + mPostsDB +'/posts');
topicsDB.load();
postsDB.load();
store.dispatch({
type: "DATABASES_LOADED",
orbitdb: orbitdb,
topicsDB: topicsDB,
postsDB: postsDB,
id: orbitdb.id
});
}
export { createDatabases, loadDatabases };

12
src/util/orbitReducer.js

@ -1,6 +1,9 @@
const initialState = {
ipfsInitialized: false,
ready: false,
orbitdb: null,
topicsDB: null,
postsDB: null,
id: null
};
@ -15,18 +18,27 @@ const orbitReducer = (state = initialState, action) => {
return {
...state,
ready: true,
orbitdb: action.orbitdb,
topicsDB: action.topicsDB,
postsDB: action.postsDB,
id: action.id
};
case 'DATABASES_LOADED':
return {
...state,
ready: true,
orbitdb: action.orbitdb,
topicsDB: action.topicsDB,
postsDB: action.postsDB,
id: action.id
};
case 'DATABASES_NOT_READY':
return {
...state,
ready: false,
orbitdb: null,
topicsDB: null,
postsDB: null,
id: null
};
default:

Loading…
Cancel
Save