mirror of https://gitlab.com/ecentrics/concordia
Apostolos Fanakis
6 years ago
13 changed files with 661 additions and 17 deletions
@ -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); |
@ -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)); |
@ -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); |
@ -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); |
@ -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 |
|||
}; |
|||
} |
@ -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; |
Loading…
Reference in new issue