diff --git a/app/package.json b/app/package.json index d7b7dfa..fd7c064 100644 --- a/app/package.json +++ b/app/package.json @@ -17,13 +17,18 @@ "orbit-db-keystore": "^0.1.0", "prop-types": "^15.7.2", "react": "^16.8.3", + "react-content-loader": "^4.0.1", "react-dom": "^16.8.3", + "react-markdown": "^4.0.6", "react-redux": "^6.0.1", "react-router-dom": "^4.3.1", "react-scripts": "^2.1.5", + "react-timeago": "^4.4.0", + "react-user-avatar": "^1.10.0", "redux": "^4.0.1", "redux-saga": "^0.16.2", - "semantic-ui-react": "^0.85.0" + "semantic-ui-react": "^0.85.0", + "uuid": "^3.3.2" }, "scripts": { "start": "react-scripts start", diff --git a/app/src/components/FloatingButton.js b/app/src/components/FloatingButton.js new file mode 100644 index 0000000..522005a --- /dev/null +++ b/app/src/components/FloatingButton.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Button, Icon } from 'semantic-ui-react' + +const FloatingButton = (props) => { + return ( +
+ +
+ ); +}; + +export default FloatingButton; \ No newline at end of file diff --git a/app/src/components/NewTopicPreview.js b/app/src/components/NewTopicPreview.js new file mode 100644 index 0000000..ebc7b82 --- /dev/null +++ b/app/src/components/NewTopicPreview.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { Grid, Divider } from 'semantic-ui-react' + +import TimeAgo from 'react-timeago'; +import UserAvatar from 'react-user-avatar'; +import ReactMarkdown from 'react-markdown'; + +class Post extends Component { + constructor(props, context) { + super(props); + } + + render(){ + return ( +
+ + #0 + + + + + + + +
+
+ + + {this.props.user.username} + + + + + +
+
+ + Subject: {this.props.subject} + +
+
+ +
+
+
+
+
+
+ ); + } +}; + +const mapStateToProps = state => { + return { + user: state.user + } +}; + +export default connect(mapStateToProps)(Post); \ No newline at end of file diff --git a/app/src/components/Topic.js b/app/src/components/Topic.js new file mode 100644 index 0000000..4f97352 --- /dev/null +++ b/app/src/components/Topic.js @@ -0,0 +1,104 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router' + +import ContentLoader from "react-content-loader" +import { Card } from 'semantic-ui-react' + +import TimeAgo from 'react-timeago'; +import epochTimeConverter from '../helpers/EpochTimeConverter' + +class Topic extends Component { + constructor(props){ + super(props); + + this.fetchSubject = this.fetchSubject.bind(this); + + this.topicSubject = null; + this.topicSubjectFetchStatus = "pending"; + } + + async fetchSubject(topicID) { + this.topicSubjectFetchStatus = "fetching"; + + if (this.props.topicData.value[1] === this.props.user.address) { + let orbitData = this.props.orbitDB.topicsDB.get(topicID); + this.topicSubject = orbitData['subject']; + this.topicSubjectFetchStatus = "fetched"; + } else { + const fullAddress = "/orbitdb/" + this.props.topicData.value[0] + "/topics"; + const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress); + await store.load(); + + let localOrbitData = store.get(topicID); + if (localOrbitData) { + this.topicSubject = localOrbitData['subject']; + } else { + // Wait until we have received something from the network + store.events.on('replicated', () => { + this.topicSubject = store.get(topicID)['subject']; + }) + } + this.topicSubjectFetchStatus = "fetched"; + } + } + + render(){ + return ( + {this.props.history.push("/topic/" + this.props.topicID)}}> + +
+

+ {this.topicSubject !== null ? this.topicSubject + : + + } +

+
+
+
+

+ {this.props.topicData !== null + ?this.props.topicData.value[2] + :"Username" + } +

+

+ {"Number of replies: " + (this.props.topicData !== null + ?this.props.topicData.value[4].length + :"") + } +

+

+ {this.props.topicData !== null && + + } +

