Browse Source

Multiple fixes, Init Topic UI

develop
Apostolos Fanakis 6 years ago
parent
commit
6d9d6ac1ab
  1. 175
      app/src/components/NewPost.js
  2. 193
      app/src/components/Post.js
  3. 66
      app/src/components/PostList.js
  4. 3
      app/src/components/Topic.js
  5. 16
      app/src/components/TopicList.js
  6. 23
      app/src/containers/BoardContainer.js
  7. 4
      app/src/containers/NavBarContainer.js
  8. 156
      app/src/containers/TopicContainer.js
  9. 5
      app/src/containers/TransactionsMonitorContainer.js
  10. 10
      app/src/redux/actions/userInterfaceActions.js
  11. 4
      app/src/redux/reducers/rootReducer.js
  12. 20
      app/src/redux/reducers/userInterfaceReducer.js
  13. 3
      app/src/router/routes.js

175
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 (
<div className="post" ref={this.newPostOuterRef}>
<Divider horizontal>
<span className="grey-text">#{this.props.postIndex}</span>
</Divider>
<Grid>
<Grid.Row columns={16} stretched>
<Grid.Column width={1} className="user-avatar">
<UserAvatar
size="52"
className="inline user-avatar"
src={this.props.avatarUrl}
name={this.props.user.username}
/>
</Grid.Column>
<Grid.Column width={15}>
<div className="">
<div className="stretch-space-between">
<span><strong>{this.props.user.username}</strong></span>
<span className="grey-text">
{this.state.previewEnabled &&
<TimeAgo date={this.state.previewDate}/>
}
</span>
</div>
<div className="stretch-space-between">
<span><strong>
{this.state.previewEnabled &&
("Subject: " + this.state.postSubjectInput)
}
</strong></span>
</div>
<div className="post-content">
<div style={{display: this.state.previewEnabled ? "block" : "none"}}>
<ReactMarkdown source={this.state.postContentInput}
className="markdown-preview" />
</div>
<Form className="topic-form">
<Form.Input key={"postSubjectInput"}
style={{display: this.state.previewEnabled ? "none" : ""}}
name={"postSubjectInput"}
error={this.state.postSubjectInputEmptySubmit}
type="text"
value={this.state.postSubjectInput}
placeholder="Subject"
id="postSubjectInput"
onChange={this.handleInputChange} />
<TextArea key={"postContentInput"}
style={{display: this.state.previewEnabled ? "none" : ""}}
name={"postContentInput"}
className={this.state.postContentInputEmptySubmit ? "form-textarea-required" : ""}
value={this.state.postContentInput}
placeholder="Post"
id="postContentInput"
onChange={this.handleInputChange}
rows={4} autoHeight />
<br/><br/>
<Button.Group>
<Button key="submit"
type="button"
onClick={this.validateAndPost}
color='teal'
animated>
<Button.Content visible>Post</Button.Content>
<Button.Content hidden>
<Icon name='reply' />
</Button.Content>
</Button>
<Button type="button"
onClick={this.handlePreviewToggle}
color='yellow'>
{this.state.previewEnabled ? "Edit" : "Preview"}
</Button>
<Button type="button"
onClick={this.props.onCancelClick}
color='red'>
Cancel
</Button>
</Button.Group>
</Form>
</div>
</div>
</Grid.Column>
</Grid.Row>
</Grid>
</div>
);
}
componentDidMount(){
this.newPostOuterRef.current.scrollIntoView(true);
}
}
const mapStateToProps = state => {
return {
orbitDB: state.orbitDB,
user: state.user
}
};
export default connect(mapStateToProps)(NewPost);

