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
+
+
+
+
+ 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 &&
+
+ }
+
+
+ ,
+ ]
+ }
+
+
+
+ Post
+
+
+
+
+
+ {previewEditText}
+
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ orbitDB: state.orbitDB,
+ user: state.user
+ }
+};
+
+export default connect(mapStateToProps)(StartTopicContainer);
diff --git a/app/src/containers/UsernameFormContainer.js b/app/src/containers/UsernameFormContainer.js
index b15ad06..833050a 100644
--- a/app/src/containers/UsernameFormContainer.js
+++ b/app/src/containers/UsernameFormContainer.js
@@ -53,7 +53,10 @@ class UsernameFormContainer extends Component {
errorMessage: "You need to provide a username"
});
} else if (!this.state.error) {
- this.completeAction();
+ // Makes sure current input username has been checked for availability
+ if (this.checkedUsernames.some(e => e.usernameChecked === this.state.usernameInput)){
+ this.completeAction();
+ }
}
}
@@ -115,6 +118,17 @@ class UsernameFormContainer extends Component {
const placeholderText = hasSignedUp ? this.props.user.username : "Username";
const withError = this.state.error && {error: true};
+ /*var disableSubmit = true;
+ if (this.checkedUsernames.length > 0) {
+ if (this.checkedUsernames.some(e => e.usernameChecked === this.state.usernameInput)){
+ disableSubmit = false;
+ }
+ } else {
+ disableSubmit = false;
+ }
+
+ disableSubmit = (disableSubmit || this.state.error) && {loading: true};*/
+
return(