Browse Source

Add contract and OrbitDB interaction in NewPost, TopicContainer, PostList and ProfileInformation

develop
Apostolos Fanakis 7 years ago
parent
commit
6a595bc343
  1. 54
      contracts/Forum.sol
  2. 40
      src/assets/css/App.css
  3. 14
      src/components/LoadingSpinner.js
  4. 266
      src/components/NewPost.js
  5. 50
      src/components/Post.js
  6. 142
      src/components/PostList.js
  7. 9
      src/components/ProfileInformation.js
  8. 42
      src/components/Topic.js
  9. 51
      src/components/TopicList.js
  10. 20
      src/containers/BoardContainer.js
  11. 147
      src/containers/ProfileContainer.js
  12. 28
      src/containers/StartTopicContainer.js
  13. 106
      src/containers/TopicContainer.js
  14. 12
      src/helpers/EpochTimeConverter.js
  15. 2
      src/index.js
  16. 4
      src/util/drizzleOptions.js

54
contracts/Forum.sol

@ -55,6 +55,16 @@ contract Forum {
return false;
}
function getUserTopics(address userAddress) public view returns (uint[]) {
require (hasUserSignedUp(msg.sender), "User hasn't signed up yet.");
return users[userAddress].topicIDs;
}
function getUserPosts(address userAddress) public view returns (uint[]) {
require (hasUserSignedUp(msg.sender), "User hasn't signed up yet.");
return users[userAddress].postIDs;
}
//----------------------------------------OrbitDB----------------------------------------
struct OrbitDB {
string id; // TODO: set an upper bound instead of arbitrary string
@ -115,6 +125,7 @@ contract Forum {
uint postID;
address author;
uint timestamp;
uint topicID;
}
uint numTopics; // Total number of topics
@ -123,25 +134,33 @@ contract Forum {
mapping (uint => Topic) topics;
mapping (uint => Post) posts;
event TopicCreated(uint topicID);
event TopicCreated(uint topicID, uint postID);
event PostCreated(uint postID, uint topicID);
event NumberOfTopicsReceived(uint numTopics);
event TopicReceived(string orbitTopicsDB, address author, string username, uint timestamp, uint[] postIDs);
/* event NumberOfTopicsReceived(uint numTopics);
event TopicReceived(string orbitTopicsDB, address author, string username, uint timestamp, uint[] postIDs); */
function createTopic() public returns (uint) {
function createTopic() public returns (uint, uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create topics
//Creates topic
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;
//Adds first post to topic
uint postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp, topicID);
topics[topicID].postIDs.push(postID);
users[msg.sender].postIDs.push(postID);
emit TopicCreated(topicID, postID);
return (topicID, postID);
}
function createPost(uint topicID) public returns (uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create posts
require(topicID<numTopics); // Only allow posting to a topic that exists
uint postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp);
posts[postID] = Post(postID, msg.sender, block.timestamp, topicID);
topics[topicID].postIDs.push(postID);
users[msg.sender].postIDs.push(postID);
emit PostCreated(postID, topicID);
@ -149,20 +168,14 @@ contract Forum {
}
function getNumberOfTopics() public view returns (uint) {
emit NumberOfTopicsReceived(numTopics);
/* 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),
return (getOrbitTopicsDB(topics[topicID].author),
topics[topicID].author,
users[topics[topicID].author].username,
topics[topicID].timestamp,
@ -174,4 +187,15 @@ contract Forum {
require(topicID<numTopics); // Topic should exist
return topics[topicID].postIDs;
}
function getPost(uint postID) public view returns (string, address, string, uint, uint) {
//require(hasUserSignedUp(msg.sender)); needed?
require(postID<numPosts);
return (getOrbitPostsDB(posts[postID].author),
posts[postID].author,
users[posts[postID].author].username,
posts[postID].timestamp,
posts[postID].topicID
);
}
}

40
src/assets/css/App.css

@ -261,6 +261,40 @@ body,
padding: 7px;
}
/* PROFILE */
.profile-tabs-header {
width: 100%;
font-size: 1.72em;
font-weight: bold;
margin-bottom: 20px;
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
}
.profile-tabs-header p {
width: 100%;
height: 100%;
text-align: center;
background-color: lightgrey;
padding: 10px;
margin: 0px;
cursor: pointer;
}
.profile-tabs-header p:hover {
background-color: grey;
}
.profile-tab-selected {
border-bottom: 2px solid #0c1a2b;
}
.profile-tab, .profile-tab>div {
width: 100%;
}
/* FORMS */
.pure-form {
@ -348,6 +382,12 @@ a {
text-align: center;
}
.vertical-center-children {
display: flex;
flex-flow: row nowrap;
align-items: flex-start;
}
#overlay {
position: fixed;
display: block;

14
src/components/LoadingSpinner.js

@ -0,0 +1,14 @@
import React from 'react';
const LoadingSpinner = (props) => {
return(
<div className={"center-in-parent" + (props.className ? props.className : "")}
style={props.style ? props.style : []}>
<p>
<i className="fas fa-spinner fa-3x fa-spin"></i>
</p>
</div>
);
}
export default LoadingSpinner;

266
src/components/NewPost.js

@ -1,39 +1,98 @@
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import uuidv4 from 'uuid/v4';
import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown';
const contract = "Forum";
const contractMethod = "createPost";
class NewPost extends Component {
constructor(props, context) {
super(props);
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.state = {
postContent: '',
previewEnabled: false
postSubjectInput: this.props.subject,
postContentInput: '',
postSubjectInputEmptySubmit: false,
postContentInputEmptySubmit: false,
previewEnabled: false,
previewDate: "",
creatingPost: false,
transactionState: null,
savingToOrbitDB: null,
transactionOutputTimerActive: false
};
}
async handleSubmit() {
/*this.stackId = this.contracts[contract].methods[startTopicMethod].cacheSend();*/
async validateAndPost() {
if (this.state.postSubjectInput === '' || this.state.postContentInput === ''){
this.setState({
postSubjectInputEmptySubmit: this.state.postSubjectInput === '',
postContentInputEmptySubmit: this.state.postContentInput === ''
});
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"
});
}
async pushToDatabase() {
await this.props.orbitDB.postsDB.put(this.postIDFetched, {
subject: this.state.postSubjectInput,
content: this.state.postContentInput
});
this.setState({'savingToOrbitDB': "SUCCESS"});
}
handleInputChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handlePreviewToggle(){
handlePreviewToggle() {
this.setState((prevState, props) => ({
previewEnabled: !prevState.previewEnabled
previewEnabled: !prevState.previewEnabled,
previewDate: this.getDate()
}));
}
getDate() {
const currentdate = new Date();
return ((currentdate.getMonth() + 1) + " "
+ currentdate.getDate() + ", "
+ currentdate.getFullYear() + ", "
+ currentdate.getHours() + ":"
+ currentdate.getMinutes() + ":"
+ currentdate.getSeconds());
}
render() {
return (
<div className="pure-u-1-1 post card">
{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>
}
<div className="post-header">
<UserAvatar
size="40"
@ -41,7 +100,7 @@ class NewPost extends Component {
src={this.props.user.avatarUrl}
name={this.props.user.username}/>
<p className="inline no-margin">
<strong>{this.props.user.username}<br/>Subject: {this.props.subject}</strong>
<strong>{this.props.user.username}<br/>Subject: {this.state.postSubjectInput}</strong>
</p>
<div className="post-info">
<span></span>
@ -52,17 +111,27 @@ class NewPost extends Component {
<div className="post-content">
<form className="topic-form">
{this.state.previewEnabled
? <ReactMarkdown source={this.state.postContent} className="markdownPreview" />
: <textarea key={"postContent"}
name={"postContent"}
value={this.state.postContent}
placeholder="Post"
id="postContent"
onChange={this.handleInputChange} />}
? <ReactMarkdown source={this.state.postContentInput} className="markdownPreview" />
: [
<input key={"postSubjectInput"}
name={"postSubjectInput"}
className={this.state.postSubjectInputEmptySubmit ? "form-input-required" : ""}
type="text"
value={this.state.postSubjectInput}
placeholder="Subject"
id="postSubjectInput"
onChange={this.handleInputChange} />,
<textarea key={"postContentInput"}
name={"postContentInput"}
value={this.state.postContentInput}
placeholder="Post"
id="postContentInput"
onChange={this.handleInputChange} />
]}
<button key="submit"
className="pure-button pure-button-primary"
type="button"
onClick={this.handleSubmit}>
onClick={this.validateAndPost}>
Post
</button>
<button className="pure-button margin-left-small"
@ -80,12 +149,169 @@ class NewPost extends Component {
</div>
);
}
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);
}
}
}
}
NewPost.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
user: state.user
}
return {
transactions: state.transactions,
transactionStack: state.transactionStack,
orbitDB: state.orbitDB,
user: state.user
}
};
export default drizzleConnect(NewPost, mapStateToProps);

50
src/components/Post.js

@ -4,27 +4,53 @@ import TimeAgo from 'react-timeago';
import ReactMarkdown from 'react-markdown';
const Post = (props) => {
const username = props.username && [props.username, <br key={props.id}/>];
return (
<div className="pure-u-1-1 post card">
props.post !== null
? <div className="pure-u-1-1 post card">
<div className="post-header">
<div className="vertical-center-children">
<UserAvatar
size="40"
className="inline user-avatar"
src={props.post.avatarUrl}
name={props.post.username}/>
<p className="inline no-margin">
<strong>
{props.post.username}
<br/>
Subject: {props.post.subject}
</strong>
</p>
</div>
<div className="post-info">
<span>Posted <TimeAgo date={props.post.date}/></span>
<span>#{props.post.postIndex}</span>
</div>
</div>
<hr/>
<div className="post-content">
{props.post.postContent
? <ReactMarkdown source={props.post.postContent} />
: <p style={{color: 'grey'}}>Post content...</p>
}
</div>
<hr/>
<div className="post-meta">
Maybe add buttons for upvote etc here...
</div>
</div>
: <div className="pure-u-1-1 post card" style={{color: 'grey'}}>
<div className="post-header">
{props.avatarUrl && <UserAvatar
size="40"
className="inline user-avatar"
src={props.avatarUrl}
name={props.username}/>}
<p className="inline no-margin">
<strong>{username}Subject: {props.subject}</strong>
<strong>Subject</strong>
</p>
<div className="post-info">
<span>Posted <TimeAgo date={props.date}/></span>
{props.postIndex && <span>#{props.postIndex}</span>}
<span>Posted </span>
</div>
</div>
<hr/>
<div className="post-content">
<ReactMarkdown source={props.postContent} />
<p>Post content...</p>
</div>
<hr/>
<div className="post-meta">

142
src/components/PostList.js

@ -1,45 +1,107 @@
import React from 'react';
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import { Link } from 'react-router';
import PropTypes from 'prop-types';
import Post from './Post';
const posts1 = [
{avatarUrl: "https://i.makeagif.com/media/4-18-2018/8BLiwJ.gif",
username: "Apostolof",
subject: "Some very important topic of discussion2!",
date: "May 25, 2018, 11:11:11",
postIndex: "1",
postContent: "# We have markdown!!!\n \n**Oh yes we do!!** \n*ITALICS* \n \n```Some code```",
id: 2,
address: 0x083c41ea13af6c2d5aaddf6e73142eb9a7b00183
},
{avatarUrl: "",
username: "",
subject: "Some very important topic of discussion!",
date: "May 20, 2018, 10:10:10",
postIndex: "",
postContent: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur, natus ipsum minima.",
id: 1,
address: 0x5fe3062B24033113fbf52b2b75882890D7d8CA54
import epochTimeConverter from '../helpers/EpochTimeConverter'
const contract = "Forum";
const contractMethod = "getPost";
class PostList extends Component {
constructor(props, context) {
super(props);
this.fetchPost = this.fetchPost.bind(this);
this.drizzle = context.drizzle;
this.dataKeys = [];
this.postsData = new Array(parseInt(this.props.postIDs.length, 10)).fill(undefined);
this.orbitPostsData = new Array(parseInt(this.props.postIDs.length, 10)).fill(undefined);
this.orbitPostsDataFetchStatus = new Array(parseInt(this.props.postIDs.length, 10)).fill("pending");
for (var i = 0; i < this.props.postIDs.length; ++i){
this.dataKeys[i] = this.drizzle.contracts[contract].methods[contractMethod]
.cacheCall(this.props.postIDs[i]);
}
}
async fetchPost(index) {
/*const fullAddress = this.postsData[postID][1];
const store = await this.props.orbitDB.orbitdb.keyvalue(JSON.stringify(fullAddress));
await store.load();
var som = store.get(JSON.stringify(postID));
this.orbitPostsData[postID] = som['subject'];
this.orbitPostsDataFetchStatus[postID] = "fetched";*/
var som = this.props.orbitDB.postsDB.get(this.props.postIDs[index]);
this.orbitPostsData[index] = som;
this.orbitPostsDataFetchStatus[index] = "fetched";
}
render (){
const posts = this.postsData.map((post, index) => {
if (post) {
return (
<Link to={"/topic/" + post[4] + "/" +
((this.orbitPostsData[index] !== undefined) ? this.orbitPostsData[index].subject + "/" +
this.props.postIDs[index] : "")}
key={index}>
<Post post={{
avatarUrl: post.avatarUrl,
username: post[2],
subject: (this.orbitPostsData[index] !== undefined) && this.orbitPostsData[index].subject,
date: epochTimeConverter(post[3]),
postIndex: index,
postContent: (this.orbitPostsData[index] !== undefined) && this.orbitPostsData[index].content
}}
id={index}
key={index}/>
</Link>
);
} else {
return (
<Post post={null}
id={index}
key={index}/>
);
}
});
return (
<div className="posts-list">
{posts}
</div>
);
}
componentWillReceiveProps() {
for (var i = 0; i < this.props.postIDs.length; ++i){
if (this.postsData[i] === undefined) {
let currentDrizzleState = this.drizzle.store.getState();
let dataFetched = (currentDrizzleState.contracts[contract][contractMethod])[this.dataKeys[i]];
if (dataFetched){
this.postsData[i] = dataFetched.value;
}
} else if (!this.orbitPostsData[i] && this.orbitPostsDataFetchStatus[i] === "pending") {
this.orbitPostsDataFetchStatus[i] = "fetching";
this.fetchPost(i);
}
}
}
};
PostList.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
user: state.user, //Needed!!
orbitDB: state.orbitDB,
}
];
const PostList = (props) => {
const posts = posts1.map((post) =>
<Post avatarUrl={post.avatarUrl}
username={post.username}
subject={post.subject}
date={post.date}
postIndex={post.postIndex}
postContent={post.postContent}
id={post.id}
key={post.id}
address={post.address}/>
);
return (
<div className="posts-list">
{posts}
</div>
);
};
export default PostList;
export default drizzleConnect(PostList, mapStateToProps);

9
src/components/ProfileInformation.js

@ -11,7 +11,7 @@ const ProfileInformation = (props) => {
src={props.avatarUrl}
name={props.username}/>}
<p className="no-margin inline">
<strong>Username</strong>: {props.username}
<strong>Username:</strong> {props.username}
</p>
<p className="no-margin">
<strong>Account address:</strong> {props.address}
@ -20,9 +20,12 @@ const ProfileInformation = (props) => {
<strong>OrbitDB:</strong> {props.orbitAddress}
</p>
<p className="no-margin">
Number of posts: TODO?
<strong>Number of topics created:</strong> {props.numberOfTopics}
</p>
<UsernameFormContainer/>
<p className="no-margin">
<strong>Number of posts:</strong> {props.numberOfPosts}
</p>
{props.self && <UsernameFormContainer/>}
</div>
);
};

42
src/components/Topic.js

@ -2,29 +2,29 @@ import React from 'react';
import TimeAgo from 'react-timeago';
const Topic = (props) => {
return (
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>
return (
props.topic !== null
? <div className={"pure-u-1-1 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 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 className={"pure-u-1-1 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>
);
};
export default Topic;

51
src/components/TopicList.js

@ -1,10 +1,12 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import { Link } from 'react-router';
import PropTypes from 'prop-types'
import PropTypes from 'prop-types';
import Topic from './Topic';
import epochTimeConverter from '../helpers/EpochTimeConverter'
const contract = "Forum";
const contractMethod = "getTopic";
@ -13,62 +15,50 @@ class TopicList extends Component {
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.topicsData = new Array(parseInt(this.props.topicIDs.length, 10)).fill(undefined);
this.topicsSubjects = [];
this.topicsSubjectsFetched = [];
this.topicsSubjectsFetchStatus = new Array(parseInt(this.props.topicIDs.length, 10)).fill("pending");
for (var i = 0; i < this.props.numberOfTopics; ++i){
this.dataKeys[i] = this.drizzle.contracts[contract].methods[contractMethod].cacheCall(i);
for (var i = 0; i < this.props.topicIDs.length; ++i){
this.dataKeys[i] = this.drizzle.contracts[contract].methods[contractMethod]
.cacheCall(this.props.topicIDs[i]);
}
this.state = {
};
}
async fetchSubject(topicID) {
async fetchSubject(topicIndex) {
/*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;*/
this.topicsSubjectsFetchStatus[topicID] = "fetched";*/
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())
var som =this.props.orbitDB.topicsDB.get(this.props.topicIDs[topicIndex]);
this.topicsSubjects[topicIndex] = som['subject'];
this.topicsSubjectsFetchStatus[topicIndex] = "fetched";
}
render (){
const topics = this.topicsData.slice(0).reverse().map((topic, index) => {
const topics = this.topicsData.map((topic, index) => {
if (topic){
return (
<Link to={"/topic/" + index + "/" +
((this.topicsSubjects[index] !== undefined) ? this.topicsSubjects[index] : "")}
((this.topicsSubjects[index] !== undefined) ? this.topicsSubjects[index] + "/" + 0 : "")}
key={index}>
<Topic topic={{
topicSubject: ((this.topicsSubjects[index] !== undefined) && this.topicsSubjects[index]),
topicStarter: topic[2],
numberOfReplies: topic[4].length,
date: this.correctTimeFormat(topic[3])
date: epochTimeConverter(topic[3])
}}
id={index}
key={index}
address={topic[1]}/>
key={index}/>
</Link>
);
} else {
@ -85,20 +75,21 @@ class TopicList extends Component {
return (
<div className="topics-list">
{topics}
{topics.slice(0).reverse()}
</div>
);
}
componentWillReceiveProps() {
for (var i = 0; i < this.props.numberOfTopics; ++i){
for (var i = 0; i < this.props.topicIDs.length; ++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]) {
} else if (!this.topicsSubjects[i] && this.topicsSubjectsFetchStatus[i] === "pending") {
this.topicsSubjectsFetchStatus[i] = "fetching";
this.fetchSubject(i);
}
}

20
src/containers/BoardContainer.js

@ -5,6 +5,7 @@ import { Link } from 'react-router';
import TopicList from '../components/TopicList';
import FloatingButton from '../components/FloatingButton';
import LoadingSpinner from '../components/LoadingSpinner';
const contract = "Forum";
const contractMethod = "getNumberOfTopics";
@ -16,7 +17,6 @@ class Board extends Component {
this.drizzle = context.drizzle;
this.state = {
startingNewTopic: false,
transactionState: null
};
}
@ -25,18 +25,14 @@ class Board extends Component {
var boardContents;
if (this.state.transactionState !== "SUCCESS") {
boardContents = (
<div className="center-in-parent">
<p>
<i className="fas fa-spinner fa-3x fa-spin"></i>
</p>
</div>
<LoadingSpinner/>
);
} else {
boardContents = <TopicList numberOfTopics={this.numberOfTopics}/>;
boardContents = <TopicList topicIDs={this.topicIDs}/>;
}
return (
<div style={{marginBottom: '100px'}}>
<div style={{marginBottom: '70px'}}>
{boardContents}
<Link to="/startTopic">
<FloatingButton onClick={this.handleClick}/>
@ -47,7 +43,7 @@ class Board extends Component {
componentWillReceiveProps() {
if (this.state.transactionState === null){
if (this.drizzle.contracts[contract]){
if (this.drizzle.contracts[contract]){ //Waits until drizzle is initialized
//This gets called only once but should be called every time someone posts
this.dataKey = this.drizzle.contracts[contract].methods[contractMethod].cacheCall();
this.setState({'transactionState': "IN_PROGRESS"});
@ -57,7 +53,11 @@ class Board extends Component {
let currentDrizzleState = this.drizzle.store.getState();
let dataFetched = (currentDrizzleState.contracts[contract][contractMethod])[this.dataKey];
if (dataFetched){
this.numberOfTopics = dataFetched.value
this.numberOfTopics = dataFetched.value;
this.topicIDs = [];
for (var i = 0; i < this.numberOfTopics; i++) {
this.topicIDs.push(i);
}
this.setState({'transactionState': "SUCCESS"});
}
}

147
src/containers/ProfileContainer.js

@ -1,25 +1,154 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ProfileInformation from '../components/ProfileInformation';
import TopicList from '../components/TopicList';
import PostList from '../components/PostList';
import LoadingSpinner from '../components/LoadingSpinner';
const contract = "Forum";
const contractMethods = {
getOrbitDB: "getOrbitDBId",
getUserTopics: "getUserTopics",
getUserPosts: "getUserPosts"
};
class Profile extends Component {
constructor(props, context) {
super(props);
//THIS WILL CHANGE WITH ACTUAL DATA
this.match = {
username: this.props.user.username,
userAddress: this.props.user.address
}
this.handleTabClick = this.handleTabClick.bind(this);
this.drizzle = context.drizzle;
this.state = {
viewSelected: "topics",
username: this.match.username, // TODO actually get them from match
userAddress: this.match.address, // when router is fixed
orbitDBId: this.match.address === this.props.user.address ? this.props.orbitDB.id : "",
getOrbitDBTransactionState: this.match.address === this.props.user.address ? "SUCCESS" : null,
getTopicsTransactionState: null,
getPostsTransactionState: null,
topicIDs: [],
postIDs: []
};
}
handleTabClick(id) {
this.setState({viewSelected: id});
}
class SignUp extends Component {
render() {
return (
<div className="pure-g">
<ProfileInformation username={this.props.user.username}
address={this.props.user.address}
orbitAddress={this.props.orbitDB.id}/>
<p className="pure-u-1-1">
My posts:
</p>
<PostList/> {/*TODO change this with actual user's posts*/}
<ProfileInformation username={this.state.username}
address={this.state.userAddress}
orbitAddress={this.state.orbitDBId}
numberOfTopics={this.state.topicIDs.length}
numberOfPosts={this.state.postIDs.length}
self/>
<div className="pure-u-1-1 profile-tabs-header">
<p onClick={() => (this.handleTabClick("topics"))}
className={this.state.viewSelected === "topics" ? "profile-tab-selected" : ""}>
Topics
</p>
<p onClick={() => (this.handleTabClick("posts"))}
className={this.state.viewSelected === "posts" ? "profile-tab-selected" : ""}>
Posts
</p>
</div>
{this.state.viewSelected === "topics"
?<div className="profile-tab">
{this.state.getTopicsTransactionState === "SUCCESS"
? <TopicList topicIDs={this.state.topicIDs}/>
: <LoadingSpinner />
}
</div>
:<div className="profile-tab">
{this.state.getPostsTransactionState === "SUCCESS"
? <PostList postIDs={this.state.postIDs}/>
: <LoadingSpinner />
}
</div>
}
</div>
);
}
componentWillReceiveProps() {
if (this.state.getOrbitDBTransactionState === null){
if (this.drizzle.contracts[contract]){ //Waits until drizzle is initialized
//This gets called only once but should be called every time someone posts
this.orbitDBIdKey = this.drizzle.contracts[contract]
.methods[contractMethods.getOrbitDB].cacheCall(this.match.userAddress);
this.setState({'getOrbitDBTransactionState': "IN_PROGRESS"});
}
}
if (this.state.getOrbitDBTransactionState === "IN_PROGRESS") {
let currentDrizzleState = this.drizzle.store.getState();
let dataFetched = (currentDrizzleState
.contracts[contract][contractMethods.getOrbitDB])[this.orbitDBIdKey];
if (dataFetched){
this.setState({
'orbitDBId': dataFetched.value,
'getOrbitDBTransactionState': "SUCCESS"
});
}
}
if (this.state.getTopicsTransactionState === null){
if (this.drizzle.contracts[contract]){ //Waits until drizzle is initialized
//This gets called only once but should be called every time someone posts
this.getTopicsKey = this.drizzle.contracts[contract]
.methods[contractMethods.getUserTopics].cacheCall(this.match.userAddress);
this.setState({'getTopicsTransactionState': "IN_PROGRESS"});
}
}
if (this.state.getTopicsTransactionState === "IN_PROGRESS") {
let currentDrizzleState = this.drizzle.store.getState();
let dataFetched = (currentDrizzleState
.contracts[contract][contractMethods.getUserTopics])[this.getTopicsKey];
if (dataFetched){
this.setState({
'topicIDs': dataFetched.value,
'getTopicsTransactionState': "SUCCESS"
});
}
}
if (this.state.getPostsTransactionState === null){
if (this.drizzle.contracts[contract]){ //Waits until drizzle is initialized
//This gets called only once but should be called every time someone posts
this.getPostsKey = this.drizzle.contracts[contract]
.methods[contractMethods.getUserPosts].cacheCall(this.match.userAddress);
this.setState({'getPostsTransactionState': "IN_PROGRESS"});
}
}
if (this.state.getPostsTransactionState === "IN_PROGRESS") {
let currentDrizzleState = this.drizzle.store.getState();
let dataFetched = (currentDrizzleState
.contracts[contract][contractMethods.getUserPosts])[this.getPostsKey];
if (dataFetched){
this.setState({
'postIDs': dataFetched.value,
'getPostsTransactionState': "SUCCESS"
});
}
}
}
}
Profile.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
accounts: state.accounts,
@ -30,6 +159,6 @@ const mapStateToProps = state => {
}
};
const ProfileContainer = drizzleConnect(SignUp, mapStateToProps);
const ProfileContainer = drizzleConnect(Profile, mapStateToProps);
export default ProfileContainer;

28
src/containers/StartTopicContainer.js

@ -54,6 +54,10 @@ class StartTopic extends Component {
async pushToDatabase() {
await this.props.orbitDB.topicsDB.put(this.topicIDFetched, {
subject: this.state.topicSubjectInput
});
await this.props.orbitDB.postsDB.put(this.postIDFetched, {
subject: this.state.topicSubjectInput,
content: this.state.topicMessageInput
});
@ -82,7 +86,7 @@ class StartTopic extends Component {
}
render() {
return(
return (
<div>
{this.state.creatingTopic && <div id="overlay">
<div id="overlay-content">
@ -93,11 +97,13 @@ class StartTopic extends Component {
</div>
}
{this.state.previewEnabled &&
<Post avatarUrl={this.props.user.avatarUrl}
username={this.props.user.username}
subject={this.state.topicSubjectInput}
date={this.state.previewDate}
postContent={this.state.topicMessageInput}
<Post post = {{
avatarUrl: this.props.user.avatarUrl,
username: this.props.user.username,
subject: this.state.topicSubjectInput,
date: this.state.previewDate,
postContent: this.state.topicMessageInput
}}
id={0}/>}
<form className="topic-form">
{!this.state.previewEnabled &&
@ -155,8 +161,10 @@ class StartTopic extends Component {
/* Transaction completed successfully */
//Gets topic's id returned by contract
this.topicIDFetched = this.props.transactions[this.txHash].receipt
.events.TopicCreated.returnValues.topicID;
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()}/>);
@ -168,7 +176,7 @@ class StartTopic extends Component {
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'green'}}>
<strong>
TopicID = {this.topicIDFetched}
TopicID = {this.topicIDFetched}, PostID = {this.postIDFetched}
</strong>
</span>);
this.setState({'transactionState': "SUCCESS"});
@ -215,7 +223,7 @@ class StartTopic extends Component {
this.transactionProgressText.push(<br key={uuidv4()}/>);
this.transactionProgressText.push(<span key={uuidv4()} style={{color: 'green'}}>
<strong>
Post successfully saved in OrbitDB.
Topic successfully saved in OrbitDB.
</strong>
</span>);
this.setState({'transactionOutputTimerActive': true});

106
src/containers/TopicContainer.js

@ -1,18 +1,42 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import PostList from '../components/PostList';
import NewPost from '../components/NewPost';
import FloatingButton from '../components/FloatingButton';
import LoadingSpinner from '../components/LoadingSpinner';
const contract = "Forum";
const contractMethod = "getTopic";
class Topic extends Component {
constructor(props) {
constructor(props, context) {
super(props);
this.fetchTopicSubject = this.fetchTopicSubject.bind(this);
this.handleClick = this.handleClick.bind(this);
this.postCreated = this.postCreated.bind(this);
this.drizzle = context.drizzle;
this.state = {
posting: false
getPostsTransactionState: null,
posting: false,
topicSubject: null
};
}
this.handleClick = this.handleClick.bind(this);
async fetchTopicSubject(orbitDBAddress, 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.topicsSubjectsFetchStatus[topicID] = "fetched";*/
var som =this.props.orbitDB.topicsDB.get(JSON.stringify(topicID));
this.setState({'topicSubject': som['subject']});
}
handleClick(event) {
@ -24,29 +48,73 @@ class Topic extends Component {
}));
}
postCreated(){
this.setState(prevState => ({
getPostsTransactionState: null,
posting: false
}));
}
render() {
var topicContents;
if (this.state.getPostsTransactionState !== "SUCCESS") {
topicContents = (
<LoadingSpinner/>
);
} else {
topicContents = (
this.state.posting
?(<div style={{marginBottom: '100px'}}>
<NewPost topicID={1}
subject={this.state.topicSubject}
onCancelClick={() => {this.handleClick()}}
onPostCreated={() => {this.postCreated()}}
/>
<PostList postIDs={this.posts}/>
</div>)
:(<div style={{marginBottom: '100px'}}>
<PostList postIDs={this.posts}/>
<FloatingButton onClick={this.handleClick}/>
</div>)
)
}
return (
this.state.posting
?(<div style={{marginBottom: '100px'}}>
<PostList/>
<NewPost onCancelClick={() => {this.handleClick()}}/>
</div>)
:(<div style={{marginBottom: '100px'}}>
<PostList/>
<FloatingButton onClick={this.handleClick}/>
</div>)
<div style={{marginBottom: '70px'}}>
{topicContents}
</div>
);
}
componentWillReceiveProps() {
if (this.state.getPostsTransactionState === null){
if (this.drizzle.contracts[contract]){ //Waits until drizzle is initialized
//This gets called only once but should be called every time someone posts
this.getPostsDataKey = this.drizzle.contracts[contract].methods[contractMethod].cacheCall(1);
this.setState({'getPostsTransactionState': "IN_PROGRESS"});
}
}
if (this.state.getPostsTransactionState === "IN_PROGRESS") {
let currentDrizzleState = this.drizzle.store.getState();
let dataFetched = (currentDrizzleState.contracts[contract][contractMethod])[this.getPostsDataKey];
if (dataFetched){
this.posts = dataFetched.value[4];
this.setState({'getPostsTransactionState': "SUCCESS"});
this.fetchTopicSubject(dataFetched.value[0], 1);
}
}
}
}
Topic.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 TopicContainer = drizzleConnect(Topic, mapStateToProps);

12
src/helpers/EpochTimeConverter.js

@ -0,0 +1,12 @@
const epochTimeConverter = (timestamp) => {
var timestampDate = new Date(0);
timestampDate.setUTCSeconds(timestamp);
return ((timestampDate.getMonth() + 1) + " "
+ timestampDate.getDate() + ", "
+ timestampDate.getFullYear() + ", "
+ timestampDate.getHours() + ":"
+ timestampDate.getMinutes() + ":"
+ timestampDate.getSeconds())
}
export default epochTimeConverter;

2
src/index.js

@ -31,7 +31,7 @@ render((
<Router history={history}>
<Route path="/" component={CoreLayout}>
<IndexRoute component={HomeContainer} />
<PrivateRouteContainer path="/topic/:topicId/:topicSubject" component={TopicContainer} redirectTo="/" />
<PrivateRouteContainer path="/topic/:topicId/:topicSubject/:postId" component={TopicContainer} redirectTo="/" />
<PrivateRouteContainer path='/profile' component={ProfileContainer} redirectTo="/" />
<PrivateRouteContainer path='/startTopic' component={StartTopicContainer} redirectTo="/" />
<Route path='/404' component={NotFoundView} />

4
src/util/drizzleOptions.js

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

Loading…
Cancel
Save