193
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
? <UserAvatar
size="52"
className="inline"
src={this.props.avatarUrl}
name={this.props.postData.value[2]}/>
: <div className="user-avatar">
<ContentLoader height={52} width={52} speed={2}
primaryColor="#b2e8e6" secondaryColor="#00b5ad">
<circle cx="26" cy="26" r="26" />
</ContentLoader>
</div>
);
return (
<Transition animation='tada' duration={500} visible={this.state.animateOnToggle}>
<div className="post" ref={this.postRef ? this.postRef : null}>
<Divider horizontal>
<span className="grey-text">#{this.props.postIndex}</span>
</Divider>
<Grid>
<Grid.Row columns={16} stretched>
<Grid.Column width={1} className="user-avatar">
{this.props.postData !== null
?<Link to={"/profile/" + this.props.postData.value[1]
+ "/" + this.props.postData.value[2]}
onClick={(event) => {event.stopPropagation()}}>
{avatarView}
</Link>
:avatarView
}
</Grid.Column>
<Grid.Column width={15}>
<div className="">
<div className="stretch-space-between">
<span className={this.props.postData !== null ? "" : "grey-text"}>
<strong>
{this.props.postData !== null
?this.props.postData.value[2]
:"Username"
}
</strong>
</span>
<span className="grey-text">
{this.props.postData !== null &&
<TimeAgo date={epochTimeConverter(this.props.postData.value[3])}/>
}
</span>
</div>
<div className="stretch-space-between">
<span className={this.state.postSubject === '' ? "" : "grey-text"}>
<strong>
{this.state.postSubject === ''
? <ContentLoader height={5.8} width={300} speed={2}
primaryColor="#b2e8e6" secondaryColor="#00b5ad" >
<rect x="0" y="0" rx="3" ry="3" width="75" height="5.5" />
</ContentLoader>
: 'Subject:' + this.state.postSubject
}
</strong>
</span>
</div>
<div className="post-content">
{this.state.postContent !== ''
? <ReactMarkdown source={this.state.postContent} />
: <ContentLoader height={11.2} width={300} speed={2}
primaryColor="#b2e8e6" secondaryColor="#00b5ad" >
<rect x="0" y="0" rx="3" ry="3" width="180" height="4.0" />
<rect x="0" y="6.5" rx="3" ry="3" width="140" height="4.0" />
</ContentLoader>
}
</div>
</div>
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column floated="right" textAlign="right">
<Button icon size='mini' style={{marginRight: "0px"}}>
<Icon name='chevron up' />
</Button>
<Label color="teal">8000</Label>
<Button icon size='mini'>
<Icon name='chevron down' />
</Button>
<Button icon size='mini'
onClick={this.props.postData
? () => { this.props.navigateTo("/topic/"
+ this.props.postData.value[4] + "/"
+ this.props.postID)}
: () => {}}>
<Icon name='linkify' />
</Button>
</Grid.Column>
</Grid.Row>
</Grid>
</div>
</Transition>
);
}
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));

66
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 (<Post
postData={(this.dataKeys[postID] && this.props.contracts[contract][getPostMethod][this.dataKeys[postID]])
? this.props.contracts[contract][getPostMethod][this.dataKeys[postID]]
: null}
avatarUrl={""}
postIndex={index}
postID={postID}
getFocus={this.props.focusOnPost === postID ? true : false}
key={postID} />)
});
return (
<div>
{this.props.recentToTheTop
?posts.slice(0).reverse()
:posts
}
</div>
);
}
};
const mapStateToProps = state => {
return {
contracts: state.contracts,
drizzleStatus: state.drizzleStatus
}
};
export default connect(mapStateToProps)(PostList);

3
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
}
}

16
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} />)
});

23
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'){

4
app/src/containers/NavBarContainer.js

@ -31,6 +31,9 @@ class NavBarContainer extends Component {
</Menu.Item>
</Menu.Menu>
}
<div className="navBarText">
{this.props.navBarTitle !== '' && <span>{this.props.navBarTitle}</span>}
</div>
</Menu>
);
}
@ -43,6 +46,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({
const mapStateToProps = state => {
return {
hasSignedUp: state.user.hasSignedUp,
navBarTitle: state.interface.navBarTitle
}
};

156
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 = (
(<div>
<PostList postIDs={this.props.contracts[contract][getTopicMethod][this.dataKey].value[4]}
focusOnPost={this.state.postFocus ? this.state.postFocus : null}/>
{this.state.posting &&
<NewPost topicID={this.state.topicID}
subject={this.state.topicSubject}
postIndex={this.props.contracts[contract][getTopicMethod][this.dataKey].value[4].length}
onCancelClick={() => {this.togglePostingState()}}
onPostCreated={() => {this.postCreated()}}
/>
}
<div className="posts-list-spacer"></div>
{this.props.user.hasSignedUp && !this.state.posting &&
<FloatingButton onClick={this.togglePostingState}/>
}
</div>)
)
}
return (
<div className="fill">
{topicContents}
{!this.state.posting &&
<div className="bottom-overlay-pad"></div>
}
</div>
);
}
}
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);

5
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;
}
}

10
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
};
}

4
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
})

20
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;

3
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 = (
<Redirect from='/home' to="/" />
<Route path="/signup" component={SignUpContainer} />
<Route path="/startTopic" component={StartTopicContainer} />
<Route path="/topic/:topicId" component={TopicContainer} />
<Route path='/404' component={NotFound} />
<Route component={NotFound} />
</Switch>
</CoreLayoutContainer>

Loading…
Cancel
Save