diff --git a/app/src/components/NewPost.js b/app/src/components/NewPost.js
new file mode 100644
index 0000000..59d79bc
--- /dev/null
+++ b/app/src/components/NewPost.js
@@ -0,0 +1,175 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import { Grid, Form, TextArea, Button, Icon, Divider } from 'semantic-ui-react'
+
+import TimeAgo from 'react-timeago';
+import UserAvatar from 'react-user-avatar';
+import ReactMarkdown from 'react-markdown';
+
+/*import { createPost } from '../redux/actions/transactionsMonitorActions';*/
+
+class NewPost 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.newPostOuterRef = React.createRef();
+
+ this.state = {
+ postSubjectInput: this.props.subject ? this.props.subject : "",
+ postContentInput: '',
+ postSubjectInputEmptySubmit: false,
+ postContentInputEmptySubmit: false,
+ previewEnabled: false,
+ previewDate: ""
+ };
+ }
+
+ async validateAndPost() {
+ if (this.state.postSubjectInput === '' || this.state.postContentInput === ''){
+ this.setState({
+ postSubjectInputEmptySubmit: this.state.postSubjectInput === '',
+ postContentInputEmptySubmit: this.state.postContentInput === ''
+ });
+ return;
+ }
+
+ /*this.props.store.dispatch(
+ createPost(this.props.topicID,
+ {
+ postSubject: this.state.postSubjectInput,
+ postMessage: this.state.postContentInput
+ }
+ )
+ );*/
+ this.props.onPostCreated();
+ }
+
+ 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() {
+ return (
+
+
+ #{this.props.postIndex}
+
+
+
+
+
+
+
+
+
+ {this.props.user.username}
+
+ {this.state.previewEnabled &&
+
+ }
+
+
+
+
+ {this.state.previewEnabled &&
+ ("Subject: " + this.state.postSubjectInput)
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ componentDidMount(){
+ this.newPostOuterRef.current.scrollIntoView(true);
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ orbitDB: state.orbitDB,
+ user: state.user
+ }
+};
+
+export default connect(mapStateToProps)(NewPost);
\ No newline at end of file
diff --git a/app/src/components/Post.js b/app/src/components/Post.js
new file mode 100644
index 0000000..aaa3210
--- /dev/null
+++ b/app/src/components/Post.js
@@ -0,0 +1,193 @@
+import React, { Component } from 'react';
+import { bindActionCreators } from 'redux';
+import { push } from 'connected-react-router'
+import { Link, withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
+
+import ContentLoader from "react-content-loader"
+import { Transition } from 'semantic-ui-react'
+import { Grid, Divider, Button, Icon, Label } from 'semantic-ui-react'
+
+import TimeAgo from 'react-timeago';
+import epochTimeConverter from '../helpers/EpochTimeConverter';
+import UserAvatar from 'react-user-avatar';
+import ReactMarkdown from 'react-markdown';
+
+class Post extends Component {
+ constructor(props) {
+ super(props);
+
+ this.fetchPost = this.fetchPost.bind(this);
+ if (props.getFocus){
+ this.postRef = React.createRef();
+ }
+
+ this.state = {
+ fetchPostDataStatus: 'pending',
+ postContent: '',
+ postSubject: '',
+ readyForAnimation: false,
+ animateOnToggle: true
+ }
+ }
+
+ async fetchPost(postID) {
+ let orbitPostData;
+ if (this.props.postData.value[1] === this.props.user.address) {
+ orbitPostData = this.props.orbitDB.postsDB.get(postID);
+ } else {
+ const fullAddress = "/orbitdb/" + this.props.postData.value[0] + "/posts";
+ const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress);
+ await store.load();
+
+ let localOrbitData = store.get(postID);
+ if (localOrbitData) {
+ orbitPostData = localOrbitData;
+ } else {
+ // Wait until we have received something from the network
+ store.events.on('replicated', () => {
+ orbitPostData = store.get(postID);
+ })
+ }
+ }
+
+ this.setState({
+ postContent: orbitPostData.content,
+ postSubject: orbitPostData.subject,
+ fetchPostDataStatus: 'fetched',
+ readyForAnimation: true
+ });
+ }
+
+ render(){
+ let avatarView = (this.props.postData
+ ?
+ :
+
+
+
+
+ );
+
+ return (
+
+
+
+ #{this.props.postIndex}
+
+
+
+
+ {this.props.postData !== null
+ ? {event.stopPropagation()}}>
+ {avatarView}
+
+ :avatarView
+ }
+
+
+
+
+
+
+ {this.props.postData !== null
+ ?this.props.postData.value[2]
+ :"Username"
+ }
+
+
+
+ {this.props.postData !== null &&
+
+ }
+
+
+
+
+
+ {this.state.postSubject === ''
+ ?
+
+
+ : 'Subject:' + this.state.postSubject
+ }
+
+
+
+
+ {this.state.postContent !== ''
+ ?
+ :
+
+
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ componentDidUpdate() {
+ if (this.props.postData && this.state.fetchPostDataStatus === "pending") {
+ this.setState({ fetchPostDataStatus: 'fetching' });
+ /*this.fetchPost(this.props.postID);*/
+ }
+ if (this.state.readyForAnimation){
+ if (this.postRef){
+ setTimeout(() => {
+ this.postRef.current.scrollIntoView({ block: 'start', behavior: 'smooth' });
+ setTimeout(() => {
+ this.setState({ animateOnToggle: false });
+ }, 300);
+ }, 100);
+ this.setState({ readyForAnimation: false });
+ }
+ }
+ }
+};
+
+const mapDispatchToProps = dispatch => bindActionCreators({
+ navigateTo: (location) => push(location)
+}, dispatch);
+
+const mapStateToProps = state => {
+ return {
+ user: state.user,
+ orbitDB: state.orbit
+ }
+};
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Post));
\ No newline at end of file
diff --git a/app/src/components/PostList.js b/app/src/components/PostList.js
new file mode 100644
index 0000000..65e12fa
--- /dev/null
+++ b/app/src/components/PostList.js
@@ -0,0 +1,66 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { drizzle } from '../index';
+
+import Post from './Post';
+
+const contract = "Forum";
+const getPostMethod = "getPost";
+
+class PostList extends Component {
+ constructor(props) {
+ super(props);
+
+ this.dataKeys = [];
+
+ if (this.props.drizzleStatus['initialized']){
+ this.props.postIDs.forEach( postID => {
+ if (!this.dataKeys[postID]) {
+ this.dataKeys[postID] = drizzle.contracts[contract].methods[getPostMethod].cacheCall(postID);
+ }
+ })
+ }
+ }
+
+ componentDidUpdate(){
+ if (this.props.drizzleStatus['initialized']){
+ this.props.postIDs.forEach( postID => {
+ if (!this.dataKeys[postID]) {
+ this.dataKeys[postID] = drizzle.contracts[contract].methods[getPostMethod].cacheCall(postID);
+ }
+ })
+ }
+ }
+
+ render() {
+ const posts = this.props.postIDs.map((postID, index) => {
+ return ()
+ });
+
+ return (
+
+ {this.props.recentToTheTop
+ ?posts.slice(0).reverse()
+ :posts
+ }
+
+ );
+ }
+};
+
+const mapStateToProps = state => {
+ return {
+ contracts: state.contracts,
+ drizzleStatus: state.drizzleStatus
+ }
+};
+
+export default connect(mapStateToProps)(PostList);
diff --git a/app/src/components/Topic.js b/app/src/components/Topic.js
index 4f97352..35cbebb 100644
--- a/app/src/components/Topic.js
+++ b/app/src/components/Topic.js
@@ -97,7 +97,8 @@ class Topic extends Component {
const mapStateToProps = state => {
return {
user: state.user,
- orbitDB: state.orbit
+ orbitDB: state.orbit,
+ topicsDB: state.topicsDB
}
}
diff --git a/app/src/components/TopicList.js b/app/src/components/TopicList.js
index f81a376..a63cebe 100644
--- a/app/src/components/TopicList.js
+++ b/app/src/components/TopicList.js
@@ -13,23 +13,22 @@ class TopicList extends Component {
this.dataKeys = [];
- this.state = {
- topicsLoading: true
+ if (this.props.drizzleStatus['initialized']){
+ this.props.topicIDs.forEach( topicID => {
+ if (!this.dataKeys[topicID]) {
+ this.dataKeys[topicID] = drizzle.contracts[contract].methods[getTopicMethod].cacheCall(topicID);
+ }
+ })
}
}
componentDidUpdate(){
- if (this.state.topicsLoading && this.props.drizzleStatus['initialized']){
- var topicsLoading = false;
-
+ if (this.props.drizzleStatus['initialized']){
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 });
}
}
@@ -39,6 +38,7 @@ class TopicList extends Component {
topicData={(this.dataKeys[topicID] && this.props.contracts[contract][getTopicMethod][this.dataKeys[topicID]])
? this.props.contracts[contract][getTopicMethod][this.dataKeys[topicID]]
: null}
+ topicID={topicID}
key={topicID} />)
});
diff --git a/app/src/containers/BoardContainer.js b/app/src/containers/BoardContainer.js
index 4c90ef2..cca208a 100644
--- a/app/src/containers/BoardContainer.js
+++ b/app/src/containers/BoardContainer.js
@@ -21,9 +21,17 @@ class BoardContainer extends Component {
this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this);
+ var pageStatus = 'initialized';
+ if (this.props.drizzleStatus['initialized']){
+ this.dataKey = drizzle.contracts[contract].methods[getNumberOfTopicsMethod].cacheCall();
+ pageStatus = 'loading';
+ }
+ if (this.dataKey && this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey]){
+ pageStatus = 'loaded';
+ }
+
this.state = {
- pageLoading: true,
- pageLoaded: false
+ pageStatus: pageStatus
}
}
@@ -32,20 +40,21 @@ class BoardContainer extends Component {
}
componentDidUpdate(){
- if (this.state.pageLoading && !this.state.pageLoaded && this.props.drizzleStatus['initialized']){
+ if (this.state.pageStatus === 'initialized' &&
+ this.props.drizzleStatus['initialized']){
this.dataKey = drizzle.contracts[contract].methods[getNumberOfTopicsMethod].cacheCall();
- this.setState({ pageLoading: false });
+ this.setState({ pageStatus: 'loading' });
}
- if (!this.state.pageLoaded && this.dataKey &&
+ if (this.state.pageStatus === 'loading' &&
this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey]){
+ this.setState({ pageStatus: 'loaded' });
/*this.props.store.dispatch(hideProgressBar());*/
- this.setState({ pageLoaded: true });
}
}
render() {
var boardContents;
- if (this.dataKey && this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey]){
+ if (this.state.pageStatus === 'loaded'){
var numberOfTopics = this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey].value;
if (numberOfTopics !== '0'){
diff --git a/app/src/containers/NavBarContainer.js b/app/src/containers/NavBarContainer.js
index 8e6cb1c..93a0297 100644
--- a/app/src/containers/NavBarContainer.js
+++ b/app/src/containers/NavBarContainer.js
@@ -31,6 +31,9 @@ class NavBarContainer extends Component {
}
+
+ {this.props.navBarTitle !== '' && {this.props.navBarTitle}}
+
);
}
@@ -43,6 +46,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({
const mapStateToProps = state => {
return {
hasSignedUp: state.user.hasSignedUp,
+ navBarTitle: state.interface.navBarTitle
}
};
diff --git a/app/src/containers/TopicContainer.js b/app/src/containers/TopicContainer.js
new file mode 100644
index 0000000..aca4551
--- /dev/null
+++ b/app/src/containers/TopicContainer.js
@@ -0,0 +1,156 @@
+import React, { Component } from 'react';
+import { bindActionCreators } from 'redux';
+import { push } from 'connected-react-router'
+import { connect } from 'react-redux';
+import { drizzle } from '../index';
+
+import PostList from '../components/PostList';
+import NewPost from '../components/NewPost';
+import FloatingButton from '../components/FloatingButton';
+
+import { setNavBarTitle } from '../redux/actions/userInterfaceActions.js';
+
+const contract = "Forum";
+const getTopicMethod = "getTopic";
+
+class TopicContainer extends Component {
+ constructor(props) {
+ super(props);
+
+ //Topic ID should be a positive integer
+ if (!/^[0-9]+$/.test(this.props.match.params.topicId)){
+ this.props.navigateTo('/404');
+ }
+
+ this.fetchTopicSubject = this.fetchTopicSubject.bind(this);
+ this.togglePostingState = this.togglePostingState.bind(this);
+ this.postCreated = this.postCreated.bind(this);
+
+ var pageStatus = 'initialized';
+ if (this.props.drizzleStatus['initialized']) {
+ this.dataKey = drizzle.contracts[contract].methods[getTopicMethod].cacheCall(this.props.match.params.topicId);
+ pageStatus = 'loading';
+ }
+ if (this.dataKey && this.props.contracts[contract][getTopicMethod][this.dataKey]) {
+ pageStatus = 'loaded';
+ }
+
+ this.state = {
+ pageStatus: pageStatus,
+ topicID: this.props.match.params.topicId,
+ topicSubject: null,
+ postFocus: this.props.match.params.postId && /^[0-9]+$/.test(this.props.match.params.postId)
+ ? this.props.match.params.postId
+ : null,
+ fetchTopicSubjectStatus: null,
+ posting: false
+ };
+ }
+
+ componentDidUpdate() {
+ if (this.state.pageStatus === 'initialized' &&
+ this.props.drizzleStatus['initialized']) {
+ this.dataKey = drizzle.contracts[contract].methods[getTopicMethod].cacheCall(this.state.topicId);
+ this.setState({ pageStatus: 'loading' });
+ }
+ if (this.state.pageStatus === 'loading' &&
+ this.props.contracts[contract][getTopicMethod][this.dataKey]) {
+ this.setState({ pageStatus: 'loaded' });
+ if (this.state.fetchTopicSubjectStatus === null){
+ this.setState({ fetchTopicSubjectStatus: "fetching"})
+ /*this.fetchTopicSubject(this.props.contracts[contract][getTopicMethod][this.dataKey].value[0]);*/
+ }
+ }
+ }
+
+ async fetchTopicSubject(orbitDBAddress) {
+ let orbitData;
+ if (this.props.contracts[contract][getTopicMethod][this.dataKey].value[1] === this.props.user.address) {
+ orbitData = this.props.orbitDB.topicsDB.get(this.state.topicID);
+ } else {
+ const fullAddress = "/orbitdb/" + orbitDBAddress + "/topics";
+ const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress);
+ await store.load();
+
+ let localOrbitData = store.get(this.state.topicID);
+ if (localOrbitData) {
+ orbitData = localOrbitData;
+ } else {
+ // Wait until we have received something from the network
+ store.events.on('replicated', () => {
+ orbitData = store.get(this.state.topicID);
+ })
+ }
+ }
+
+ this.props.setNavBarTitle(orbitData['subject']);
+ this.setState({
+ 'topicSubject': orbitData['subject'],
+ fetchTopicSubjectStatus: "fetched"
+ });
+ }
+
+ togglePostingState(event) {
+ if (event){
+ event.preventDefault();
+ }
+ this.setState(prevState => ({
+ posting: !prevState.posting
+ }));
+ }
+
+ postCreated(){
+ this.setState(prevState => ({
+ posting: false
+ }));
+ }
+
+ render() {
+ var topicContents;
+ if (this.state.pageStatus === 'loaded') {
+ topicContents = (
+ (
+
+ {this.state.posting &&
+
{this.togglePostingState()}}
+ onPostCreated={() => {this.postCreated()}}
+ />
+ }
+
+ {this.props.user.hasSignedUp && !this.state.posting &&
+
+ }
+ )
+ )
+ }
+
+ return (
+
+ {topicContents}
+ {!this.state.posting &&
+
+ }
+
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => bindActionCreators({
+ navigateTo: (location) => push(location),
+ setNavBarTitle: (navBarTitle) => setNavBarTitle(navBarTitle)
+}, dispatch);
+
+const mapStateToProps = state => {
+ return {
+ user: state.user,
+ contracts: state.contracts,
+ drizzleStatus: state.drizzleStatus,
+ orbitDB: state.orbit
+ }
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(TopicContainer);
\ No newline at end of file
diff --git a/app/src/containers/TransactionsMonitorContainer.js b/app/src/containers/TransactionsMonitorContainer.js
index 5199902..100b5e5 100644
--- a/app/src/containers/TransactionsMonitorContainer.js
+++ b/app/src/containers/TransactionsMonitorContainer.js
@@ -25,6 +25,10 @@ class RightSideBar extends Component {
if (this.props.transactions[transactionHash].receipt &&
this.props.transactions[transactionHash].receipt.events) {
switch (Object.keys(this.props.transactions[transactionHash].receipt.events)[0]){
+ case 'UserSignedUp':
+ this.props.history.push("/profile");
+ this.handleMessageDismiss(null, index);
+ break;
case 'TopicCreated':
this.props.history.push("/topic/" +
this.props.transactions[transactionHash].receipt.events.TopicCreated.returnValues.topicID
@@ -32,6 +36,7 @@ class RightSideBar extends Component {
this.handleMessageDismiss(null, index);
break;
default:
+ this.handleMessageDismiss(null, index);
break;
}
}
diff --git a/app/src/redux/actions/userInterfaceActions.js b/app/src/redux/actions/userInterfaceActions.js
new file mode 100644
index 0000000..ca23345
--- /dev/null
+++ b/app/src/redux/actions/userInterfaceActions.js
@@ -0,0 +1,10 @@
+//Action creators
+
+export const SET_NAVBAR_TITLE = 'SET_NAVBAR_TITLE';
+
+export function setNavBarTitle(newTitle){
+ return {
+ type: SET_NAVBAR_TITLE,
+ title: newTitle
+ };
+}
diff --git a/app/src/redux/reducers/rootReducer.js b/app/src/redux/reducers/rootReducer.js
index 03155e9..0e5cef0 100644
--- a/app/src/redux/reducers/rootReducer.js
+++ b/app/src/redux/reducers/rootReducer.js
@@ -2,11 +2,13 @@ import { combineReducers } from 'redux';
import { drizzleReducers } from 'drizzle';
import { connectRouter } from 'connected-react-router'
import userReducer from './userReducer';
-import orbitReducer from "./orbitReducer";
+import orbitReducer from './orbitReducer';
+import userInterfaceReducer from './userInterfaceReducer';
export default (history) => combineReducers({
router: connectRouter(history),
user: userReducer,
orbit: orbitReducer,
+ interface: userInterfaceReducer,
...drizzleReducers
})
\ No newline at end of file
diff --git a/app/src/redux/reducers/userInterfaceReducer.js b/app/src/redux/reducers/userInterfaceReducer.js
new file mode 100644
index 0000000..f95f8c3
--- /dev/null
+++ b/app/src/redux/reducers/userInterfaceReducer.js
@@ -0,0 +1,20 @@
+import {
+ SET_NAVBAR_TITLE
+} from '../actions/userInterfaceActions';
+
+const initialState = {
+ navBarTitle: ''
+};
+
+const userInterfaceReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case SET_NAVBAR_TITLE:
+ return {
+ navBarTitle: action.title
+ }
+ default:
+ return state;
+ }
+};
+
+export default userInterfaceReducer;
diff --git a/app/src/router/routes.js b/app/src/router/routes.js
index bcecf7f..c15e9f6 100644
--- a/app/src/router/routes.js
+++ b/app/src/router/routes.js
@@ -4,6 +4,7 @@ import CoreLayoutContainer from '../containers/CoreLayoutContainer';
import HomeContainer from '../containers/HomeContainer'
import SignUpContainer from '../containers/SignUpContainer'
import StartTopicContainer from '../containers/StartTopicContainer'
+import TopicContainer from '../containers/TopicContainer'
import NotFound from '../components/NotFound'
const routes = (
@@ -14,6 +15,8 @@ const routes = (
+
+