+
+
+
+ ); + } + + componentDidUpdate(){ + if (this.props.topicData !== null && + this.topicSubjectFetchStatus === "pending" && + this.props.orbitDB.ipfsInitialized && + this.props.orbitDB.orbitdb) { + this.fetchSubject(this.props.topicID); + } + } +}; + +const mapStateToProps = state => { + return { + user: state.user, + orbitDB: state.orbit + } +} + +export default withRouter(connect(mapStateToProps)(Topic)); diff --git a/app/src/components/TopicList.js b/app/src/components/TopicList.js new file mode 100644 index 0000000..f81a376 --- /dev/null +++ b/app/src/components/TopicList.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { drizzle } from '../index'; + +import Topic from './Topic'; + +const contract = "Forum"; +const getTopicMethod = "getTopic"; + +class TopicList extends Component { + constructor(props) { + super(props); + + this.dataKeys = []; + + this.state = { + topicsLoading: true + } + } + + componentDidUpdate(){ + if (this.state.topicsLoading && this.props.drizzleStatus['initialized']){ + var topicsLoading = false; + + this.props.topicIDs.forEach( topicID => { + if (!this.dataKeys[topicID]) { + this.dataKeys[topicID] = drizzle.contracts[contract].methods[getTopicMethod].cacheCall(topicID); + topicsLoading = true; + } + }) + + this.setState({ topicsLoading: topicsLoading }); + } + } + + render() { + const topics = this.props.topicIDs.map((topicID) => { + return () + }); + + return ( +
+ {topics.slice(0).reverse()} +
+ ); + } +}; + +const mapStateToProps = state => { + return { + contracts: state.contracts, + drizzleStatus: state.drizzleStatus + } +}; + +export default connect(mapStateToProps)(TopicList); diff --git a/app/src/containers/BoardContainer.js b/app/src/containers/BoardContainer.js new file mode 100644 index 0000000..9f78752 --- /dev/null +++ b/app/src/containers/BoardContainer.js @@ -0,0 +1,108 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { drizzle } from '../index'; +import { withRouter } from 'react-router' + +import { Header } from 'semantic-ui-react'; + +import TopicList from '../components/TopicList'; +import FloatingButton from '../components/FloatingButton'; + +/*import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions';*/ + +const contract = "Forum"; +const getNumberOfTopicsMethod = "getNumberOfTopics"; + +class BoardContainer extends Component { + constructor(props) { + super(props); + + /*this.props.store.dispatch(showProgressBar());*/ + + this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this); + + this.state = { + pageLoading: true, + pageLoaded: false + } + } + + handleCreateTopicClick() { + this.props.history.push("/startTopic"); + } + + componentDidUpdate(){ + if (this.state.pageLoading && !this.state.pageLoaded && this.props.drizzleStatus['initialized']){ + this.dataKey = drizzle.contracts[contract].methods[getNumberOfTopicsMethod].cacheCall(); + this.setState({ pageLoading: false }); + } + if (!this.state.pageLoaded && this.dataKey && + this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey]){ + /*this.props.store.dispatch(hideProgressBar());*/ + this.setState({ pageLoaded: true }); + } + } + + render() { + var boardContents; + if (this.dataKey && this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey]){ + var numberOfTopics = this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey].value; + + if (numberOfTopics !== '0'){ + this.topicIDs = []; + for (var i = 0; i < numberOfTopics; i++) { + this.topicIDs.push(i); + } + boardContents = ([ + , +
, + this.props.hasSignedUp && + + ]); + } else { + if (!this.props.hasSignedUp){ + boardContents = ( +
+
+ There are no topics yet! +
+
+ Sign up to be the first to post. +
+
+ ); + } else { + boardContents = ( +
+
+ There are no topics yet! +
+
+ Click the add button at the bottom of the page to be the first to post. +
+ +
+ ); + } + } + } + + return ( +
+ {boardContents} +
+ ); + } +} + +const mapStateToProps = state => { + return { + contracts: state.contracts, + drizzleStatus: state.drizzleStatus, + hasSignedUp: state.user.hasSignedUp + } +}; + +export default withRouter(connect(mapStateToProps)(BoardContainer)); diff --git a/app/src/containers/HomeContainer.js b/app/src/containers/HomeContainer.js index f706b18..9fc06ae 100644 --- a/app/src/containers/HomeContainer.js +++ b/app/src/containers/HomeContainer.js @@ -1,23 +1,26 @@ import React, { Component } from 'react'; -import { connect } from 'react-redux'; + +import BoardContainer from './BoardContainer'; class HomeContainer extends Component { render() { - return (
-
-

Active Account

- {this.props.accounts[0]} -
-
); + //We can add a modal to tell the user to sign up + + /*var modal = this.props.user.hasSignedUp && ( + + Select a Photo + + + +
Default Profile Image
+

We've found the following gravatar image associated with your e-mail address.

+

Is it okay to use this photo?

+
+
+
);*/ + + return (); } } -const mapStateToProps = state => { - return { - accounts: state.accounts, - Forum: state.contracts.Forum, - drizzleStatus: state.drizzleStatus - }; -}; - -export default connect(mapStateToProps)(HomeContainer); +export default HomeContainer; diff --git a/app/src/containers/SignUpContainer.js b/app/src/containers/SignUpContainer.js index aa2691d..16d56cd 100644 --- a/app/src/containers/SignUpContainer.js +++ b/app/src/containers/SignUpContainer.js @@ -4,7 +4,7 @@ import { Header } from 'semantic-ui-react'; import {connect} from "react-redux"; import UsernameFormContainer from './UsernameFormContainer'; -class SignUp extends Component { +class SignUpContainer extends Component { componentDidUpdate(prevProps) { if (this.props.user.hasSignedUp && !prevProps.user.hasSignedUp) this.props.history.push("/"); @@ -40,6 +40,4 @@ const mapStateToProps = state => { } }; -const SignUpContainer = connect(mapStateToProps)(SignUp); - -export default SignUpContainer; +export default connect(mapStateToProps)(SignUpContainer); diff --git a/app/src/containers/StartTopicContainer.js b/app/src/containers/StartTopicContainer.js new file mode 100644 index 0000000..cd9ee4b --- /dev/null +++ b/app/src/containers/StartTopicContainer.js @@ -0,0 +1,132 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { Form, TextArea, Button, Icon } from 'semantic-ui-react' +import NewTopicPreview from '../components/NewTopicPreview' + +import { createTopic } from '../redux/actions/transactionsActions'; + +class StartTopicContainer extends Component { + constructor(props, context) { + super(props); + + this.handleInputChange = this.handleInputChange.bind(this); + this.handlePreviewToggle = this.handlePreviewToggle.bind(this); + this.validateAndPost = this.validateAndPost.bind(this); + + this.state = { + topicSubjectInput: '', + topicMessageInput: '', + topicSubjectInputEmptySubmit: false, + topicMessageInputEmptySubmit: false, + previewEnabled: false, + previewDate: "" + }; + } + + async validateAndPost() { + if (this.state.topicSubjectInput === '' || this.state.topicMessageInput === ''){ + this.setState({ + topicSubjectInputEmptySubmit: this.state.topicSubjectInput === '', + topicMessageInputEmptySubmit: this.state.topicMessageInput === '' + }); + return; + } + + this.props.dispatch( + createTopic( + { + topicSubject: this.state.topicSubjectInput, + topicMessage: this.state.topicMessageInput + } + ) + ); + this.props.history.push("/home"); + } + + handleInputChange(event) { + this.setState({[event.target.name]: event.target.value}); + } + + handlePreviewToggle() { + this.setState((prevState, props) => ({ + 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() { + if (!this.props.user.hasSignedUp) { + this.props.history.push("/signup"); + return(null); + } + + var previewEditText = this.state.previewEnabled ? "Edit" : "Preview"; + return ( +
+ {this.state.previewEnabled && + + } +
+ {!this.state.previewEnabled && + [ + + , +