mirror of https://gitlab.com/ecentrics/concordia
Apostolos Fanakis
6 years ago
17 changed files with 677 additions and 24 deletions
@ -0,0 +1,14 @@ |
|||
import React from 'react'; |
|||
import { Button, Icon } from 'semantic-ui-react' |
|||
|
|||
const FloatingButton = (props) => { |
|||
return ( |
|||
<div className="action-button" onClick={props.onClick}> |
|||
<Button icon color='teal' size='large'> |
|||
<Icon name='add'/> |
|||
</Button> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default FloatingButton; |
@ -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 ( |
|||
<div className="post"> |
|||
<Divider horizontal> |
|||
<span className="grey-text">#0</span> |
|||
</Divider> |
|||
<Grid> |
|||
<Grid.Row columns={16} stretched> |
|||
<Grid.Column width={1} className="user-avatar"> |
|||
<UserAvatar |
|||
size="52" |
|||
className="inline" |
|||
src={this.props.user.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"> |
|||
<TimeAgo date={this.props.date}/> |
|||
</span> |
|||
</div> |
|||
<div className="stretch-space-between"> |
|||
<span><strong> |
|||
Subject: {this.props.subject} |
|||
</strong></span> |
|||
</div> |
|||
<div className="post-content"> |
|||
<ReactMarkdown source={this.props.content} /> |
|||
</div> |
|||
</div> |
|||
</Grid.Column> |
|||
</Grid.Row> |
|||
</Grid> |
|||
</div> |
|||
); |
|||
} |
|||
}; |
|||
|
|||
const mapStateToProps = state => { |
|||
return { |
|||
user: state.user |
|||
} |
|||
}; |
|||
|
|||
export default connect(mapStateToProps)(Post); |
@ -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 ( |
|||
<Card link className="card" |
|||
onClick={() => {this.props.history.push("/topic/" + this.props.topicID)}}> |
|||
<Card.Content> |
|||
<div className={"topic-subject" + (this.topicSubject ? "" : " grey-text")}> |
|||
<p><strong> |
|||
{this.topicSubject !== null ? this.topicSubject |
|||
:<ContentLoader height={5.8} width={300} speed={2} |
|||
primaryColor="#b2e8e6" secondaryColor="#00b5ad" > |
|||
<rect x="0" y="0" rx="3" ry="3" width="150" height="5.5" /> |
|||
</ContentLoader>} |
|||
</strong></p> |
|||
</div> |
|||
<hr/> |
|||
<div className="topic-meta"> |
|||
<p className={"no-margin" + |
|||
(this.props.topicData !== null ? "" : " grey-text")}> |
|||
{this.props.topicData !== null |
|||
?this.props.topicData.value[2] |
|||
:"Username" |
|||
} |
|||
</p> |
|||
<p className={"no-margin" + |
|||
(this.props.topicData !== null ? "" : " grey-text")}> |
|||
{"Number of replies: " + (this.props.topicData !== null |
|||
?this.props.topicData.value[4].length |
|||
:"") |
|||
} |
|||
</p> |
|||
<p className="topic-date grey-text"> |
|||
{this.props.topicData !== null && |
|||
<TimeAgo date={epochTimeConverter(this.props.topicData.value[3])}/> |
|||
} |
|||
</p> |
|||
</div> |
|||
</Card.Content> |
|||
</Card> |
|||
); |
|||
} |
|||
|
|||
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)); |
@ -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 (<Topic |
|||
topicData={(this.dataKeys[topicID] && this.props.contracts[contract][getTopicMethod][this.dataKeys[topicID]]) |
|||
? this.props.contracts[contract][getTopicMethod][this.dataKeys[topicID]] |
|||
: null} |
|||
key={topicID} />) |
|||
}); |
|||
|
|||
return ( |
|||
<div className="topics-list"> |
|||
{topics.slice(0).reverse()} |
|||
</div> |
|||
); |
|||
} |
|||
}; |
|||
|
|||
const mapStateToProps = state => { |
|||
return { |
|||
contracts: state.contracts, |
|||
drizzleStatus: state.drizzleStatus |
|||
} |
|||
}; |
|||
|
|||
export default connect(mapStateToProps)(TopicList); |
@ -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 = ([ |
|||
<TopicList topicIDs={this.topicIDs} key="topicList"/>, |
|||
<div className="bottom-overlay-pad" key="pad"></div>, |
|||
this.props.hasSignedUp && |
|||
<FloatingButton onClick={this.handleCreateTopicClick} |
|||
key="createTopicButton"/> |
|||
]); |
|||
} else { |
|||
if (!this.props.hasSignedUp){ |
|||
boardContents = ( |
|||
<div className="vertical-center-in-parent"> |
|||
<Header color='teal' textAlign='center' as='h2'> |
|||
There are no topics yet! |
|||
</Header> |
|||
<Header color='teal' textAlign='center' as='h4'> |
|||
Sign up to be the first to post. |
|||
</Header> |
|||
</div> |
|||
); |
|||
} else { |
|||
boardContents = ( |
|||
<div className="vertical-center-in-parent"> |
|||
<Header color='teal' textAlign='center' as='h2'> |
|||
There are no topics yet! |
|||
</Header> |
|||
<Header color='teal' textAlign='center' as='h4'> |
|||
Click the add button at the bottom of the page to be the first to post. |
|||
</Header> |
|||
<FloatingButton onClick={this.handleCreateTopicClick} |
|||
key="createTopicButton"/> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<div className="fill"> |
|||
{boardContents} |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const mapStateToProps = state => { |
|||
return { |
|||
contracts: state.contracts, |
|||
drizzleStatus: state.drizzleStatus, |
|||
hasSignedUp: state.user.hasSignedUp |
|||
} |
|||
}; |
|||
|
|||
export default withRouter(connect(mapStateToProps)(BoardContainer)); |
@ -1,23 +1,26 @@ |
|||
import React, { Component } from 'react'; |
|||
import { connect } from 'react-redux'; |
|||
|
|||
import BoardContainer from './BoardContainer'; |
|||
|
|||
class HomeContainer extends Component { |
|||
render() { |
|||
return (<div className="App"> |
|||
<div className="section"> |
|||
<h1>Active Account</h1> |
|||
{this.props.accounts[0]} |
|||
</div> |
|||
</div>); |
|||
//We can add a modal to tell the user to sign up
|
|||
|
|||
/*var modal = this.props.user.hasSignedUp && ( |
|||
<Modal dimmer='blurring' open={this.state.open}> |
|||
<Modal.Header>Select a Photo</Modal.Header> |
|||
<Modal.Content image> |
|||
<Image wrapped size='medium' src='/assets/images/avatar/large/rachel.png' /> |
|||
<Modal.Description> |
|||
<Header>Default Profile Image</Header> |
|||
<p>We've found the following gravatar image associated with your e-mail address.</p> |
|||
<p>Is it okay to use this photo?</p> |
|||
</Modal.Description> |
|||
</Modal.Content> |
|||
</Modal>);*/ |
|||
|
|||
return (<BoardContainer/>); |
|||
} |
|||
} |
|||
|
|||
const mapStateToProps = state => { |
|||
return { |
|||
accounts: state.accounts, |
|||
Forum: state.contracts.Forum, |
|||
drizzleStatus: state.drizzleStatus |
|||
}; |
|||
}; |
|||
|
|||
export default connect(mapStateToProps)(HomeContainer); |
|||
export default HomeContainer; |
|||
|
@ -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 ( |
|||
<div> |
|||
{this.state.previewEnabled && |
|||
<NewTopicPreview |
|||
date={this.state.previewDate} |
|||
subject={this.state.topicSubjectInput} |
|||
content={this.state.topicMessageInput} |
|||
/> |
|||
} |
|||
<Form> |
|||
{!this.state.previewEnabled && |
|||
[<Form.Field key={"topicSubjectInput"}> |
|||
<Form.Input name={"topicSubjectInput"} |
|||
error={this.state.topicSubjectInputEmptySubmit} |
|||
type="text" |
|||
value={this.state.topicSubjectInput} |
|||
placeholder="Subject" |
|||
id="topicSubjectInput" |
|||
onChange={this.handleInputChange} /> |
|||
</Form.Field>, |
|||
<TextArea key={"topicMessageInput"} |
|||
name={"topicMessageInput"} |
|||
className={this.state.topicMessageInputEmptySubmit ? "form-textarea-required" : ""} |
|||
value={this.state.topicMessageInput} |
|||
placeholder="Post" |
|||
id="topicMessageInput" |
|||
rows={5} |
|||
autoHeight |
|||
onChange={this.handleInputChange} />] |
|||
} |
|||
<br/><br/> |
|||
<Button.Group> |
|||
<Button animated key="submit" type="button" color='teal' |
|||
onClick={this.validateAndPost}> |
|||
<Button.Content visible>Post</Button.Content> |
|||
<Button.Content hidden> |
|||
<Icon name='send' /> |
|||
</Button.Content> |
|||
</Button> |
|||
<Button type="button" color='yellow' |
|||
onClick={this.handlePreviewToggle}> |
|||
{previewEditText} |
|||
</Button> |
|||
</Button.Group> |
|||
</Form> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const mapStateToProps = state => { |
|||
return { |
|||
orbitDB: state.orbitDB, |
|||
user: state.user |
|||
} |
|||
}; |
|||
|
|||
export default connect(mapStateToProps)(StartTopicContainer); |
@ -0,0 +1,12 @@ |
|||
const epochTimeConverter = (timestamp) => { |
|||
var timestampDate = new Date(0); |
|||
timestampDate.setUTCSeconds(timestamp); |
|||
return ((timestampDate.getMonth() + 1) + " " |
|||
+ timestampDate.getDate() + ", " |
|||
+ timestampDate.getFullYear() + ", " |
|||
+ timestampDate.getHours() + ":" |
|||
+ timestampDate.getMinutes() + ":" |
|||
+ timestampDate.getSeconds()) |
|||
} |
|||
|
|||
export default epochTimeConverter; |
@ -0,0 +1,58 @@ |
|||
//Action creators
|
|||
import uuid from 'uuid/v1'; |
|||
|
|||
export const INIT_TRANSACTION = 'INIT_TRANSACTION'; |
|||
export const UPDATE_TRANSACTION = 'UPDATE_TRANSACTION'; |
|||
|
|||
export function updateUsername(newUsername, callback){ |
|||
return { |
|||
type: INIT_TRANSACTION, |
|||
transactionDescriptor: |
|||
{ |
|||
contract: 'Forum', |
|||
method: 'updateUsername', |
|||
params: [newUsername], |
|||
event: 'UsernameUpdated' |
|||
}, |
|||
uid: uuid(), |
|||
callback: callback |
|||
}; |
|||
} |
|||
|
|||
export function createTopic(userInputs){ |
|||
return { |
|||
type: INIT_TRANSACTION, |
|||
transactionDescriptor: |
|||
{ |
|||
contract: 'Forum', |
|||
method: 'createTopic', |
|||
params: [], |
|||
event: 'TopicCreated' |
|||
}, |
|||
uid: uuid(), |
|||
userInputs: userInputs |
|||
}; |
|||
} |
|||
|
|||
export function createPost(topicID, userInputs){ |
|||
return { |
|||
type: INIT_TRANSACTION, |
|||
transactionDescriptor: |
|||
{ |
|||
contract: 'Forum', |
|||
method: 'createPost', |
|||
params: [topicID], |
|||
event: 'PostCreated' |
|||
}, |
|||
uid: uuid(), |
|||
userInputs: userInputs |
|||
}; |
|||
} |
|||
|
|||
export function updateTransaction(transactionIndex, updateDescriptor){ |
|||
return { |
|||
type: UPDATE_TRANSACTION, |
|||
index: transactionIndex, |
|||
transactionUpdates: updateDescriptor |
|||
}; |
|||
} |
@ -0,0 +1,39 @@ |
|||
import { INIT_TRANSACTION, UPDATE_TRANSACTION } from '../actions/transactionsMonitorActions'; |
|||
|
|||
const initialState = { |
|||
transactions: Object.create(null) |
|||
}; |
|||
|
|||
const transactionsReducer = (state = initialState, action) => { |
|||
switch (action.type) { |
|||
case INIT_TRANSACTION: |
|||
let transactionsShallowCopy = state.transactions.slice(); |
|||
transactionsShallowCopy.push({ |
|||
status: 'initialized', |
|||
contract: action.transactionDescriptor.contract, |
|||
method: action.transactionDescriptor.method, |
|||
params: action.transactionDescriptor.params, |
|||
event: action.transactionDescriptor.event, |
|||
returnData: null, |
|||
userInputs: action.userInputs |
|||
}); |
|||
return { |
|||
transactions: transactionsShallowCopy |
|||
}; |
|||
case UPDATE_TRANSACTION: |
|||
return { transactions: state.transactions.map( (transaction, index) => { |
|||
if (index !== action.index){ |
|||
return transaction; |
|||
} |
|||
|
|||
return { |
|||
...transaction, |
|||
...action.transactionUpdates |
|||
} |
|||
})}; |
|||
default: |
|||
return state; |
|||
} |
|||
}; |
|||
|
|||
export default transactionsReducer; |
@ -0,0 +1,36 @@ |
|||
import {call, put, select, take, takeEvery} from 'redux-saga/effects' |
|||
import { contract, getCurrentAccount } from './drizzleUtilsSaga'; |
|||
|
|||
import { drizzle } from '../../index' |
|||
|
|||
let transactionsHistory = Object.create(null); |
|||
|
|||
function* initTransaction(action) { |
|||
transactionsHistory[action.uid] = action; |
|||
transactionsHistory[action.uid].dataKey = drizzle.contracts[action.transactionDescriptor.contract] |
|||
.methods[action.transactionDescriptor['method']] |
|||
.cacheSend(...[action.transactionDescriptor.params]); |
|||
|
|||
transactionsHistory[action.uid].state = 'initialized'; |
|||
} |
|||
|
|||
function* completeWithOrbitInteractions(action) { |
|||
const orbit = yield select((state) => state.orbit); |
|||
|
|||
yield call(orbit.topicsDB.put, action.receipt.events['TopicCreated'].returnValues.topicID, { |
|||
subject: 'tada' |
|||
}); |
|||
|
|||
yield call(orbit.postsDB.put, action.receipt.events['TopicCreated'].returnValues.postID, { |
|||
subject: 'tada', |
|||
content: 'it worked!' |
|||
}); |
|||
} |
|||
|
|||
function* transactionsSaga() { |
|||
yield take("DRIZZLE_UTILS_SAGA_INITIALIZED"); |
|||
yield takeEvery("INIT_TRANSACTION", initTransaction); |
|||
yield takeEvery("TX_SUCCESSFUL", completeWithOrbitInteractions); |
|||
} |
|||
|
|||
export default transactionsSaga; |
Loading…
Reference in new issue