Browse Source

Init home screen and topic starting

develop
Apostolos Fanakis 6 years ago
parent
commit
1dfad58fdb
  1. 7
      app/package.json
  2. 14
      app/src/components/FloatingButton.js
  3. 65
      app/src/components/NewTopicPreview.js
  4. 104
      app/src/components/Topic.js
  5. 60
      app/src/components/TopicList.js
  6. 108
      app/src/containers/BoardContainer.js
  7. 35
      app/src/containers/HomeContainer.js
  8. 6
      app/src/containers/SignUpContainer.js
  9. 132
      app/src/containers/StartTopicContainer.js
  10. 16
      app/src/containers/UsernameFormContainer.js
  11. 12
      app/src/helpers/EpochTimeConverter.js
  12. 4
      app/src/index.js
  13. 58
      app/src/redux/actions/transactionsActions.js
  14. 39
      app/src/redux/reducers/transactionsReducer.js
  15. 3
      app/src/redux/sagas/rootSaga.js
  16. 36
      app/src/redux/sagas/transactionsSaga.js
  17. 2
      app/src/router/routes.js

7
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",

14
app/src/components/FloatingButton.js

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

65
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 (
<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);

104
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 (
<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));

60
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 (<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);

108
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 = ([
<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));

35
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 (<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;

6
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);

132
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 (
<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);

16
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(
<div>
<Form onSubmit={this.handleSubmit} {...withError}>

12
app/src/helpers/EpochTimeConverter.js

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

4
app/src/index.js

@ -14,7 +14,9 @@ import drizzleOptions from "./config/drizzleOptions";
initIPFS();
export const drizzle = new Drizzle(drizzleOptions, store);
const drizzle = new Drizzle(drizzleOptions, store);
export { drizzle };
render(
<Provider store={store}>

58
app/src/redux/actions/transactionsActions.js

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

39
app/src/redux/reducers/transactionsReducer.js

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

3
app/src/redux/sagas/rootSaga.js

@ -3,9 +3,10 @@ import { drizzleSagas } from 'drizzle'
import drizzleUtilsSaga from './drizzleUtilsSaga'
import userSaga from './userSaga';
import orbitSaga from "./orbitSaga";
import transactionsSaga from "./transactionsSaga";
export default function* root() {
let sagas = [...drizzleSagas, drizzleUtilsSaga, orbitSaga, userSaga];
let sagas = [...drizzleSagas, drizzleUtilsSaga, orbitSaga, userSaga, transactionsSaga];
yield all(
sagas.map(saga => fork(saga))
)

36
app/src/redux/sagas/transactionsSaga.js

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

2
app/src/router/routes.js

@ -3,6 +3,7 @@ import { Route, Redirect, Switch } from 'react-router-dom'
import CoreLayoutContainer from '../containers/CoreLayoutContainer';
import HomeContainer from '../containers/HomeContainer'
import SignUpContainer from '../containers/SignUpContainer'
import StartTopicContainer from '../containers/StartTopicContainer'
import NotFound from '../components/NotFound'
const routes = (
@ -12,6 +13,7 @@ const routes = (
<Route exact path="/" component={HomeContainer} />
<Redirect from='/home' to="/" />
<Route path="/signup" component={SignUpContainer} />
<Route path="/startTopic" component={StartTopicContainer} />
<Route component={NotFound} />
</Switch>
</CoreLayoutContainer>

Loading…
Cancel
Save