Browse Source

ESLint init

develop
Ezerous 6 years ago
parent
commit
c5bd23377b
  1. 15
      .eslintrc.json
  2. 1
      .eslintrignore
  3. 3
      app/.eslintrc.json
  4. 1
      app/.eslintrignore
  5. 5
      app/package.json
  6. 174
      app/src/assets/css/App.css
  7. 24
      app/src/assets/css/board-container.css
  8. 4
      app/src/assets/css/profile-container.css
  9. 14
      app/src/assets/css/sign-up-container.css
  10. 6
      app/src/assets/css/start-topic-container.css
  11. 50
      app/src/assets/css/topic-container.css
  12. 11742
      app/src/assets/fonts/fontawesome-free-5.7.2/all.js
  13. 20
      app/src/components/FloatingButton.js
  14. 27
      app/src/components/LoadingSpinner.js
  15. 349
      app/src/components/NewPost.js
  16. 109
      app/src/components/NewTopicPreview.js
  17. 17
      app/src/components/NotFound.js
  18. 431
      app/src/components/Post.js
  19. 120
      app/src/components/PostList.js
  20. 224
      app/src/components/ProfileInformation.js
  21. 213
      app/src/components/Topic.js
  22. 108
      app/src/components/TopicList.js
  23. 32
      app/src/config/drizzleOptions.js
  24. 24
      app/src/config/ipfsOptions.js
  25. 193
      app/src/containers/BoardContainer.js
  26. 47
      app/src/containers/CoreLayoutContainer.js
  27. 28
      app/src/containers/HomeContainer.js
  28. 88
      app/src/containers/NavBarContainer.js
  29. 323
      app/src/containers/ProfileContainer.js
  30. 67
      app/src/containers/SignUpContainer.js
  31. 241
      app/src/containers/StartTopicContainer.js
  32. 291
      app/src/containers/TopicContainer.js
  33. 221
      app/src/containers/TransactionsMonitorContainer.js
  34. 329
      app/src/containers/UsernameFormContainer.js
  35. 20
      app/src/helpers/EpochTimeConverter.js
  36. 22
      app/src/index.js
  37. 22
      app/src/redux/actions/orbitActions.js
  38. 86
      app/src/redux/actions/transactionsActions.js
  39. 12
      app/src/redux/actions/userInterfaceActions.js
  40. 93
      app/src/redux/reducers/orbitReducer.js
  41. 16
      app/src/redux/reducers/rootReducer.js
  42. 18
      app/src/redux/reducers/userInterfaceReducer.js
  43. 42
      app/src/redux/reducers/userReducer.js
  44. 41
      app/src/redux/sagas/drizzleUtilsSaga.js
  45. 82
      app/src/redux/sagas/orbitSaga.js
  46. 25
      app/src/redux/sagas/rootSaga.js
  47. 116
      app/src/redux/sagas/transactionsSaga.js
  48. 84
      app/src/redux/sagas/userSaga.js
  49. 25
      app/src/redux/store.js
  50. 43
      app/src/router/PrivateRoute.js
  51. 49
      app/src/router/routes.js
  52. 140
      app/src/utils/drizzleUtils.js
  53. 92
      app/src/utils/orbitUtils.js
  54. 129
      app/src/utils/serviceWorker.js
  55. 7
      package.json
  56. 40
      truffle-config.js

15
.eslintrc.json

@ -0,0 +1,15 @@
{
"rules": {
"comma-dangle": ["error", "never"],
"no-console": "off",
"no-unused-vars": "warn",
"object-curly-newline": ["error", {
"ObjectExpression": "always",
"ObjectPattern": { "multiline": true },
"ImportDeclaration": "never",
"ExportDeclaration": "never"
}],
"object-curly-spacing": ["error", "always"]
},
"extends": "airbnb"
}

1
.eslintrignore

@ -0,0 +1 @@
# node_modules in the project root is ignored by default

3
app/.eslintrc.json

@ -0,0 +1,3 @@
{
"extends": "plugin:react/recommended"
}

1
app/.eslintrignore

@ -0,0 +1 @@
node_modules/*

5
app/package.json

@ -25,7 +25,7 @@
"react-user-avatar": "1.10.0", "react-user-avatar": "1.10.0",
"redux": "4.0.1", "redux": "4.0.1",
"redux-saga": "0.16.2", "redux-saga": "0.16.2",
"semantic-ui-react": "0.85.0", "semantic-ui-react": "0.86.0",
"uuid": "3.3.2", "uuid": "3.3.2",
"web3": "1.0.0-beta.48" "web3": "1.0.0-beta.48"
}, },
@ -35,9 +35,6 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [ "browserslist": [
">0.2%", ">0.2%",
"not dead", "not dead",

174
app/src/assets/css/App.css

@ -1,161 +1,161 @@
/* PAGE */ /* PAGE */
html, body { html, body {
margin: 0; margin: 0;
display: block; display: block;
height: 100%; height: 100%;
} }
strong { strong {
font-weight: bold !important; font-weight: bold !important;
} }
#root { #root {
height: 100%; height: 100%;
} }
.App { .App {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0px; margin: 0px;
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
align-items: flex-start; align-items: flex-start;
} }
.page-container { .page-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 71px 0px 0px; margin: 71px 0px 0px;
} }
.left-side-panel { .left-side-panel {
margin-top: 71px; margin-top: 71px;
position: fixed; position: fixed;
width: 20%; width: 20%;
height: calc(100% - 71px); height: calc(100% - 71px);
top: 0; top: 0;
left: 0; left: 0;
} }
.main-panel { .main-panel {
width: 60%; width: 60%;
height: 100%; height: 100%;
margin: 0px 20%; margin: 0px 20%;
} }
.right-side-panel { .right-side-panel {
margin-top: 71px; margin-top: 71px;
position: fixed; position: fixed;
width: 20%; width: 20%;
height: calc(100% - 71px); height: calc(100% - 71px);
top: 0; top: 0;
right: 0; right: 0;
} }
.sidebar-message { .sidebar-message {
margin: 0px 5px 12px 12px; margin: 0px 5px 12px 12px;
padding: 0px; padding: 0px;
} }
.view-container { .view-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0px auto; margin: 0px auto;
} }
/* MISC */ /* MISC */
.navBarText { .navBarText {
height: 61px; height: 61px;
width: 1192px; width: 1192px;
position: absolute; position: absolute;
left: calc(50% - 596px); left: calc(50% - 596px);
text-align: center; text-align: center;
z-index: -1; /* Temporary (?) */ z-index: -1; /* Temporary (?) */
} }
.navBarText span { .navBarText span {
color: #00b5ad; color: #00b5ad;
height: 61px; height: 61px;
line-height: 61px; line-height: 61px;
vertical-align: middle; vertical-align: middle;
font-size: 1.5em; font-size: 1.5em;
} }
.form-textarea-required { .form-textarea-required {
color: rgb(159, 58, 56) !important; color: rgb(159, 58, 56) !important;
outline-color: rgb(159, 58, 56) !important; outline-color: rgb(159, 58, 56) !important;
border-color: rgb(224, 180, 180) !important; border-color: rgb(224, 180, 180) !important;
background-color: rgb(255, 246, 246) !important; background-color: rgb(255, 246, 246) !important;
} }
.card { .card {
width: 100% !important; width: 100% !important;
} }
.bottom-overlay-pad { .bottom-overlay-pad {
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
z-index: 10; z-index: 10;
position: fixed; position: fixed;
bottom: 0px; bottom: 0px;
height: 62px; height: 62px;
width: 60%; width: 60%;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }
.action-button { .action-button {
z-index: 11; z-index: 11;
position: fixed; position: fixed;
bottom: 10px; bottom: 10px;
left: calc(50% - 24px); left: calc(50% - 24px);
} }
.grey-text { .grey-text {
color: grey; color: grey;
} }
.inline { .inline {
display: inline-block; display: inline-block;
} }
.no-margin { .no-margin {
margin: 0px; margin: 0px;
} }
hr { hr {
color: #0c1a2b; color: #0c1a2b;
margin: 0px; margin: 0px;
} }
*:focus { *:focus {
outline:none !important outline: none !important
} }
a { a {
color:inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
.center-in-parent { .center-in-parent {
width: 100%; width: 100%;
text-align: center; text-align: center;
} }
.vertical-center-in-parent { .vertical-center-in-parent {
vertical-align: middle; vertical-align: middle;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
.vertical-center-children { .vertical-center-children {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
#overlay { #overlay {
@ -167,21 +167,21 @@ a {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: rgba(0,0,0,0.5); background-color: rgba(0, 0, 0, 0.5);
z-index: 2; z-index: 2;
} }
#overlay-content{ #overlay-content {
position: absolute; position: absolute;
text-align: center; text-align: center;
top: 50%; top: 50%;
left: 50%; left: 50%;
color: white; color: white;
transform: translate(-50%,-50%); transform: translate(-50%, -50%);
-ms-transform: translate(-50%,-50%); -ms-transform: translate(-50%, -50%);
} }
.fill { .fill {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

24
app/src/assets/css/board-container.css

@ -1,30 +1,30 @@
/* TOPICS LIST SCREEN */ /* TOPICS LIST SCREEN */
.topics-list { .topics-list {
padding: 0px 2px; padding: 0px 2px;
margin-bottom: 75px; margin-bottom: 75px;
} }
.topics-list a { .topics-list a {
color: black !important; color: black !important;
text-decoration: none !important; text-decoration: none !important;
} }
.topics-list a:hover { .topics-list a:hover {
color: black !important; color: black !important;
text-decoration: none !important; text-decoration: none !important;
} }
.topic-subject { .topic-subject {
margin: 0px 0px 5px; margin: 0px 0px 5px;
} }
.topic-meta { .topic-meta {
margin: 5px 0px 0px; margin: 5px 0px 0px;
} }
.topic-date { .topic-date {
margin-bottom: 0px; margin-bottom: 0px;
font-size: 0.77vw !important; font-size: 0.77vw !important;
text-align: right; text-align: right;
} }

4
app/src/assets/css/profile-container.css

@ -1,5 +1,5 @@
/* PROFILE SCREEN */ /* PROFILE SCREEN */
.profile-tab { .profile-tab {
width: 100%; width: 100%;
} }

14
app/src/assets/css/sign-up-container.css

@ -1,12 +1,12 @@
/* SIGN UP SCREEN */ /* SIGN UP SCREEN */
.sign-up-container { .sign-up-container {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
.sign-up-container>div { .sign-up-container > div {
margin: auto; margin: auto;
} }

6
app/src/assets/css/start-topic-container.css

@ -1,6 +1,6 @@
/* START TOPIC SCREEN */ /* START TOPIC SCREEN */
.topic-form { .topic-form {
width: 100%; width: 100%;
margin: 20px 0px; margin: 20px 0px;
} }

50
app/src/assets/css/topic-container.css

@ -1,51 +1,51 @@
/* POSTS LIST SCREEN */ /* POSTS LIST SCREEN */
.posts-list-spacer { .posts-list-spacer {
margin-bottom: 85px; margin-bottom: 85px;
height: 0px; height: 0px;
} }
.post { .post {
width: 100%; width: 100%;
background-color: #FFFFFF; background-color: #FFFFFF;
margin: 20px 0px; margin: 20px 0px;
padding: 0px; padding: 0px;
} }
.post-meta { .post-meta {
float: right; float: right;
margin-right: 11.25px; margin-right: 11.25px;
} }
.user-avatar { .user-avatar {
width: 52px; width: 52px;
height: 52px; height: 52px;
text-align: center; text-align: center;
} }
.user-avatar a { .user-avatar a {
color: inherit !important; color: inherit !important;
text-decoration: none !important; text-decoration: none !important;
} }
.stretch-space-between { .stretch-space-between {
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
justify-content: space-between; justify-content: space-between;
} }
.user-info { .user-info {
background-color: #FFFFFF; background-color: #FFFFFF;
margin: 12px auto; margin: 12px auto;
padding: 7px; padding: 7px;
} }
.post-content a{ .post-content a {
margin-top: 10px; margin-top: 10px;
color: #039be5; color: #039be5;
} }
.post-form { .post-form {
width: 100%; width: 100%;
margin: 20px 0px; margin: 20px 0px;
} }

11742
app/src/assets/fonts/fontawesome-free-5.7.2/all.js

File diff suppressed because one or more lines are too long

20
app/src/components/FloatingButton.js

@ -1,14 +1,12 @@
import React from 'react'; import React from 'react';
import { Button, Icon } from 'semantic-ui-react' import { Button, Icon } from 'semantic-ui-react';
const FloatingButton = (props) => { const FloatingButton = props => (
return ( <div className="action-button" onClick={props.onClick}>
<div className="action-button" onClick={props.onClick}> <Button icon color="teal" size="large">
<Button icon color='teal' size='large'> <Icon name="add" />
<Icon name='add'/> </Button>
</Button> </div>
</div> );
);
};
export default FloatingButton; export default FloatingButton;

27
app/src/components/LoadingSpinner.js

@ -1,16 +1,17 @@
import React from 'react'; import React from 'react';
const LoadingSpinner = (props) => { const LoadingSpinner = props => (
return( <div className="vertical-center-children">
<div className="vertical-center-children"> <div
<div className={"center-in-parent " + (props.className ? props.className : "")} className={`center-in-parent ${
style={props.style ? props.style : []}> props.className ? props.className : ''}`}
<p> style={props.style ? props.style : []}
<i className="fas fa-spinner fa-3x fa-spin"></i> >
</p> <p>
</div> <i className="fas fa-spinner fa-3x fa-spin" />
</div> </p>
); </div>
} </div>
);
export default LoadingSpinner; export default LoadingSpinner;

349
app/src/components/NewPost.js

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Grid, Form, TextArea, Button, Icon, Divider } from 'semantic-ui-react' import { Button, Divider, Form, Grid, Icon, TextArea } from 'semantic-ui-react';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import UserAvatar from 'react-user-avatar'; import UserAvatar from 'react-user-avatar';
@ -10,166 +10,201 @@ import ReactMarkdown from 'react-markdown';
import { createPost } from '../redux/actions/transactionsActions'; import { createPost } from '../redux/actions/transactionsActions';
class NewPost extends Component { class NewPost extends Component {
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
this.handlePreviewToggle = this.handlePreviewToggle.bind(this); this.handlePreviewToggle = this.handlePreviewToggle.bind(this);
this.validateAndPost = this.validateAndPost.bind(this); this.validateAndPost = this.validateAndPost.bind(this);
this.newPostOuterRef = React.createRef(); this.newPostOuterRef = React.createRef();
this.state = { this.state = {
postSubjectInput: this.props.subject ? this.props.subject : "", postSubjectInput: this.props.subject ? this.props.subject : '',
postContentInput: '', postContentInput: '',
postSubjectInputEmptySubmit: false, postSubjectInputEmptySubmit: false,
postContentInputEmptySubmit: false, postContentInputEmptySubmit: false,
previewEnabled: false, previewEnabled: false,
previewDate: '' previewDate: ''
}; };
}
async validateAndPost() {
if (this.state.postSubjectInput === '' || this.state.postContentInput
=== '') {
this.setState({
postSubjectInputEmptySubmit: this.state.postSubjectInput === '',
postContentInputEmptySubmit: this.state.postContentInput === ''
});
return;
} }
async validateAndPost() { this.props.dispatch(
if (this.state.postSubjectInput === '' || this.state.postContentInput === ''){ createPost(this.props.topicID,
this.setState({ {
postSubjectInputEmptySubmit: this.state.postSubjectInput === '', postSubject: this.state.postSubjectInput,
postContentInputEmptySubmit: this.state.postContentInput === '' postMessage: this.state.postContentInput
}); }),
return; );
} this.props.onPostCreated();
}
this.props.dispatch(
createPost(this.props.topicID, handleInputChange(event) {
{ this.setState({
postSubject: this.state.postSubjectInput, [event.target.name]: event.target.value
postMessage: this.state.postContentInput });
} }
)
); handlePreviewToggle() {
this.props.onPostCreated(); this.setState((prevState, props) => ({
} previewEnabled: !prevState.previewEnabled,
previewDate: this.getDate()
handleInputChange(event) { }));
this.setState({[event.target.name]: event.target.value}); }
}
getDate() {
handlePreviewToggle() { const currentdate = new Date();
this.setState((prevState, props) => ({ return (`${currentdate.getMonth() + 1} ${
previewEnabled: !prevState.previewEnabled, currentdate.getDate()}, ${
previewDate: this.getDate() currentdate.getFullYear()}, ${
})); currentdate.getHours()}:${
} currentdate.getMinutes()}:${
currentdate.getSeconds()}`);
getDate() { }
const currentdate = new Date();
return ((currentdate.getMonth() + 1) + " " render() {
+ currentdate.getDate() + ", " return (
+ currentdate.getFullYear() + ", " <div className="post" ref={this.newPostOuterRef}>
+ currentdate.getHours() + ":" <Divider horizontal>
+ currentdate.getMinutes() + ":" <span className="grey-text">
+ currentdate.getSeconds()); #
} {this.props.postIndex}
</span>
render() { </Divider>
return ( <Grid>
<div className="post" ref={this.newPostOuterRef}> <Grid.Row columns={16} stretched>
<Divider horizontal> <Grid.Column width={1} className="user-avatar">
<span className="grey-text">#{this.props.postIndex}</span> <UserAvatar
</Divider> size="52"
<Grid> className="inline user-avatar"
<Grid.Row columns={16} stretched> src={this.props.avatarUrl}
<Grid.Column width={1} className="user-avatar"> name={this.props.user.username}
<UserAvatar />
size="52" </Grid.Column>
className="inline user-avatar" <Grid.Column width={15}>
src={this.props.avatarUrl} <div className="">
name={this.props.user.username} <div className="stretch-space-between">
/> <span><strong>{this.props.user.username}</strong></span>
</Grid.Column> <span className="grey-text">
<Grid.Column width={15}> {this.state.previewEnabled
<div className=""> && <TimeAgo date={this.state.previewDate} />
<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> </span>
</div> </div>
<div className="stretch-space-between"> <div className="stretch-space-between">
<span><strong> <span>
{this.state.previewEnabled && <strong>
("Subject: " + this.state.postSubjectInput) {this.state.previewEnabled
&& (`Subject: ${
this.state.postSubjectInput}`)
} }
</strong></span> </strong>
</div> </span>
<div className="post-content"> </div>
<div style={{display: this.state.previewEnabled ? "block" : "none"}}> <div className="post-content">
<ReactMarkdown source={this.state.postContentInput} <div style={{
className="markdown-preview" /> display: this.state.previewEnabled
</div> ? 'block'
<Form className="topic-form"> : 'none'
<Form.Input key={"postSubjectInput"} }}
style={{display: this.state.previewEnabled ? "none" : ""}} >
name={"postSubjectInput"} <ReactMarkdown
error={this.state.postSubjectInputEmptySubmit} source={this.state.postContentInput}
type="text" className="markdown-preview"
value={this.state.postSubjectInput} />
placeholder="Subject" </div>
id="postSubjectInput" <Form className="topic-form">
onChange={this.handleInputChange} /> <Form.Input
<TextArea key={"postContentInput"} key="postSubjectInput"
style={{display: this.state.previewEnabled ? "none" : ""}} style={{
name={"postContentInput"} display: this.state.previewEnabled
className={this.state.postContentInputEmptySubmit ? "form-textarea-required" : ""} ? 'none'
value={this.state.postContentInput} : ''
placeholder="Post" }}
id="postContentInput" name="postSubjectInput"
onChange={this.handleInputChange} error={this.state.postSubjectInputEmptySubmit}
rows={4} autoHeight /> type="text"
<br/><br/> value={this.state.postSubjectInput}
<Button.Group> placeholder="Subject"
<Button key="submit" id="postSubjectInput"
type="button" onChange={this.handleInputChange}
onClick={this.validateAndPost} />
color='teal' <TextArea
animated> key="postContentInput"
<Button.Content visible>Post</Button.Content> style={{
<Button.Content hidden> display: this.state.previewEnabled
<Icon name='reply' /> ? 'none'
</Button.Content> : ''
</Button> }}
<Button type="button" name="postContentInput"
onClick={this.handlePreviewToggle} className={this.state.postContentInputEmptySubmit
color='yellow'> ? 'form-textarea-required'
{this.state.previewEnabled ? "Edit" : "Preview"} : ''}
</Button> value={this.state.postContentInput}
<Button type="button" placeholder="Post"
onClick={this.props.onCancelClick} id="postContentInput"
color='red'> onChange={this.handleInputChange}
Cancel rows={4}
</Button> autoHeight
</Button.Group> />
</Form> <br />
</div> <br />
</div> <Button.Group>
</Grid.Column> <Button
</Grid.Row> key="submit"
</Grid> type="button"
</div> onClick={this.validateAndPost}
); color="teal"
} animated
>
componentDidMount(){ <Button.Content visible>Post</Button.Content>
this.newPostOuterRef.current.scrollIntoView(true); <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 => { const mapStateToProps = state => ({
return { orbitDB: state.orbitDB,
orbitDB: state.orbitDB, user: state.user
user: state.user });
}
};
export default connect(mapStateToProps)(NewPost); export default connect(mapStateToProps)(NewPost);

109
app/src/components/NewTopicPreview.js

@ -1,65 +1,68 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Grid, Divider } from 'semantic-ui-react' import { Divider, Grid } from 'semantic-ui-react';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import UserAvatar from 'react-user-avatar'; import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
class Post extends Component { class Post extends Component {
constructor(props, context) { constructor(props, context) {
super(props); super(props);
} }
render(){ render() {
return ( return (
<div className="post"> <div className="post">
<Divider horizontal> <Divider horizontal>
<span className="grey-text">#0</span> <span className="grey-text">#0</span>
</Divider> </Divider>
<Grid> <Grid>
<Grid.Row columns={16} stretched> <Grid.Row columns={16} stretched>
<Grid.Column width={1} className="user-avatar"> <Grid.Column width={1} className="user-avatar">
<UserAvatar <UserAvatar
size="52" size="52"
className="inline" className="inline"
src={this.props.user.avatarUrl} src={this.props.user.avatarUrl}
name={this.props.user.username}/> name={this.props.user.username}
</Grid.Column> />
<Grid.Column width={15}> </Grid.Column>
<div className=""> <Grid.Column width={15}>
<div className="stretch-space-between"> <div className="">
<span> <div className="stretch-space-between">
<strong> <span>
{this.props.user.username} <strong>
</strong> {this.props.user.username}
</span> </strong>
<span className="grey-text"> </span>
<TimeAgo date={this.props.date}/> <span className="grey-text">
</span> <TimeAgo date={this.props.date} />
</div> </span>
<div className="stretch-space-between"> </div>
<span><strong> <div className="stretch-space-between">
Subject: {this.props.subject} <span>
</strong></span> <strong>
</div> Subject:
<div className="post-content"> {' '}
<ReactMarkdown source={this.props.content} /> {this.props.subject}
</div> </strong>
</div> </span>
</Grid.Column> </div>
</Grid.Row> <div className="post-content">
</Grid> <ReactMarkdown source={this.props.content} />
</div> </div>
); </div>
} </Grid.Column>
}; </Grid.Row>
</Grid>
</div>
);
}
}
const mapStateToProps = state => { const mapStateToProps = state => ({
return { user: state.user
user: state.user });
}
};
export default connect(mapStateToProps)(Post); export default connect(mapStateToProps)(Post);

17
app/src/components/NotFound.js

@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import pageNotFound from '../assets/images/PageNotFound.jpg'; import pageNotFound from '../assets/images/PageNotFound.jpg';
const NotFound = () => { const NotFound = () => (
return ( <div style={{
<div style={{textAlign: "center"}}> textAlign: 'center'
<img src={pageNotFound} alt="Page not found!"/> }}
</div> >
); <img src={pageNotFound} alt="Page not found!" />
}; </div>
);
export default NotFound; export default NotFound;

431
app/src/components/Post.js

@ -1,204 +1,289 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { push } from 'connected-react-router' import { push } from 'connected-react-router';
import { Link, withRouter } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ContentLoader from "react-content-loader" import ContentLoader from 'react-content-loader';
import { Transition } from 'semantic-ui-react' import { Button, Divider, Grid, Icon, Label, Transition } from 'semantic-ui-react';
import { Grid, Divider, Button, Icon, Label } from 'semantic-ui-react'
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import epochTimeConverter from '../helpers/EpochTimeConverter';
import UserAvatar from 'react-user-avatar'; import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import epochTimeConverter from '../helpers/EpochTimeConverter';
class Post extends Component { class Post extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.getBlockchainData = this.getBlockchainData.bind(this); this.getBlockchainData = this.getBlockchainData.bind(this);
this.fetchPost = this.fetchPost.bind(this); this.fetchPost = this.fetchPost.bind(this);
if (props.getFocus){ if (props.getFocus) {
this.postRef = React.createRef(); this.postRef = React.createRef();
}
this.state = {
fetchPostDataStatus: 'pending',
postContent: '',
postSubject: '',
readyForAnimation: false,
animateOnToggle: true
}
} }
getBlockchainData() { this.state = {
if (this.props.postData && fetchPostDataStatus: 'pending',
this.props.orbitDB.orbitdb && postContent: '',
this.state.fetchPostDataStatus === "pending") { postSubject: '',
this.setState({ fetchPostDataStatus: 'fetching' }); readyForAnimation: false,
this.fetchPost(this.props.postID); animateOnToggle: true
} };
}
getBlockchainData() {
if (this.props.postData
&& this.props.orbitDB.orbitdb
&& this.state.fetchPostDataStatus === 'pending') {
this.setState({
fetchPostDataStatus: 'fetching'
});
this.fetchPost(this.props.postID);
} }
}
async fetchPost(postID) { async fetchPost(postID) {
let orbitPostData; let orbitPostData;
if (this.props.postData.value[1] === this.props.user.address) { if (this.props.postData.value[1] === this.props.user.address) {
orbitPostData = this.props.orbitDB.postsDB.get(postID); orbitPostData = this.props.orbitDB.postsDB.get(postID);
} else { } else {
const fullAddress = "/orbitdb/" + this.props.postData.value[0] + "/posts"; const fullAddress = `/orbitdb/${this.props.postData.value[0]}/posts`;
const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress); const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress);
await store.load(); 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({ const localOrbitData = store.get(postID);
postContent: orbitPostData.content, if (localOrbitData) {
postSubject: orbitPostData.subject, orbitPostData = localOrbitData;
fetchPostDataStatus: 'fetched', } else {
readyForAnimation: true // Wait until we have received something from the network
store.events.on('replicated', () => {
orbitPostData = store.get(postID);
}); });
}
} }
render(){ this.setState({
let avatarView = (this.props.postData postContent: orbitPostData.content,
? <UserAvatar postSubject: orbitPostData.subject,
size="52" fetchPostDataStatus: 'fetched',
className="inline" readyForAnimation: true
src={this.props.avatarUrl} });
name={this.props.postData.value[2]}/> }
: <div className="user-avatar">
<ContentLoader height={52} width={52} speed={2} render() {
primaryColor="#b2e8e6" secondaryColor="#00b5ad"> const avatarView = (this.props.postData
<circle cx="26" cy="26" r="26" /> ? (
</ContentLoader> <UserAvatar
</div> size="52"
); className="inline"
src={this.props.avatarUrl}
return ( name={this.props.postData.value[2]}
<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> <div className="user-avatar">
</Divider> <ContentLoader
<Grid> height={52}
<Grid.Row columns={16} stretched> width={52}
<Grid.Column width={1} className="user-avatar"> speed={2}
{this.props.postData !== null primaryColor="#b2e8e6"
?<Link to={"/profile/" + this.props.postData.value[1] secondaryColor="#00b5ad"
+ "/" + this.props.postData.value[2]} >
onClick={(event) => {event.stopPropagation()}}> <circle cx="26" cy="26" r="26" />
{avatarView} </ContentLoader>
</Link> </div>
:avatarView )
} );
</Grid.Column>
<Grid.Column width={15}> return (
<div className=""> <Transition
<div className="stretch-space-between"> animation="tada"
<span className={this.props.postData !== null ? "" : "grey-text"}> duration={500}
<strong> visible={this.state.animateOnToggle}
{this.props.postData !== null >
?this.props.postData.value[2] <div className="post" ref={this.postRef ? this.postRef : null}>
:"Username" <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> </strong>
</span> </span>
<span className="grey-text"> <span className="grey-text">
{this.props.postData !== null && {this.props.postData !== null
<TimeAgo date={epochTimeConverter(this.props.postData.value[3])}/> && (
<TimeAgo date={epochTimeConverter(
this.props.postData.value[3],
)}
/>
)
} }
</span> </span>
</div> </div>
<div className="stretch-space-between"> <div className="stretch-space-between">
<span className={this.state.postSubject === '' ? "" : "grey-text"}> <span
<strong> className={this.state.postSubject
{this.state.postSubject === '' === '' ? '' : 'grey-text'}
? <ContentLoader height={5.8} width={300} speed={2} >
primaryColor="#b2e8e6" secondaryColor="#00b5ad" > <strong>
<rect x="0" y="0" rx="3" ry="3" width="75" height="5.5" /> {this.state.postSubject === ''
</ContentLoader> ? (
: 'Subject: ' + 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> </strong>
</span> </span>
</div> </div>
<div className="post-content"> <div className="post-content">
{this.state.postContent !== '' {this.state.postContent !== ''
? <ReactMarkdown source={this.state.postContent} /> ? <ReactMarkdown source={this.state.postContent} />
: <ContentLoader height={11.2} width={300} speed={2} : (
primaryColor="#b2e8e6" secondaryColor="#00b5ad" > <ContentLoader
<rect x="0" y="0" rx="3" ry="3" width="180" height="4.0" /> height={11.2}
<rect x="0" y="6.5" rx="3" ry="3" width="140" height="4.0" /> width={300}
</ContentLoader> speed={2}
} primaryColor="#b2e8e6"
</div> secondaryColor="#00b5ad"
</div> >
</Grid.Column> <rect
</Grid.Row> x="0"
<Grid.Row> y="0"
<Grid.Column floated="right" textAlign="right"> rx="3"
<Button icon size='mini' style={{marginRight: "0px"}}> ry="3"
<Icon name='chevron up' /> width="180"
</Button> height="4.0"
<Label color="teal">8000</Label> />
<Button icon size='mini'> <rect
<Icon name='chevron down' /> x="0"
</Button> y="6.5"
<Button icon size='mini' rx="3"
onClick={this.props.postData ry="3"
? () => { this.props.navigateTo("/topic/" width="140"
+ this.props.postData.value[4] + "/" height="4.0"
+ this.props.postID)} />
: () => {}}> </ContentLoader>
<Icon name='linkify' /> )
</Button> }
</Grid.Column> </div>
</Grid.Row>
</Grid>
</div> </div>
</Transition> </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>
);
}
componentDidMount() { componentDidMount() {
this.getBlockchainData(); this.getBlockchainData();
} }
componentDidUpdate(){ componentDidUpdate() {
this.getBlockchainData(); this.getBlockchainData();
if (this.state.readyForAnimation){ if (this.state.readyForAnimation) {
if (this.postRef){ if (this.postRef) {
setTimeout(() => { setTimeout(() => {
this.postRef.current.scrollIntoView({ block: 'start', behavior: 'smooth' }); this.postRef.current.scrollIntoView(
setTimeout(() => { {
this.setState({ animateOnToggle: false }); block: 'start', behavior: 'smooth'
}, 300); },
}, 100); );
this.setState({ readyForAnimation: false }); setTimeout(() => {
} this.setState({
} animateOnToggle: false
});
}, 300);
}, 100);
this.setState({
readyForAnimation: false
});
}
} }
}; }
}
const mapDispatchToProps = dispatch => bindActionCreators({ const mapDispatchToProps = dispatch => bindActionCreators({
navigateTo: (location) => push(location) navigateTo: location => push(location)
}, dispatch); }, dispatch);
const mapStateToProps = state => { const mapStateToProps = state => ({
return { user: state.user,
user: state.user, orbitDB: state.orbit
orbitDB: state.orbit });
}
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Post)); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Post));

120
app/src/components/PostList.js

@ -4,77 +4,79 @@ import { drizzle } from '../index';
import Post from './Post'; import Post from './Post';
const contract = "Forum"; const contract = 'Forum';
const getPostMethod = "getPost"; const getPostMethod = 'getPost';
class PostList extends Component { class PostList extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.getBlockchainData = this.getBlockchainData.bind(this); this.getBlockchainData = this.getBlockchainData.bind(this);
this.state = { this.state = {
dataKeys: [] dataKeys: []
} };
} }
getBlockchainData(){ render() {
if (this.props.drizzleStatus['initialized']){ const posts = this.props.postIDs.map((postID, index) => (
let dataKeysShallowCopy = this.state.dataKeys.slice(); <Post
let fetchingNewData = false; postData={(this.state.dataKeys[postID]
&& this.props.contracts[contract][getPostMethod][this.state.dataKeys[postID]])
? this.props.contracts[contract][getPostMethod][this.state.dataKeys[postID]]
: null}
avatarUrl=""
postIndex={index}
postID={postID}
getFocus={this.props.focusOnPost === postID}
key={postID}
/>
));
this.props.postIDs.forEach( postID => { return (
if (!this.state.dataKeys[postID]) { <div>
dataKeysShallowCopy[postID] = drizzle.contracts[contract].methods[getPostMethod].cacheCall(postID); {this.props.recentToTheTop
fetchingNewData = true; ? posts.slice(0).reverse()
} : posts
}) }
</div>
);
}
if (fetchingNewData){ componentDidMount() {
this.setState({ this.getBlockchainData();
dataKeys: dataKeysShallowCopy }
});
}
}
}
render() { componentDidUpdate() {
const posts = this.props.postIDs.map((postID, index) => { this.getBlockchainData();
return (<Post }
postData={(this.state.dataKeys[postID] && this.props.contracts[contract][getPostMethod][this.state.dataKeys[postID]])
? this.props.contracts[contract][getPostMethod][this.state.dataKeys[postID]]
: null}
avatarUrl={""}
postIndex={index}
postID={postID}
getFocus={this.props.focusOnPost === postID ? true : false}
key={postID} />)
});
return ( getBlockchainData() {
<div> if (this.props.drizzleStatus.initialized) {
{this.props.recentToTheTop const dataKeysShallowCopy = this.state.dataKeys.slice();
?posts.slice(0).reverse() let fetchingNewData = false;
:posts
}
</div>
);
}
componentDidMount() { this.props.postIDs.forEach((postID) => {
this.getBlockchainData(); if (!this.state.dataKeys[postID]) {
} dataKeysShallowCopy[postID] = drizzle.contracts[contract].methods[getPostMethod].cacheCall(
postID,
);
fetchingNewData = true;
}
});
componentDidUpdate(){ if (fetchingNewData) {
this.getBlockchainData(); this.setState({
dataKeys: dataKeysShallowCopy
});
}
} }
}; }
}
const mapStateToProps = state => { const mapStateToProps = state => ({
return { contracts: state.contracts,
contracts: state.contracts, drizzleStatus: state.drizzleStatus
drizzleStatus: state.drizzleStatus });
}
};
export default connect(mapStateToProps)(PostList); export default connect(mapStateToProps)(PostList);

224
app/src/components/ProfileInformation.js

@ -1,132 +1,144 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import UserAvatar from 'react-user-avatar';
import { drizzle } from '../index'; import { drizzle } from '../index';
import UserAvatar from 'react-user-avatar';
import epochTimeConverter from '../helpers/EpochTimeConverter'; import epochTimeConverter from '../helpers/EpochTimeConverter';
import UsernameFormContainer from '../containers/UsernameFormContainer'; import UsernameFormContainer from '../containers/UsernameFormContainer';
const callsInfo = [{ const callsInfo = [
contract: 'Forum', {
method: 'getUserDateOfRegister' contract: 'Forum',
},{ method: 'getUserDateOfRegister'
contract: 'Forum', }, {
method: 'getOrbitDBId' contract: 'Forum',
}] method: 'getOrbitDBId'
}];
class ProfileInformation extends Component { class ProfileInformation extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.getBlockchainData = this.getBlockchainData.bind(this); this.getBlockchainData = this.getBlockchainData.bind(this);
this.dataKey = []; this.dataKey = [];
this.state = { this.state = {
pageStatus: 'initialized', pageStatus: 'initialized',
dateOfRegister: '', dateOfRegister: '',
orbitDBId: '' orbitDBId: ''
}; };
}
getBlockchainData() {
if (this.state.pageStatus === 'initialized'
&& this.props.drizzleStatus.initialized) {
callsInfo.forEach((call, index) => {
this.dataKey[index] = drizzle.contracts[call.contract].methods[call.method].cacheCall(
this.props.address,
);
});
this.setState({
pageStatus: 'loading'
});
} }
getBlockchainData(){ if (this.state.pageStatus === 'loading') {
if (this.state.pageStatus === 'initialized' && let pageStatus = 'loaded';
this.props.drizzleStatus['initialized']) { callsInfo.forEach((call, index) => {
callsInfo.forEach((call, index) => { if (!this.props.contracts[call.contract][call.method][this.dataKey[index]]) {
this.dataKey[index] = drizzle.contracts[call.contract] pageStatus = 'loading';
.methods[call.method].cacheCall(this.props.address);
})
this.setState({ pageStatus: 'loading' });
} }
});
if (this.state.pageStatus === 'loading') { if (pageStatus === 'loaded') {
var pageStatus = 'loaded'; this.setState({
callsInfo.forEach((call, index) => { pageStatus
if (!this.props.contracts[call.contract][call.method][this.dataKey[index]]) { });
pageStatus = 'loading'; }
return; }
}
})
if (pageStatus === 'loaded') { if (this.state.pageStatus === 'loaded') {
this.setState({ pageStatus: pageStatus }); if (this.state.dateOfRegister === '') {
} const transaction = this.props.contracts[callsInfo[0].contract][callsInfo[0].method][this.dataKey[0]];
if (transaction) {
this.setState({
dateOfRegister: transaction.value
});
} }
}
if (this.state.pageStatus === 'loaded'){ if (this.state.orbitDBId === '') {
if (this.state.dateOfRegister === ''){ const transaction = this.props.contracts[callsInfo[1].contract][callsInfo[1].method][this.dataKey[1]];
let transaction = this.props.contracts[callsInfo[0].contract][callsInfo[0].method][this.dataKey[0]]; if (transaction) {
if (transaction){ this.setState({
this.setState({ dateOfRegister: transaction.value }); orbitDBId: transaction.value
} });
}
if (this.state.orbitDBId === ''){
let transaction = this.props.contracts[callsInfo[1].contract][callsInfo[1].method][this.dataKey[1]];
if (transaction){
this.setState({ orbitDBId: transaction.value });
}
}
} }
}
} }
}
render() { render() {
return ( return (
<div className="user-info"> <div className="user-info">
{this.props.avatarUrl && <UserAvatar {this.props.avatarUrl && (
size="40" <UserAvatar
className="inline user-avatar" size="40"
src={this.props.avatarUrl} className="inline user-avatar"
name={this.props.username}/>} src={this.props.avatarUrl}
<table className="highlight centered responsive-table"> name={this.props.username}
<tbody> />
<tr> )}
<td><strong>Username:</strong></td> <table className="highlight centered responsive-table">
<td>{this.props.username}</td> <tbody>
</tr> <tr>
<tr> <td><strong>Username:</strong></td>
<td><strong>Account address:</strong></td> <td>{this.props.username}</td>
<td>{this.props.address}</td> </tr>
</tr> <tr>
<tr> <td><strong>Account address:</strong></td>
<td><strong>OrbitDB:</strong></td> <td>{this.props.address}</td>
<td>{this.state.orbitDBId}</td> </tr>
</tr> <tr>
<tr> <td><strong>OrbitDB:</strong></td>
<td><strong>Number of topics created:</strong></td> <td>{this.state.orbitDBId}</td>
<td>{this.props.numberOfTopics}</td> </tr>
</tr> <tr>
<tr> <td><strong>Number of topics created:</strong></td>
<td><strong>Number of posts:</strong></td> <td>{this.props.numberOfTopics}</td>
<td>{this.props.numberOfPosts}</td> </tr>
</tr> <tr>
{this.state.dateOfRegister && <td><strong>Number of posts:</strong></td>
<tr> <td>{this.props.numberOfPosts}</td>
<td><strong>Member since:</strong></td> </tr>
<td>{epochTimeConverter(this.state.dateOfRegister)}</td> {this.state.dateOfRegister
</tr> && (
} <tr>
</tbody> <td><strong>Member since:</strong></td>
</table> <td>{epochTimeConverter(this.state.dateOfRegister)}</td>
{this.props.self && <UsernameFormContainer/>} </tr>
</div> )
); }
} </tbody>
</table>
{this.props.self && <UsernameFormContainer />}
</div>
);
}
componentDidMount() { componentDidMount() {
this.getBlockchainData(); this.getBlockchainData();
} }
componentDidUpdate(){ componentDidUpdate() {
this.getBlockchainData(); this.getBlockchainData();
} }
}; }
const mapStateToProps = state => { const mapStateToProps = state => ({
return { drizzleStatus: state.drizzleStatus,
drizzleStatus: state.drizzleStatus, contracts: state.contracts,
contracts: state.contracts, user: state.user
user: state.user });
}
};
export default connect(mapStateToProps)(ProfileInformation); export default connect(mapStateToProps)(ProfileInformation);

213
app/src/components/Topic.js

@ -1,118 +1,139 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router' import { withRouter } from 'react-router-dom';
import ContentLoader from "react-content-loader" import ContentLoader from 'react-content-loader';
import { Card } from 'semantic-ui-react' import { Card } from 'semantic-ui-react';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import epochTimeConverter from '../helpers/EpochTimeConverter' import epochTimeConverter from '../helpers/EpochTimeConverter';
class Topic extends Component { class Topic extends Component {
constructor(props){ constructor(props) {
super(props); super(props);
this.fetchSubject = this.fetchSubject.bind(this); this.fetchSubject = this.fetchSubject.bind(this);
this.state = { this.state = {
topicSubject: null, topicSubject: null,
topicSubjectFetchStatus: 'pending' topicSubjectFetchStatus: 'pending'
} };
} }
async fetchSubject(topicID) {
var topicSubject;
if (this.props.topicData.value[1] === this.props.user.address) { async fetchSubject(topicID) {
let orbitData = this.props.orbitDB.topicsDB.get(topicID); let topicSubject;
topicSubject = orbitData['subject']
} 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 (this.props.topicData.value[1] === this.props.user.address) {
if (localOrbitData) { const orbitData = this.props.orbitDB.topicsDB.get(topicID);
topicSubject = localOrbitData['subject']; topicSubject = orbitData.subject;
} else { } else {
// Wait until we have received something from the network const fullAddress = `/orbitdb/${this.props.topicData.value[0]
store.events.on('replicated', () => { }/topics`;
topicSubject = store.get(topicID)['subject']; const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress);
}) await store.load();
}
}
this.setState({ const localOrbitData = store.get(topicID);
topicSubject: topicSubject, if (localOrbitData) {
topicSubjectFetchStatus: 'fetched' topicSubject = localOrbitData.subject;
}) } else {
// Wait until we have received something from the network
store.events.on('replicated', () => {
topicSubject = store.get(topicID).subject;
});
}
} }
render(){ this.setState({
return ( topicSubject,
<Card link className="card" topicSubjectFetchStatus: 'fetched'
onClick={() => {this.props.history.push("/topic/" + this.props.topicID)}}> });
<Card.Content> }
<div className={"topic-subject" + (this.state.topicSubject ? "" : " grey-text")}>
<p><strong>
{this.state.topicSubject !== null ? this.state.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>
);
}
componentDidMount() { render() {
if (this.props.topicData !== null && return (
this.state.topicSubjectFetchStatus === "pending" && <Card
this.props.orbitDB.ipfsInitialized && link
this.props.orbitDB.orbitdb) { className="card"
this.fetchSubject(this.props.topicID); onClick={() => {
} this.props.history.push(`/topic/${this.props.topicID}`);
} }}
>
<Card.Content>
<div className={`topic-subject${
this.state.topicSubject ? '' : ' grey-text'}`}
>
<p>
<strong>
{this.state.topicSubject !== null ? this.state.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() { componentDidMount() {
if (this.props.topicData !== null && if (this.props.topicData !== null
this.state.topicSubjectFetchStatus === "pending" && && this.state.topicSubjectFetchStatus === 'pending'
this.props.orbitDB.ipfsInitialized && && this.props.orbitDB.ipfsInitialized
this.props.orbitDB.orbitdb) { && this.props.orbitDB.orbitdb) {
this.fetchSubject(this.props.topicID); this.fetchSubject(this.props.topicID);
}
} }
}; }
const mapStateToProps = state => { componentDidUpdate() {
return { if (this.props.topicData !== null
user: state.user, && this.state.topicSubjectFetchStatus === 'pending'
orbitDB: state.orbit && this.props.orbitDB.ipfsInitialized
&& this.props.orbitDB.orbitdb) {
this.fetchSubject(this.props.topicID);
} }
}
} }
const mapStateToProps = state => ({
user: state.user,
orbitDB: state.orbit
});
export default withRouter(connect(mapStateToProps)(Topic)); export default withRouter(connect(mapStateToProps)(Topic));

108
app/src/components/TopicList.js

@ -4,71 +4,73 @@ import { drizzle } from '../index';
import Topic from './Topic'; import Topic from './Topic';
const contract = "Forum"; const contract = 'Forum';
const getTopicMethod = "getTopic"; const getTopicMethod = 'getTopic';
class TopicList extends Component { class TopicList extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.getBlockchainData = this.getBlockchainData.bind(this); this.getBlockchainData = this.getBlockchainData.bind(this);
this.state = { this.state = {
dataKeys: [] dataKeys: []
} };
} }
getBlockchainData(){
if (this.props.drizzleStatus['initialized']){
let dataKeysShallowCopy = this.state.dataKeys.slice();
let fetchingNewData = false;
this.props.topicIDs.forEach( topicID => { getBlockchainData() {
if (!this.state.dataKeys[topicID]) { if (this.props.drizzleStatus.initialized) {
dataKeysShallowCopy[topicID] = drizzle.contracts[contract].methods[getTopicMethod].cacheCall(topicID); const dataKeysShallowCopy = this.state.dataKeys.slice();
fetchingNewData = true; let fetchingNewData = false;
}
})
if (fetchingNewData){ this.props.topicIDs.forEach((topicID) => {
this.setState({ if (!this.state.dataKeys[topicID]) {
dataKeys: dataKeysShallowCopy dataKeysShallowCopy[topicID] = drizzle.contracts[contract].methods[getTopicMethod].cacheCall(
}); topicID,
} );
fetchingNewData = true;
} }
} });
render() { if (fetchingNewData) {
const topics = this.props.topicIDs.map( topicID => { this.setState({
return (<Topic dataKeys: dataKeysShallowCopy
topicData={(this.state.dataKeys[topicID] && this.props.contracts[contract][getTopicMethod][this.state.dataKeys[topicID]])
? this.props.contracts[contract][getTopicMethod][this.state.dataKeys[topicID]]
: null}
topicID={topicID}
key={topicID} />)
}); });
}
return (
<div className="topics-list">
{topics.slice(0).reverse()}
</div>
);
} }
}
componentDidMount() { render() {
this.getBlockchainData(); const topics = this.props.topicIDs.map(topicID => (
} <Topic
topicData={(this.state.dataKeys[topicID]
&& this.props.contracts[contract][getTopicMethod][this.state.dataKeys[topicID]])
? this.props.contracts[contract][getTopicMethod][this.state.dataKeys[topicID]]
: null}
topicID={topicID}
key={topicID}
/>
));
componentDidUpdate(){ return (
this.getBlockchainData(); <div className="topics-list">
} {topics.slice(0).reverse()}
}; </div>
);
}
const mapStateToProps = state => { componentDidMount() {
return { this.getBlockchainData();
contracts: state.contracts, }
drizzleStatus: state.drizzleStatus
} componentDidUpdate() {
}; this.getBlockchainData();
}
}
const mapStateToProps = state => ({
contracts: state.contracts,
drizzleStatus: state.drizzleStatus
});
export default connect(mapStateToProps)(TopicList); export default connect(mapStateToProps)(TopicList);

32
app/src/config/drizzleOptions.js

@ -1,20 +1,20 @@
import Forum from "../contracts/Forum.json"; import Forum from '../contracts/Forum.json';
const drizzleOptions = { const drizzleOptions = {
web3: { web3: {
fallback: { fallback: {
type: 'ws', type: 'ws',
url: 'ws://127.0.0.1:9545' url: 'ws://127.0.0.1:9545'
} }
}, },
contracts: [Forum], contracts: [Forum],
events: { events: {
Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated'] Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated']
}, },
polls: { polls: {
accounts: 2000, accounts: 2000,
blocks: 2000 blocks: 2000
}, }
}; };
export default drizzleOptions; export default drizzleOptions;

24
app/src/config/ipfsOptions.js

@ -1,17 +1,19 @@
// OrbitDB uses Pubsub which is an experimental feature // OrbitDB uses Pubsub which is an experimental feature
// and need to be turned on manually. // and need to be turned on manually.
const ipfsOptions = { const ipfsOptions = {
EXPERIMENTAL: { EXPERIMENTAL: {
pubsub: true pubsub: true
}, config: { },
Addresses: { config: {
Swarm: [ Addresses: {
'/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star', Swarm: [
// Use local signal server (https://github.com/libp2p/js-libp2p-websocket-star-rendezvous) '/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star',
'/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star' // Use local signal server (https://github.com/libp2p/js-libp2p-websocket-star-rendezvous)
] // (e.g. rendezvous --port=9090 --host=127.0.0.1)
} '/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star'
]
} }
}
}; };
export default ipfsOptions; export default ipfsOptions;

193
app/src/containers/BoardContainer.js

@ -1,117 +1,124 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { drizzle } from '../index'; import { withRouter } from 'react-router-dom';
import { withRouter } from 'react-router-dom'
import { Header } from 'semantic-ui-react'; import { Header } from 'semantic-ui-react';
import { drizzle } from '../index';
import TopicList from '../components/TopicList'; import TopicList from '../components/TopicList';
import FloatingButton from '../components/FloatingButton'; import FloatingButton from '../components/FloatingButton';
/*import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions';*/ /* import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions'; */
const contract = "Forum"; const contract = 'Forum';
const getNumberOfTopicsMethod = "getNumberOfTopics"; const getNumberOfTopicsMethod = 'getNumberOfTopics';
class BoardContainer extends Component { class BoardContainer extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
/*this.props.store.dispatch(showProgressBar());*/ /* this.props.store.dispatch(showProgressBar()); */
this.getBlockchainData = this.getBlockchainData.bind(this); this.getBlockchainData = this.getBlockchainData.bind(this);
this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this); this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this);
this.state = { this.state = {
pageStatus: 'initialized' pageStatus: 'initialized'
} };
}
getBlockchainData() {
if (this.state.pageStatus === 'initialized'
&& this.props.drizzleStatus.initialized) {
this.dataKey = drizzle.contracts[contract].methods[getNumberOfTopicsMethod].cacheCall();
this.setState({
pageStatus: 'loading'
});
} }
if (this.state.pageStatus === 'loading'
getBlockchainData() { && this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey]) {
if (this.state.pageStatus === 'initialized' && this.setState({
this.props.drizzleStatus['initialized']){ pageStatus: 'loaded'
this.dataKey = drizzle.contracts[contract].methods[getNumberOfTopicsMethod].cacheCall(); });
this.setState({ pageStatus: 'loading' }); /* this.props.store.dispatch(hideProgressBar()); */
}
if (this.state.pageStatus === 'loading' &&
this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey]){
this.setState({ pageStatus: 'loaded' });
/*this.props.store.dispatch(hideProgressBar());*/
}
} }
}
handleCreateTopicClick() { handleCreateTopicClick() {
this.props.history.push("/startTopic"); this.props.history.push('/startTopic');
} }
render() { render() {
var boardContents; let boardContents;
if (this.state.pageStatus === 'loaded'){ if (this.state.pageStatus === 'loaded') {
var numberOfTopics = this.props.contracts[contract][getNumberOfTopicsMethod][this.dataKey].value; const 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 ( if (numberOfTopics !== '0') {
<div className="fill"> this.topicIDs = [];
{boardContents} for (let i = 0; i < numberOfTopics; i++) {
</div> this.topicIDs.push(i);
}
boardContents = ([
<TopicList topicIDs={this.topicIDs} key="topicList" />,
<div className="bottom-overlay-pad" key="pad" />,
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>
); );
}
} }
componentDidMount() { return (
this.getBlockchainData(); <div className="fill">
} {boardContents}
</div>
);
}
componentDidUpdate(){ componentDidMount() {
this.getBlockchainData(); this.getBlockchainData();
} }
componentDidUpdate() {
this.getBlockchainData();
}
} }
const mapStateToProps = state => { const mapStateToProps = state => ({
return { contracts: state.contracts,
contracts: state.contracts, drizzleStatus: state.drizzleStatus,
drizzleStatus: state.drizzleStatus, hasSignedUp: state.user.hasSignedUp
hasSignedUp: state.user.hasSignedUp });
}
};
export default withRouter(connect(mapStateToProps)(BoardContainer)); export default withRouter(connect(mapStateToProps)(BoardContainer));

47
app/src/containers/CoreLayoutContainer.js

@ -2,10 +2,8 @@ import React, { Component } from 'react';
import NavBarContainer from './NavBarContainer'; import NavBarContainer from './NavBarContainer';
import RightSideBarContainer from './TransactionsMonitorContainer'; import RightSideBarContainer from './TransactionsMonitorContainer';
/*import TransactionsMonitorContainer from '../../containers/TransactionsMonitorContainer';*/
// Styles // Styles
import '../assets/fonts/fontawesome-free-5.7.2/all.js'; //TODO: check https://fontawesome.com/how-to-use/on-the-web/setup/using-package-managers import '../assets/fonts/fontawesome-free-5.7.2/all.js'; // TODO: check https://fontawesome.com/how-to-use/on-the-web/setup/using-package-managers
import '../assets/css/App.css'; import '../assets/css/App.css';
import '../assets/css/sign-up-container.css'; import '../assets/css/sign-up-container.css';
@ -14,32 +12,33 @@ import '../assets/css/start-topic-container.css';
import '../assets/css/topic-container.css'; import '../assets/css/topic-container.css';
import '../assets/css/profile-container.css'; import '../assets/css/profile-container.css';
/* import TransactionsMonitorContainer from '../../containers/TransactionsMonitorContainer'; */
class CoreLayout extends Component { class CoreLayout extends Component {
render() { render() {
return ( return (
<div className="App"> <div className="App">
<NavBarContainer/> <NavBarContainer />
{/*<div className="progress-bar-container" {/* <div className="progress-bar-container"
style={{display: this.props.isProgressBarVisible ? "block" : "none"}}> style={{display: this.props.isProgressBarVisible ? "block" : "none"}}>
<div className="progress"> <div className="progress">
<div className="indeterminate"></div> <div className="indeterminate"></div>
</div> </div>
</div>*/} </div> */}
<div className="page-container"> <div className="page-container">
<aside className="left-side-panel"> <aside className="left-side-panel" />
</aside> <div className="main-panel">
<div className="main-panel"> <div className="view-container">
<div className="view-container"> {this.props.children}
{this.props.children}
</div>
</div>
<aside className="right-side-panel">
<RightSideBarContainer />
</aside>
</div>
</div> </div>
); </div>
} <aside className="right-side-panel">
<RightSideBarContainer />
</aside>
</div>
</div>
);
}
} }
export default CoreLayout export default CoreLayout;

28
app/src/containers/HomeContainer.js

@ -4,22 +4,22 @@ import BoardContainer from './BoardContainer';
class HomeContainer extends Component { class HomeContainer extends Component {
render() { render() {
//We can add a modal to tell the user to sign up // We can add a modal to tell the user to sign up
/*var modal = this.props.user.hasSignedUp && ( /* var modal = this.props.user.hasSignedUp && (
<Modal dimmer='blurring' open={this.state.open}> <Modal dimmer='blurring' open={this.state.open}>
<Modal.Header>Select a Photo</Modal.Header> <Modal.Header>Select a Photo</Modal.Header>
<Modal.Content image> <Modal.Content image>
<Image wrapped size='medium' src='/assets/images/avatar/large/rachel.png' /> <Image wrapped size='medium' src='/assets/images/avatar/large/rachel.png' />
<Modal.Description> <Modal.Description>
<Header>Default Profile Image</Header> <Header>Default Profile Image</Header>
<p>We've found the following gravatar image associated with your e-mail address.</p> <p>We've found the following gravatar image associated with your e-mail address.</p>
<p>Is it okay to use this photo?</p> <p>Is it okay to use this photo?</p>
</Modal.Description> </Modal.Description>
</Modal.Content> </Modal.Content>
</Modal>);*/ </Modal>); */
return (<BoardContainer/>); return (<BoardContainer />);
} }
} }

88
app/src/containers/NavBarContainer.js

@ -1,53 +1,63 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { push } from 'connected-react-router' import { push } from 'connected-react-router';
import { Image, Menu } from 'semantic-ui-react' import { Image, Menu } from 'semantic-ui-react';
import logo from '../assets/images/logo.png'; import logo from '../assets/images/logo.png';
class NavBarContainer extends Component { class NavBarContainer extends Component {
render() { render() {
return ( return (
<Menu fixed='top' inverted> <Menu fixed="top" inverted>
<Menu.Item header onClick={() => {this.props.navigateTo('/')}}> <Menu.Item header onClick={() => { this.props.navigateTo('/'); }}>
<Image <Image
size='mini' size="mini"
src={logo} src={logo}
style={{ marginRight: '1.5em' }} style={{
/> marginRight: '1.5em'
Apella }}
</Menu.Item> />
<Menu.Item onClick={() => {this.props.navigateTo('/home')}}> Apella
Home </Menu.Item>
</Menu.Item> <Menu.Item onClick={() => { this.props.navigateTo('/home'); }}>
{this.props.hasSignedUp Home
?<Menu.Item onClick={() => {this.props.navigateTo('/profile')}}> </Menu.Item>
Profile {this.props.hasSignedUp
</Menu.Item> ? (
:<Menu.Menu position='right' style={{backgroundColor: '#00b5ad'}}> <Menu.Item onClick={() => { this.props.navigateTo('/profile'); }}>
<Menu.Item onClick={() => {this.props.navigateTo('/signup')}}> Profile
SignUp </Menu.Item>
</Menu.Item> )
</Menu.Menu> : (
} <Menu.Menu
<div className="navBarText"> position="right"
{this.props.navBarTitle !== '' && <span>{this.props.navBarTitle}</span>} style={{
</div> backgroundColor: '#00b5ad'
</Menu> }}
); >
} <Menu.Item onClick={() => { this.props.navigateTo('/signup'); }}>
SignUp
</Menu.Item>
</Menu.Menu>
)
}
<div className="navBarText">
{this.props.navBarTitle !== ''
&& <span>{this.props.navBarTitle}</span>}
</div>
</Menu>
);
}
} }
const mapDispatchToProps = dispatch => bindActionCreators({ const mapDispatchToProps = dispatch => bindActionCreators({
navigateTo: (location) => push(location) navigateTo: location => push(location)
}, dispatch); }, dispatch);
const mapStateToProps = state => { const mapStateToProps = state => ({
return { hasSignedUp: state.user.hasSignedUp,
hasSignedUp: state.user.hasSignedUp, navBarTitle: state.interface.navBarTitle
navBarTitle: state.interface.navBarTitle });
}
};
export default connect(mapStateToProps, mapDispatchToProps)(NavBarContainer); export default connect(mapStateToProps, mapDispatchToProps)(NavBarContainer);

323
app/src/containers/ProfileContainer.js

@ -1,10 +1,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { push } from 'connected-react-router' import { push } from 'connected-react-router';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Tab } from 'semantic-ui-react';
import { drizzle } from '../index'; import { drizzle } from '../index';
import { Tab } from 'semantic-ui-react'
import ProfileInformation from '../components/ProfileInformation'; import ProfileInformation from '../components/ProfileInformation';
import TopicList from '../components/TopicList'; import TopicList from '../components/TopicList';
@ -12,177 +12,192 @@ import PostList from '../components/PostList';
import LoadingSpinner from '../components/LoadingSpinner'; import LoadingSpinner from '../components/LoadingSpinner';
import { setNavBarTitle } from '../redux/actions/userInterfaceActions'; import { setNavBarTitle } from '../redux/actions/userInterfaceActions';
const callsInfo = [{ const callsInfo = [
contract: 'Forum', {
method: 'getUsername' contract: 'Forum',
},{ method: 'getUsername'
contract: 'Forum', }, {
method: 'getUserTopics' contract: 'Forum',
},{ method: 'getUserTopics'
contract: 'Forum', }, {
method: 'getUserPosts' contract: 'Forum',
} method: 'getUserPosts'
}
]; ];
class ProfileContainer extends Component { class ProfileContainer extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.getBlockchainData = this.getBlockchainData.bind(this); this.getBlockchainData = this.getBlockchainData.bind(this);
this.dataKey = []; this.dataKey = [];
var address = this.props.match.params.address const address = this.props.match.params.address
? this.props.match.params.address ? this.props.match.params.address
: this.props.user.address; : this.props.user.address;
this.state = { this.state = {
pageStatus: 'initialized', pageStatus: 'initialized',
userAddress: address, userAddress: address,
username: '', username: '',
topicIDs: null, topicIDs: null,
postIDs: null postIDs: null
}; };
}
getBlockchainData() {
if (this.state.pageStatus === 'initialized'
&& this.props.drizzleStatus.initialized) {
callsInfo.forEach((call, index) => {
this.dataKey[index] = drizzle.contracts[call.contract].methods[call.method].cacheCall(
this.state.userAddress,
);
});
this.setState({
pageStatus: 'loading'
});
} }
getBlockchainData() { if (this.state.pageStatus === 'loading') {
if (this.state.pageStatus === 'initialized' && let pageStatus = 'loaded';
this.props.drizzleStatus['initialized']) { callsInfo.forEach((call, index) => {
callsInfo.forEach((call, index) => { if (!this.props.contracts[call.contract][call.method][this.dataKey[index]]) {
this.dataKey[index] = drizzle.contracts[call.contract] pageStatus = 'loading';
.methods[call.method].cacheCall(this.state.userAddress);
})
this.setState({ pageStatus: 'loading' });
}
if (this.state.pageStatus === 'loading') {
var pageStatus = 'loaded';
callsInfo.forEach((call, index) => {
if (!this.props.contracts[call.contract][call.method][this.dataKey[index]]) {
pageStatus = 'loading';
return;
}
})
if (pageStatus === 'loaded') {
this.setState({ pageStatus: pageStatus });
}
} }
});
if (this.state.pageStatus === 'loaded'){ if (pageStatus === 'loaded') {
if (this.state.username === ''){ this.setState({
let transaction = this.props.contracts[callsInfo[0].contract][callsInfo[0].method][this.dataKey[0]]; pageStatus
if (transaction){ });
var username = transaction.value; }
this.props.setNavBarTitle(username);
this.setState({ username: username });
}
}
if (this.state.topicIDs === null){
let transaction = this.props.contracts[callsInfo[1].contract][callsInfo[1].method][this.dataKey[1]];
if (transaction){
this.setState({ topicIDs: transaction.value });
}
}
if (this.state.postIDs === null){
let transaction = this.props.contracts[callsInfo[2].contract][callsInfo[2].method][this.dataKey[2]];
if (transaction){
this.setState({ postIDs: transaction.value });
}
}
/*this.props.store.dispatch(hideProgressBar());*/
}
} }
render() { if (this.state.pageStatus === 'loaded') {
if (!this.props.user.hasSignedUp) { if (this.state.username === '') {
this.props.navigateTo("/signup"); const transaction = this.props.contracts[callsInfo[0].contract][callsInfo[0].method][this.dataKey[0]];
return(null); if (transaction) {
const username = transaction.value;
this.props.setNavBarTitle(username);
this.setState({
username
});
} }
}
if (this.state.topicIDs === null) {
const transaction = this.props.contracts[callsInfo[1].contract][callsInfo[1].method][this.dataKey[1]];
if (transaction) {
this.setState({
topicIDs: transaction.value
});
}
}
if (this.state.postIDs === null) {
const transaction = this.props.contracts[callsInfo[2].contract][callsInfo[2].method][this.dataKey[2]];
if (transaction) {
this.setState({
postIDs: transaction.value
});
}
}
var infoTab = /* this.props.store.dispatch(hideProgressBar()); */
(<ProfileInformation
address={this.state.userAddress}
username={this.state.username}
numberOfTopics={this.state.topicIDs && this.state.topicIDs.length}
numberOfPosts={this.state.postIDs && this.state.postIDs.length}
self={this.state.userAddress === this.props.user.address}
key="profileInfo"
/>);
var topicsTab =
(<div className="profile-tab">
{this.state.topicIDs
? <TopicList topicIDs={this.state.topicIDs} />
: <LoadingSpinner />
}
</div>);
var postsTab =
(<div className="profile-tab">
{this.state.postIDs
? <PostList postIDs={this.state.postIDs} recentToTheTop />
: <LoadingSpinner />
}
</div>);
const profilePanes = [
{
menuItem: 'INFORMATION',
pane: {
key: 'INFORMATION',
content: (infoTab),
},
},
{
menuItem: 'TOPICS',
pane: {
key: 'TOPICS',
content: (topicsTab),
},
},
{
menuItem: 'POSTS',
pane: {
key: 'POSTS',
content: (postsTab),
},
},
]
return (
<div>
<Tab
menu={{ secondary: true, pointing: true }}
panes={profilePanes}
renderActiveOnly={false} />
</div>
);
}
componentDidMount() {
this.getBlockchainData();
} }
}
componentDidUpdate(){ render() {
this.getBlockchainData(); if (!this.props.user.hasSignedUp) {
this.props.navigateTo('/signup');
return (null);
} }
componentWillUnmount() { const infoTab = (
this.props.setNavBarTitle(''); <ProfileInformation
} address={this.state.userAddress}
username={this.state.username}
numberOfTopics={this.state.topicIDs && this.state.topicIDs.length}
numberOfPosts={this.state.postIDs && this.state.postIDs.length}
self={this.state.userAddress === this.props.user.address}
key="profileInfo"
/>
);
const topicsTab = (
<div className="profile-tab">
{this.state.topicIDs
? <TopicList topicIDs={this.state.topicIDs} />
: <LoadingSpinner />
}
</div>
);
const postsTab = (
<div className="profile-tab">
{this.state.postIDs
? <PostList postIDs={this.state.postIDs} recentToTheTop />
: <LoadingSpinner />
}
</div>
);
const profilePanes = [
{
menuItem: 'INFORMATION',
pane: {
key: 'INFORMATION',
content: (infoTab)
}
},
{
menuItem: 'TOPICS',
pane: {
key: 'TOPICS',
content: (topicsTab)
}
},
{
menuItem: 'POSTS',
pane: {
key: 'POSTS',
content: (postsTab)
}
}
];
return (
<div>
<Tab
menu={{
secondary: true, pointing: true
}}
panes={profilePanes}
renderActiveOnly={false}
/>
</div>
);
}
componentDidMount() {
this.getBlockchainData();
}
componentDidUpdate() {
this.getBlockchainData();
}
componentWillUnmount() {
this.props.setNavBarTitle('');
}
} }
const mapDispatchToProps = dispatch => bindActionCreators({ const mapDispatchToProps = dispatch => bindActionCreators({
navigateTo: (location) => push(location), navigateTo: location => push(location),
setNavBarTitle: (navBarTitle) => setNavBarTitle(navBarTitle) setNavBarTitle: navBarTitle => setNavBarTitle(navBarTitle)
}, dispatch); }, dispatch);
const mapStateToProps = state => { const mapStateToProps = state => ({
return { user: state.user,
user: state.user, drizzleStatus: state.drizzleStatus,
drizzleStatus: state.drizzleStatus, contracts: state.contracts,
contracts: state.contracts, orbitDB: state.orbitDB
orbitDB: state.orbitDB });
}
};
export default connect(mapStateToProps, mapDispatchToProps)(ProfileContainer); export default connect(mapStateToProps, mapDispatchToProps)(ProfileContainer);

67
app/src/containers/SignUpContainer.js

@ -1,43 +1,46 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Header } from 'semantic-ui-react'; import { Header } from 'semantic-ui-react';
import {connect} from "react-redux"; import { connect } from 'react-redux';
import UsernameFormContainer from './UsernameFormContainer'; import UsernameFormContainer from './UsernameFormContainer';
class SignUpContainer extends Component { class SignUpContainer extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.user.hasSignedUp && !prevProps.user.hasSignedUp) if (this.props.user.hasSignedUp && !prevProps.user.hasSignedUp) this.props.history.push('/');
this.props.history.push("/"); }
}
render() { render() {
return ( return (
this.props.user.hasSignedUp this.props.user.hasSignedUp
?(<div className="vertical-center-in-parent"> ? (
<Header color='teal' textAlign='center' as='h2'> <div className="vertical-center-in-parent">
There is already an account for this addresss. <Header color="teal" textAlign="center" as="h2">
</Header> There is already an account for this addresss.
<Header color='teal' textAlign='center' as='h4'> </Header>
If you want to create another account please change your address. <Header color="teal" textAlign="center" as="h4">
</Header> If you want to create another account please change your address.
</div>) </Header>
:(<div className="sign-up-container"> </div>
<div> )
<h1>Sign Up</h1> : (
<p className="no-margin"> <div className="sign-up-container">
<strong>Account address:</strong> {this.props.user.address} <div>
</p> <h1>Sign Up</h1>
<UsernameFormContainer /> <p className="no-margin">
</div> <strong>Account address:</strong>
</div>) {' '}
); {this.props.user.address}
} </p>
<UsernameFormContainer />
</div>
</div>
)
);
}
} }
const mapStateToProps = state => { const mapStateToProps = state => ({
return { user: state.user
user: state.user });
}
};
export default connect(mapStateToProps)(SignUpContainer); export default connect(mapStateToProps)(SignUpContainer);

241
app/src/containers/StartTopicContainer.js

@ -1,132 +1,151 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, TextArea, Button, Icon } from 'semantic-ui-react' import { Button, Form, Icon, TextArea } from 'semantic-ui-react';
import NewTopicPreview from '../components/NewTopicPreview' import NewTopicPreview from '../components/NewTopicPreview';
import { createTopic } from '../redux/actions/transactionsActions'; import { createTopic } from '../redux/actions/transactionsActions';
class StartTopicContainer extends Component { class StartTopicContainer extends Component {
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
this.handlePreviewToggle = this.handlePreviewToggle.bind(this); this.handlePreviewToggle = this.handlePreviewToggle.bind(this);
this.validateAndPost = this.validateAndPost.bind(this); this.validateAndPost = this.validateAndPost.bind(this);
this.state = { this.state = {
topicSubjectInput: '', topicSubjectInput: '',
topicMessageInput: '', topicMessageInput: '',
topicSubjectInputEmptySubmit: false, topicSubjectInputEmptySubmit: false,
topicMessageInputEmptySubmit: false, topicMessageInputEmptySubmit: false,
previewEnabled: false, previewEnabled: false,
previewDate: "" previewDate: ''
}; };
} }
async validateAndPost() {
if (this.state.topicSubjectInput === '' || this.state.topicMessageInput === ''){
this.setState({
topicSubjectInputEmptySubmit: this.state.topicSubjectInput === '',
topicMessageInputEmptySubmit: this.state.topicMessageInput === ''
});
return;
}
this.props.dispatch( async validateAndPost() {
createTopic( if (this.state.topicSubjectInput === '' || this.state.topicMessageInput
{ === '') {
topicSubject: this.state.topicSubjectInput, this.setState({
topicMessage: this.state.topicMessageInput topicSubjectInputEmptySubmit: this.state.topicSubjectInput === '',
} topicMessageInputEmptySubmit: this.state.topicMessageInput === ''
) });
); return;
this.props.history.push("/home");
} }
handleInputChange(event) { this.props.dispatch(
this.setState({[event.target.name]: event.target.value}); createTopic(
} {
topicSubject: this.state.topicSubjectInput,
topicMessage: this.state.topicMessageInput
},
),
);
this.props.history.push('/home');
}
handlePreviewToggle() { handleInputChange(event) {
this.setState((prevState, props) => ({ this.setState({
previewEnabled: !prevState.previewEnabled, [event.target.name]: event.target.value
previewDate: this.getDate() });
})); }
}
getDate() { handlePreviewToggle() {
const currentdate = new Date(); this.setState((prevState, props) => ({
return ((currentdate.getMonth() + 1) + " " previewEnabled: !prevState.previewEnabled,
+ currentdate.getDate() + ", " previewDate: this.getDate()
+ currentdate.getFullYear() + ", " }));
+ currentdate.getHours() + ":" }
+ currentdate.getMinutes() + ":"
+ currentdate.getSeconds());
}
render() { getDate() {
if (!this.props.user.hasSignedUp) { const currentdate = new Date();
this.props.history.push("/signup"); return (`${currentdate.getMonth() + 1} ${
return(null); currentdate.getDate()}, ${
} currentdate.getFullYear()}, ${
currentdate.getHours()}:${
currentdate.getMinutes()}:${
currentdate.getSeconds()}`);
}
var previewEditText = this.state.previewEnabled ? "Edit" : "Preview"; render() {
return ( if (!this.props.user.hasSignedUp) {
<div> this.props.history.push('/signup');
{this.state.previewEnabled && return (null);
<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 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 => { const mapStateToProps = state => ({
return { orbitDB: state.orbitDB,
orbitDB: state.orbitDB, user: state.user
user: state.user });
}
};
export default connect(mapStateToProps)(StartTopicContainer); export default connect(mapStateToProps)(StartTopicContainer);

291
app/src/containers/TopicContainer.js

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { push } from 'connected-react-router' import { push } from 'connected-react-router';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { drizzle } from '../index'; import { drizzle } from '../index';
@ -10,157 +10,180 @@ import FloatingButton from '../components/FloatingButton';
import { setNavBarTitle } from '../redux/actions/userInterfaceActions.js'; import { setNavBarTitle } from '../redux/actions/userInterfaceActions.js';
const contract = "Forum"; const contract = 'Forum';
const getTopicMethod = "getTopic"; const getTopicMethod = 'getTopic';
class TopicContainer extends Component { class TopicContainer extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
//Topic ID should be a positive integer
if (!/^[0-9]+$/.test(this.props.match.params.topicId)){
this.props.navigateTo('/404');
}
this.getBlockchainData = this.getBlockchainData.bind(this);
this.fetchTopicSubject = this.fetchTopicSubject.bind(this);
this.togglePostingState = this.togglePostingState.bind(this);
this.postCreated = this.postCreated.bind(this);
this.state = {
pageStatus: 'initialized',
topicID: parseInt(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: 'pending',
posting: false
};
}
getBlockchainData() { // Topic ID should be a positive integer
if (this.state.pageStatus === 'initialized' && if (!/^[0-9]+$/.test(this.props.match.params.topicId)) {
this.props.drizzleStatus['initialized']) { this.props.navigateTo('/404');
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.props.orbitDB.orbitdb !== null){
this.fetchTopicSubject(this.props.contracts[contract][getTopicMethod][this.dataKey].value[0]);
this.setState({ fetchTopicSubjectStatus: 'fetching' });
}
}
if (this.state.pageStatus === 'loaded' &&
this.state.fetchTopicSubjectStatus === 'pending' &&
this.props.orbitDB.orbitdb !== null) {
this.fetchTopicSubject(this.props.contracts[contract][getTopicMethod][this.dataKey].value[0]);
this.setState({ fetchTopicSubjectStatus: 'fetching' });
}
} }
async fetchTopicSubject(orbitDBAddress) { this.getBlockchainData = this.getBlockchainData.bind(this);
let orbitData; this.fetchTopicSubject = this.fetchTopicSubject.bind(this);
if (this.props.contracts[contract][getTopicMethod][this.dataKey].value[1] === this.props.user.address) { this.togglePostingState = this.togglePostingState.bind(this);
orbitData = this.props.orbitDB.topicsDB.get(this.state.topicID); this.postCreated = this.postCreated.bind(this);
} else {
const fullAddress = "/orbitdb/" + orbitDBAddress + "/topics"; this.state = {
const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress); pageStatus: 'initialized',
await store.load(); topicID: parseInt(this.props.match.params.topicId),
topicSubject: null,
let localOrbitData = store.get(this.state.topicID); postFocus: this.props.match.params.postId
if (localOrbitData) { && /^[0-9]+$/.test(this.props.match.params.postId)
orbitData = localOrbitData; ? this.props.match.params.postId
} else { : null,
// Wait until we have received something from the network fetchTopicSubjectStatus: 'pending',
store.events.on('replicated', () => { posting: false
orbitData = store.get(this.state.topicID); };
}) }
}
} getBlockchainData() {
if (this.state.pageStatus === 'initialized'
this.props.setNavBarTitle(orbitData['subject']); && 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.props.orbitDB.orbitdb !== null) {
this.fetchTopicSubject(
this.props.contracts[contract][getTopicMethod][this.dataKey].value[0],
);
this.setState({ this.setState({
topicSubject: orbitData['subject'], fetchTopicSubjectStatus: 'fetching'
fetchTopicSubjectStatus: 'fetched'
}); });
}
} }
if (this.state.pageStatus === 'loaded'
togglePostingState(event) { && this.state.fetchTopicSubjectStatus === 'pending'
if (event){ && this.props.orbitDB.orbitdb !== null) {
event.preventDefault(); this.fetchTopicSubject(
} this.props.contracts[contract][getTopicMethod][this.dataKey].value[0],
this.setState(prevState => ({ );
posting: !prevState.posting this.setState({
})); fetchTopicSubjectStatus: 'fetching'
});
} }
}
postCreated(){
this.setState(prevState => ({ async fetchTopicSubject(orbitDBAddress) {
posting: false 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();
const 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);
});
}
} }
render() { this.props.setNavBarTitle(orbitData.subject);
var topicContents; this.setState({
if (this.state.pageStatus === 'loaded') { topicSubject: orbitData.subject,
topicContents = ( fetchTopicSubjectStatus: 'fetched'
(<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>
);
}
componentDidMount() { togglePostingState(event) {
this.getBlockchainData(); if (event) {
event.preventDefault();
} }
this.setState(prevState => ({
componentDidUpdate() { posting: !prevState.posting
this.getBlockchainData(); }));
}
postCreated() {
this.setState(prevState => ({
posting: false
}));
}
render() {
let 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" />
{this.props.user.hasSignedUp && !this.state.posting
&& <FloatingButton onClick={this.togglePostingState} />
}
</div>
)
);
} }
componentWillUnmount() { return (
this.props.setNavBarTitle(''); <div className="fill">
} {topicContents}
{!this.state.posting
&& <div className="bottom-overlay-pad" />
}
</div>
);
}
componentDidMount() {
this.getBlockchainData();
}
componentDidUpdate() {
this.getBlockchainData();
}
componentWillUnmount() {
this.props.setNavBarTitle('');
}
} }
const mapDispatchToProps = dispatch => bindActionCreators({ const mapDispatchToProps = dispatch => bindActionCreators({
navigateTo: (location) => push(location), navigateTo: location => push(location),
setNavBarTitle: (navBarTitle) => setNavBarTitle(navBarTitle) setNavBarTitle: navBarTitle => setNavBarTitle(navBarTitle)
}, dispatch); }, dispatch);
const mapStateToProps = state => { const mapStateToProps = state => ({
return { user: state.user,
user: state.user, contracts: state.contracts,
contracts: state.contracts, drizzleStatus: state.drizzleStatus,
drizzleStatus: state.drizzleStatus, orbitDB: state.orbit
orbitDB: state.orbit });
}
};
export default connect(mapStateToProps, mapDispatchToProps)(TopicContainer); export default connect(mapStateToProps, mapDispatchToProps)(TopicContainer);

221
app/src/containers/TransactionsMonitorContainer.js

@ -1,130 +1,139 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom';
import { Message } from 'semantic-ui-react'; import { Message } from 'semantic-ui-react';
class RightSideBar extends Component { class RightSideBar extends Component {
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.handleMessageClick = this.handleMessageClick.bind(this); this.handleMessageClick = this.handleMessageClick.bind(this);
this.handleMessageDismiss = this.handleMessageDismiss.bind(this); this.handleMessageDismiss = this.handleMessageDismiss.bind(this);
this.state = { this.state = {
isTransactionMessageDismissed: [] isTransactionMessageDismissed: []
};
}
handleMessageClick(index) {
const transactionHash = this.props.transactionStack[index];
if (this.props.transactions[transactionHash]) {
if (this.props.transactions[transactionHash].status === 'error') {
this.handleMessageDismiss(null, index);
} else 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 'UsernameUpdated':
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}`);
this.handleMessageDismiss(null, index);
break;
case 'PostCreated':
this.props.history.push(`/topic/${
this.props.transactions[transactionHash].receipt.events.PostCreated.returnValues.topicID
}/${
this.props.transactions[transactionHash].receipt.events.PostCreated.returnValues.postID}`);
this.handleMessageDismiss(null, index);
break;
default:
this.handleMessageDismiss(null, index);
break;
} }
}
} }
}
handleMessageClick(index) { handleMessageDismiss(event, messageIndex) {
let transactionHash = this.props.transactionStack[index]; if (event !== null) {
if (this.props.transactions[transactionHash]) { event.stopPropagation();
if (this.props.transactions[transactionHash].status === 'error') {
this.handleMessageDismiss(null, index);
} else {
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 'UsernameUpdated':
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
);
this.handleMessageDismiss(null, index);
break;
case 'PostCreated':
this.props.history.push("/topic/" +
this.props.transactions[transactionHash].receipt.events.PostCreated.returnValues.topicID +
"/" +
this.props.transactions[transactionHash].receipt.events.PostCreated.returnValues.postID
);
this.handleMessageDismiss(null, index);
break;
default:
this.handleMessageDismiss(null, index);
break;
}
}
}
}
} }
handleMessageDismiss(event, messageIndex) { const isTransactionMessageDismissedShallowCopy = this.state.isTransactionMessageDismissed.slice();
if (event !== null) { isTransactionMessageDismissedShallowCopy[messageIndex] = true;
event.stopPropagation(); this.setState({
} isTransactionMessageDismissed: isTransactionMessageDismissedShallowCopy
});
}
let isTransactionMessageDismissedShallowCopy = this.state.isTransactionMessageDismissed.slice(); render() {
isTransactionMessageDismissedShallowCopy[messageIndex] = true; if (this.props.transactionStack.length === 0) {
this.setState({ return null;
isTransactionMessageDismissed: isTransactionMessageDismissedShallowCopy
});
} }
render() { const transactionMessages = this.props.transactionStack.map(
if (this.props.transactionStack.length === 0){ (transaction, index) => {
return null; if (this.state.isTransactionMessageDismissed[index]) {
return null;
} }
let transactionMessages = this.props.transactionStack.map((transaction, index) => { let color = 'black';
if (this.state.isTransactionMessageDismissed[index]){ const message = [];
return null; message.push(
} 'New transaction has been queued and is waiting your confirmation.',
);
let color = 'black'; if (this.props.transactions[transaction]) {
let message = []; message.push(<br key="confirmed" />);
message.push("New transaction has been queued and is waiting your confirmation."); message.push('- transaction confirmed');
if (this.props.transactions[transaction]) { }
message.push(<br key="confirmed"/>); if (this.props.transactions[transaction]
message.push("- transaction confirmed"); && this.props.transactions[transaction].status === 'success') {
} /* Transaction completed successfully */
if (this.props.transactions[transaction] && message.push(<br key="mined" />);
this.props.transactions[transaction].status === 'success') { message.push('- transaction mined');
/* Transaction completed successfully */ color = 'green';
message.push(<br key="mined"/>); message.push(<br key="success" />);
message.push("- transaction mined"); message.push('- transaction completed successfully');
color = 'green'; } else if (this.props.transactions[transaction]
message.push(<br key="success"/>); && this.props.transactions[transaction].status === 'error') {
message.push("- transaction completed successfully"); /* Transaction failed to complete */
} else if (this.props.transactions[transaction] && message.push(<br key="mined" />);
this.props.transactions[transaction].status === "error"){ message.push('- transaction mined');
/* Transaction failed to complete */ color = 'red';
message.push(<br key="mined"/>); message.push(<br key="fail" />);
message.push("- transaction mined"); message.push('Transaction failed to complete!');
color = 'red'; }
message.push(<br key="fail"/>);
message.push("Transaction failed to complete!");
}
return ( return (
<div className="sidebar-message" key={index} <div
onClick={() => {this.handleMessageClick(index)}} > className="sidebar-message"
<Message color={color} key={index}
onDismiss={(e) => {this.handleMessageDismiss(e, index)}}> onClick={() => { this.handleMessageClick(index); }}
{message} >
</Message> <Message
</div> color={color}
); onDismiss={(e) => {
}); this.handleMessageDismiss(e, index);
}}
>
{message}
</Message>
</div>
);
},
);
return (transactionMessages); return (transactionMessages);
} }
} }
const mapStateToProps = state => { const mapStateToProps = state => ({
return { transactions: state.transactions,
transactions: state.transactions, transactionStack: state.transactionStack
transactionStack: state.transactionStack });
}
};
const RightSideBarContainer = withRouter(connect(mapStateToProps)(RightSideBar)); const RightSideBarContainer = withRouter(
connect(mapStateToProps)(RightSideBar),
);
export default RightSideBarContainer; export default RightSideBarContainer;

329
app/src/containers/UsernameFormContainer.js

@ -1,175 +1,196 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from "react-redux"; import { connect } from 'react-redux';
import { Button, Message, Form, Dimmer, Loader, Header } from 'semantic-ui-react'; import { Button, Dimmer, Form, Header, Loader, Message } from 'semantic-ui-react';
import { drizzle } from '../index'; import { drizzle } from '../index';
import { createDatabases } from '../utils/orbitUtils'; import { createDatabases } from '../utils/orbitUtils';
import { updateUsername } from '../redux/actions/transactionsActions'; import { updateUsername } from '../redux/actions/transactionsActions';
const contract = "Forum"; const contract = 'Forum';
const checkUsernameTakenMethod = "isUserNameTaken"; const checkUsernameTakenMethod = 'isUserNameTaken';
const signUpMethod = "signUp"; const signUpMethod = 'signUp';
class UsernameFormContainer extends Component { class UsernameFormContainer extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.completeAction = this.completeAction.bind(this); this.completeAction = this.completeAction.bind(this);
this.checkedUsernames = []; this.checkedUsernames = [];
this.state = { this.state = {
usernameInput: '', usernameInput: '',
error: false, error: false,
errorHeader: "", errorHeader: '',
errorMessage: "", errorMessage: '',
signingUp: false signingUp: false
}; };
} }
handleInputChange(e, { name, value }) {
this.setState({
[name]: value,
error: false
});
if (value !== '') {
if (this.checkedUsernames.length > 0) {
if (this.checkedUsernames.some(e => e.usernameChecked === value)) {
return;
}
}
handleInputChange(e, { name, value }) { drizzle.contracts[contract].methods[checkUsernameTakenMethod].cacheCall(
value,
);
}
}
handleSubmit() {
if (this.state.usernameInput === '') {
this.setState({
error: true,
errorHeader: 'Data Incomplete',
errorMessage: 'You need to provide a username'
});
} else if (!this.state.error) {
// Makes sure current input username has been checked for availability
if (this.checkedUsernames.some(
e => e.usernameChecked === this.state.usernameInput,
)) {
this.completeAction();
}
}
}
async completeAction() {
if (this.props.user.hasSignedUp) {
this.props.dispatch(updateUsername(...[this.state.usernameInput], null));
} else {
this.setState({
signingUp: true
});
const orbitdbInfo = await createDatabases();
this.stackId = drizzle.contracts[contract].methods[signUpMethod].cacheSend(
...[
this.state.usernameInput,
orbitdbInfo.identityId,
orbitdbInfo.identityPublicKey,
orbitdbInfo.identityPrivateKey,
orbitdbInfo.orbitId,
orbitdbInfo.orbitPublicKey,
orbitdbInfo.orbitPrivateKey,
orbitdbInfo.topicsDB,
orbitdbInfo.postsDB
], {
from: this.props.account
},
);
}
this.setState({
usernameInput: ''
});
}
componentDidUpdate() {
if (this.state.signingUp) {
const txHash = this.props.transactionStack[this.stackId];
if (txHash
&& this.props.transactions[txHash]
&& this.props.transactions[txHash].status === 'error') {
this.setState({ this.setState({
[name]: value, signingUp: false
error: false
}); });
if (value !== '') { }
if (this.checkedUsernames.length > 0) { } else {
if (this.checkedUsernames.some(e => e.usernameChecked === value)){ const temp = Object.values(
return; this.props.contracts[contract][checkUsernameTakenMethod],
} );
} this.checkedUsernames = temp.map(checked => ({
usernameChecked: checked.args[0],
drizzle.contracts[contract].methods[checkUsernameTakenMethod].cacheCall(value); isTaken: checked.value
} }));
}
if (this.checkedUsernames.length > 0) {
handleSubmit() { this.checkedUsernames.forEach((checked) => {
if (this.state.usernameInput === ''){ if (checked.usernameChecked === this.state.usernameInput
&& checked.isTaken && !this.state.error) {
this.setState({ this.setState({
error: true, error: true,
errorHeader: "Data Incomplete", errorHeader: 'Data disapproved',
errorMessage: "You need to provide a username" errorMessage: 'This username is already taken'
}); });
} else if (!this.state.error) { }
// Makes sure current input username has been checked for availability });
if (this.checkedUsernames.some(e => e.usernameChecked === this.state.usernameInput)){ }
this.completeAction();
}
}
}
async completeAction() {
if(this.props.user.hasSignedUp){
this.props.dispatch(updateUsername(...[this.state.usernameInput], null));
} else {
this.setState({ signingUp: true });
const orbitdbInfo = await createDatabases();
this.stackId = drizzle.contracts[contract].methods[signUpMethod]
.cacheSend(...[this.state.usernameInput,
orbitdbInfo.identityId,
orbitdbInfo.identityPublicKey,
orbitdbInfo.identityPrivateKey,
orbitdbInfo.orbitId,
orbitdbInfo.orbitPublicKey,
orbitdbInfo.orbitPrivateKey,
orbitdbInfo.topicsDB,
orbitdbInfo.postsDB
], { from: this.props.account});
}
this.setState({ usernameInput: '' });
} }
}
componentDidUpdate() {
if (this.state.signingUp) { render() {
const txHash = this.props.transactionStack[this.stackId]; const { hasSignedUp } = this.props.user;
if (txHash &&
this.props.transactions[txHash] && if (hasSignedUp !== null) {
this.props.transactions[txHash].status === "error") { const buttonText = hasSignedUp ? 'Update' : 'Sign Up';
this.setState({signingUp: false}); const placeholderText = hasSignedUp
} ? this.props.user.username
} else { : 'Username';
const temp = Object.values(this.props.contracts[contract][checkUsernameTakenMethod]); const withError = this.state.error && {
this.checkedUsernames = temp.map(checked => {return { error: true
usernameChecked: checked.args[0], };
isTaken: checked.value
}}); /* var disableSubmit = true;
if (this.checkedUsernames.length > 0) {
if (this.checkedUsernames.length > 0){ if (this.checkedUsernames.some(e => e.usernameChecked === this.state.usernameInput)){
this.checkedUsernames.forEach( checked => { disableSubmit = false;
if (checked.usernameChecked === this.state.usernameInput && }
checked.isTaken && !this.state.error) { } else {
this.setState({ disableSubmit = false;
error: true, }
errorHeader: "Data disapproved",
errorMessage: "This username is already taken" disableSubmit = (disableSubmit || this.state.error) && {loading: true}; */
});
} return (
}) <div>
} <Form onSubmit={this.handleSubmit} {...withError}>
} <Form.Field required>
<label>Username</label>
<Form.Input
placeholder={placeholderText}
name="usernameInput"
value={this.state.usernameInput}
onChange={this.handleInputChange}
/>
</Form.Field>
<Message
error
header={this.state.errorHeader}
content={this.state.errorMessage}
/>
<Button type="submit">{buttonText}</Button>
</Form>
<Dimmer active={this.state.signingUp} page>
<Header as="h2" inverted>
<Loader size="large">
Magic elves are processing your noble
request.
</Loader>
</Header>
</Dimmer>
</div>
);
} }
render() { return (null);
const hasSignedUp = this.props.user.hasSignedUp; }
if(hasSignedUp !== null) {
const buttonText = hasSignedUp ? "Update" : "Sign Up";
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}>
<Form.Field required>
<label>Username</label>
<Form.Input
placeholder={placeholderText}
name='usernameInput'
value={this.state.usernameInput}
onChange={this.handleInputChange}
/>
</Form.Field>
<Message
error
header={this.state.errorHeader}
content={this.state.errorMessage}
/>
<Button type='submit'>{buttonText}</Button>
</Form>
<Dimmer active={this.state.signingUp} page>
<Header as='h2' inverted>
<Loader size='large'>Magic elves are processing your noble request.</Loader>
</Header>
</Dimmer>
</div>
);
}
return(null);
}
} }
const mapStateToProps = state => { const mapStateToProps = state => ({
return { account: state.accounts[0],
account: state.accounts[0], contracts: state.contracts,
contracts: state.contracts, transactions: state.transactions,
transactions: state.transactions, transactionStack: state.transactionStack,
transactionStack: state.transactionStack, user: state.user
user: state.user });
}
};
export default connect(mapStateToProps)(UsernameFormContainer); export default connect(mapStateToProps)(UsernameFormContainer);

20
app/src/helpers/EpochTimeConverter.js

@ -1,12 +1,12 @@
const epochTimeConverter = (timestamp) => { const epochTimeConverter = (timestamp) => {
var timestampDate = new Date(0); const timestampDate = new Date(0);
timestampDate.setUTCSeconds(timestamp); timestampDate.setUTCSeconds(timestamp);
return ((timestampDate.getMonth() + 1) + " " return (`${timestampDate.getMonth() + 1} ${
+ timestampDate.getDate() + ", " timestampDate.getDate()}, ${
+ timestampDate.getFullYear() + ", " timestampDate.getFullYear()}, ${
+ timestampDate.getHours() + ":" timestampDate.getHours()}:${
+ timestampDate.getMinutes() + ":" timestampDate.getMinutes()}:${
+ timestampDate.getSeconds()) timestampDate.getSeconds()}`);
} };
export default epochTimeConverter; export default epochTimeConverter;

22
app/src/index.js

@ -1,16 +1,16 @@
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router' import { ConnectedRouter } from 'connected-react-router';
import { Drizzle } from 'drizzle'; import { Drizzle } from 'drizzle';
import store, {history} from './redux/store'; import store, { history } from './redux/store';
import routes from './router/routes' import routes from './router/routes';
import { initIPFS } from './utils/orbitUtils' import { initIPFS } from './utils/orbitUtils';
import * as serviceWorker from './utils/serviceWorker'; import * as serviceWorker from './utils/serviceWorker';
import './assets/css/index.css'; import './assets/css/index.css';
import drizzleOptions from "./config/drizzleOptions"; import drizzleOptions from './config/drizzleOptions';
initIPFS(); initIPFS();
@ -19,12 +19,12 @@ const drizzle = new Drizzle(drizzleOptions, store);
export { drizzle }; export { drizzle };
render( render(
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
{ routes } {routes}
</ConnectedRouter> </ConnectedRouter>
</Provider>, </Provider>,
document.getElementById('root') document.getElementById('root'),
); );
serviceWorker.unregister(); // See also: http://bit.ly/CRA-PWA serviceWorker.unregister(); // See also: http://bit.ly/CRA-PWA

22
app/src/redux/actions/orbitActions.js

@ -3,14 +3,18 @@ const DATABASES_CREATED = 'DATABASES_CREATED';
const DATABASES_LOADED = 'DATABASES_LOADED'; const DATABASES_LOADED = 'DATABASES_LOADED';
const DATABASES_NOT_READY = 'DATABASES_NOT_READY'; const DATABASES_NOT_READY = 'DATABASES_NOT_READY';
function updateDatabases(type, orbitdb, topicsDB, postsDB){ function updateDatabases(type, orbitdb, topicsDB, postsDB) {
return { return {
type, type,
orbitdb, orbitdb,
topicsDB, topicsDB,
postsDB, postsDB,
id: orbitdb.id id: orbitdb.id
}; };
} }
export { DATABASES_CREATED, DATABASES_LOADED, DATABASES_NOT_READY, IPFS_INITIALIZED, updateDatabases } export { DATABASES_CREATED,
DATABASES_LOADED,
DATABASES_NOT_READY,
IPFS_INITIALIZED,
updateDatabases };

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

@ -1,53 +1,53 @@
//Action creators // Action creators
export const INIT_TRANSACTION = 'INIT_TRANSACTION'; export const INIT_TRANSACTION = 'INIT_TRANSACTION';
export const UPDATE_TRANSACTION = 'UPDATE_TRANSACTION'; export const UPDATE_TRANSACTION = 'UPDATE_TRANSACTION';
export function updateUsername(newUsername, callback){ export function updateUsername(newUsername, callback) {
return { return {
type: INIT_TRANSACTION, type: INIT_TRANSACTION,
transactionDescriptor: transactionDescriptor:
{ {
contract: 'Forum', contract: 'Forum',
method: 'updateUsername', method: 'updateUsername',
params: [newUsername], params: [newUsername],
event: 'UsernameUpdated' event: 'UsernameUpdated'
}, },
callback: callback callback
}; };
} }
export function createTopic(userInputs){ export function createTopic(userInputs) {
return { return {
type: INIT_TRANSACTION, type: INIT_TRANSACTION,
transactionDescriptor: transactionDescriptor:
{ {
contract: 'Forum', contract: 'Forum',
method: 'createTopic', method: 'createTopic',
params: [], params: [],
event: 'TopicCreated' event: 'TopicCreated'
}, },
userInputs: userInputs userInputs
}; };
} }
export function createPost(topicID, userInputs){ export function createPost(topicID, userInputs) {
return { return {
type: INIT_TRANSACTION, type: INIT_TRANSACTION,
transactionDescriptor: transactionDescriptor:
{ {
contract: 'Forum', contract: 'Forum',
method: 'createPost', method: 'createPost',
params: [topicID], params: [topicID],
event: 'PostCreated' event: 'PostCreated'
}, },
userInputs: userInputs userInputs
}; };
} }
export function updateTransaction(transactionIndex, updateDescriptor){ export function updateTransaction(transactionIndex, updateDescriptor) {
return { return {
type: UPDATE_TRANSACTION, type: UPDATE_TRANSACTION,
transactionUpdates: updateDescriptor transactionUpdates: updateDescriptor
}; };
} }

12
app/src/redux/actions/userInterfaceActions.js

@ -1,10 +1,10 @@
//Action creators // Action creators
export const SET_NAVBAR_TITLE = 'SET_NAVBAR_TITLE'; export const SET_NAVBAR_TITLE = 'SET_NAVBAR_TITLE';
export function setNavBarTitle(newTitle){ export function setNavBarTitle(newTitle) {
return { return {
type: SET_NAVBAR_TITLE, type: SET_NAVBAR_TITLE,
title: newTitle title: newTitle
}; };
} }

93
app/src/redux/reducers/orbitReducer.js

@ -1,53 +1,56 @@
import { IPFS_INITIALIZED, DATABASES_CREATED, DATABASES_LOADED, DATABASES_NOT_READY } from "../actions/orbitActions"; import { DATABASES_CREATED,
DATABASES_LOADED,
DATABASES_NOT_READY,
IPFS_INITIALIZED } from '../actions/orbitActions';
const initialState = { const initialState = {
ipfs: null, ipfs: null,
ipfsInitialized: false, ipfsInitialized: false,
ready: false, ready: false,
orbitdb: null, orbitdb: null,
topicsDB: null, topicsDB: null,
postsDB: null, postsDB: null,
id: null id: null
}; };
const orbitReducer = (state = initialState, action) => { const orbitReducer = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case IPFS_INITIALIZED: case IPFS_INITIALIZED:
return { return {
...state, ...state,
ipfs: action.ipfs, ipfs: action.ipfs,
ipfsInitialized: true ipfsInitialized: true
}; };
case DATABASES_CREATED: case DATABASES_CREATED:
return { return {
...state, ...state,
ready: true, ready: true,
orbitdb: action.orbitdb, orbitdb: action.orbitdb,
topicsDB: action.topicsDB, topicsDB: action.topicsDB,
postsDB: action.postsDB, postsDB: action.postsDB,
id: action.id id: action.id
}; };
case DATABASES_LOADED: case DATABASES_LOADED:
return { return {
...state, ...state,
ready: true, ready: true,
orbitdb: action.orbitdb, orbitdb: action.orbitdb,
topicsDB: action.topicsDB, topicsDB: action.topicsDB,
postsDB: action.postsDB, postsDB: action.postsDB,
id: action.id id: action.id
}; };
case DATABASES_NOT_READY: case DATABASES_NOT_READY:
return { return {
...state, ...state,
ready: false, ready: false,
orbitdb: null, orbitdb: null,
topicsDB: null, topicsDB: null,
postsDB: null, postsDB: null,
id: null id: null
}; };
default: default:
return state return state;
} }
}; };
export default orbitReducer; export default orbitReducer;

16
app/src/redux/reducers/rootReducer.js

@ -1,14 +1,14 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { drizzleReducers } from 'drizzle'; import { drizzleReducers } from 'drizzle';
import { connectRouter } from 'connected-react-router' import { connectRouter } from 'connected-react-router';
import userReducer from './userReducer'; import userReducer from './userReducer';
import orbitReducer from './orbitReducer'; import orbitReducer from './orbitReducer';
import userInterfaceReducer from './userInterfaceReducer'; import userInterfaceReducer from './userInterfaceReducer';
export default (history) => combineReducers({ export default history => combineReducers({
router: connectRouter(history), router: connectRouter(history),
user: userReducer, user: userReducer,
orbit: orbitReducer, orbit: orbitReducer,
interface: userInterfaceReducer, interface: userInterfaceReducer,
...drizzleReducers ...drizzleReducers
}) });

18
app/src/redux/reducers/userInterfaceReducer.js

@ -1,18 +1,18 @@
import { SET_NAVBAR_TITLE } from '../actions/userInterfaceActions'; import { SET_NAVBAR_TITLE } from '../actions/userInterfaceActions';
const initialState = { const initialState = {
navBarTitle: '' navBarTitle: ''
}; };
const userInterfaceReducer = (state = initialState, action) => { const userInterfaceReducer = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case SET_NAVBAR_TITLE: case SET_NAVBAR_TITLE:
return { return {
navBarTitle: action.title navBarTitle: action.title
}; };
default: default:
return state; return state;
} }
}; };
export default userInterfaceReducer; export default userInterfaceReducer;

42
app/src/redux/reducers/userReducer.js

@ -1,27 +1,27 @@
const initialState = { const initialState = {
username: "", username: '',
address: "0x0", address: '0x0',
avatarUrl: "", avatarUrl: '',
hasSignedUp: null hasSignedUp: null
}; };
const userReducer = (state = initialState, action) => { const userReducer = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'USER_DATA_UPDATED_(AUTHENTICATED)': case 'USER_DATA_UPDATED_(AUTHENTICATED)':
return { return {
username: action.username, username: action.username,
address: action.address, address: action.address,
hasSignedUp: true hasSignedUp: true
}; };
case 'USER_DATA_UPDATED_(GUEST)': case 'USER_DATA_UPDATED_(GUEST)':
return { return {
username: "", username: '',
address: action.address, address: action.address,
hasSignedUp: false hasSignedUp: false
}; };
default: default:
return state return state;
} }
}; };
export default userReducer; export default userReducer;

41
app/src/redux/sagas/drizzleUtilsSaga.js

@ -1,38 +1,37 @@
import { getContractInstance, getWeb3 } from "../../utils/drizzleUtils"; import { call, put, select, takeLatest } from 'redux-saga/effects';
import { call, put, takeLatest, select } from 'redux-saga/effects' import { getContractInstance, getWeb3 } from '../../utils/drizzleUtils';
import Forum from '../../contracts/Forum'; import Forum from '../../contracts/Forum';
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from "../actions/drizzleUtilsActions"; import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions';
const accounts = state => state.accounts;
const accounts = (state) => state.accounts; let initFlag; let web3; let
let initFlag, web3, contract; contract;
function* init() { function* init() {
if(!initFlag) { if (!initFlag) {
web3 = yield call(getWeb3); web3 = yield call(getWeb3);
contract = yield call(getContractInstance,{ contract = yield call(getContractInstance, {
web3: web3, web3, artifact: Forum
artifact: Forum });
}); initFlag = true;
initFlag=true; yield put({
yield put({type: DRIZZLE_UTILS_SAGA_INITIALIZED, ...[]}); type: DRIZZLE_UTILS_SAGA_INITIALIZED, ...[]
} });
else } else console.warn('Attempted to reinitialize drizzleUtilsSaga!');
console.warn("Attempted to reinitialize drizzleUtilsSaga!");
} }
// If the method below proves to be problematic/ineffective (i.e. getting current account // If the method below proves to be problematic/ineffective (i.e. getting current account
// from state), consider getting it from @drizzle-utils/get-accounts instead // from state), consider getting it from @drizzle-utils/get-accounts instead
// with (yield call(getAccounts, {web3}))[0]; // with (yield call(getAccounts, {web3}))[0];
function* getCurrentAccount(){ function* getCurrentAccount() {
return (yield select(accounts))[0]; return (yield select(accounts))[0];
} }
function* drizzleUtilsSaga() { function* drizzleUtilsSaga() {
yield takeLatest("DRIZZLE_INITIALIZED", init); yield takeLatest('DRIZZLE_INITIALIZED', init);
} }
export { web3, contract, getCurrentAccount } export { web3, contract, getCurrentAccount };
export default drizzleUtilsSaga; export default drizzleUtilsSaga;

82
app/src/redux/sagas/orbitSaga.js

@ -1,46 +1,62 @@
import {all, call, put, take, takeLatest} from 'redux-saga/effects' import { all, call, put, take, takeLatest } from 'redux-saga/effects';
import { contract, getCurrentAccount} from './drizzleUtilsSaga'; import { contract, getCurrentAccount } from './drizzleUtilsSaga';
import { loadDatabases } from '../../utils/orbitUtils' import { loadDatabases } from '../../utils/orbitUtils';
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions'; import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions';
import { IPFS_INITIALIZED, DATABASES_NOT_READY } from '../actions/orbitActions'; import { DATABASES_NOT_READY, IPFS_INITIALIZED } from '../actions/orbitActions';
let latestAccount; let latestAccount;
function* getOrbitDBInfo() { function* getOrbitDBInfo() {
yield put({type: 'ORRBIT_GETTING_INFO', ...[]}); yield put({
const account = yield call(getCurrentAccount); type: 'ORRBIT_GETTING_INFO', ...[]
if(account!==latestAccount) { });
const txObj1 = yield call(contract.methods["hasUserSignedUp"], ...[account]); const account = yield call(getCurrentAccount);
try { if (account !== latestAccount) {
const callResult = yield call(txObj1.call, {address:account}); const txObj1 = yield call(contract.methods.hasUserSignedUp,
if(callResult) { ...[account]);
// console.log("Deleting local storage.."); try {
// localStorage.clear(); const callResult = yield call(txObj1.call, {
const txObj2 = yield call(contract.methods["getOrbitIdentityInfo"], ...[account]); address: account
const orbitIdentityInfo = yield call(txObj2.call, {address: account}); });
const txObj3 = yield call(contract.methods["getOrbitDBInfo"], ...[account]); if (callResult) {
const orbitDBInfo = yield call(txObj3.call, {address: account}); // console.log("Deleting local storage..");
yield call(loadDatabases, orbitIdentityInfo[0], orbitIdentityInfo[1], orbitIdentityInfo[2], // localStorage.clear();
orbitDBInfo[0], orbitDBInfo[1], orbitDBInfo[2], orbitDBInfo[3], orbitDBInfo[4]); const txObj2 = yield call(contract.methods.getOrbitIdentityInfo,
} ...[account]);
else const orbitIdentityInfo = yield call(txObj2.call, {
yield put({type: DATABASES_NOT_READY, ...[]}); address: account
});
const txObj3 = yield call(contract.methods.getOrbitDBInfo,
...[account]);
const orbitDBInfo = yield call(txObj3.call, {
address: account
});
yield call(loadDatabases, orbitIdentityInfo[0], orbitIdentityInfo[1],
orbitIdentityInfo[2],
orbitDBInfo[0], orbitDBInfo[1], orbitDBInfo[2], orbitDBInfo[3],
orbitDBInfo[4]);
} else {
yield put({
type: DATABASES_NOT_READY, ...[]
});
}
latestAccount=account; latestAccount = account;
} } catch (error) {
catch (error) { console.error(error);
console.error(error); yield put({
yield put({type: 'ORBIT_SAGA_ERROR', ...[]}); type: 'ORBIT_SAGA_ERROR', ...[]
} });
} }
}
} }
function* orbitSaga() { function* orbitSaga() {
yield all([ yield all([
take(DRIZZLE_UTILS_SAGA_INITIALIZED), take(DRIZZLE_UTILS_SAGA_INITIALIZED),
take(IPFS_INITIALIZED) take(IPFS_INITIALIZED)
]); ]);
yield takeLatest("ACCOUNT_CHANGED", getOrbitDBInfo); yield takeLatest('ACCOUNT_CHANGED', getOrbitDBInfo);
} }
export default orbitSaga; export default orbitSaga;

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

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

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

@ -1,73 +1,81 @@
import {call, select, take, takeEvery} from 'redux-saga/effects' import { call, select, take, takeEvery } from 'redux-saga/effects';
import { drizzle } from '../../index' import { drizzle } from '../../index';
import { orbitSagaPut } from '../../utils/orbitUtils' import { orbitSagaPut } from '../../utils/orbitUtils';
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions'; import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions';
let transactionsHistory = Object.create(null); const transactionsHistory = Object.create(null);
function* initTransaction(action) { function* initTransaction(action) {
var dataKey = drizzle.contracts[action.transactionDescriptor.contract] const dataKey = drizzle.contracts[action.transactionDescriptor.contract].methods[action.transactionDescriptor.method].cacheSend(
.methods[action.transactionDescriptor['method']] ...(action.transactionDescriptor.params),
.cacheSend(...(action.transactionDescriptor.params)); );
transactionsHistory[dataKey] = action; transactionsHistory[dataKey] = action;
transactionsHistory[dataKey].state = 'initialized'; transactionsHistory[dataKey].state = 'initialized';
} }
function* handleEvent(action) { function* handleEvent(action) {
var transactionStack = yield select((state) => state.transactionStack); const transactionStack = yield select(state => state.transactionStack);
var dataKey = transactionStack.indexOf(action.event.transactionHash); const dataKey = transactionStack.indexOf(action.event.transactionHash);
switch(action.event.event) { switch (action.event.event) {
case 'TopicCreated': case 'TopicCreated':
if (dataKey !== -1 && if (dataKey !== -1
transactionsHistory[dataKey] && && transactionsHistory[dataKey]
transactionsHistory[dataKey].state === 'initialized') { && transactionsHistory[dataKey].state === 'initialized') {
transactionsHistory[dataKey].state = 'success'; transactionsHistory[dataKey].state = 'success';
//Gets orbit // Gets orbit
const orbit = yield select((state) => state.orbit); const orbit = yield select(state => state.orbit);
//And saves the topic // And saves the topic
yield call(orbitSagaPut, orbit.topicsDB, action.event.returnValues.topicID, yield call(orbitSagaPut, orbit.topicsDB,
{ subject: transactionsHistory[dataKey].userInputs.topicSubject }); action.event.returnValues.topicID,
yield call(orbitSagaPut, orbit.postsDB, action.event.returnValues.postID, {
{ subject: transactionsHistory[dataKey].userInputs.topicSubject, subject: transactionsHistory[dataKey].userInputs.topicSubject
content: transactionsHistory[dataKey].userInputs.topicMessage }); });
} yield call(orbitSagaPut, orbit.postsDB,
break; action.event.returnValues.postID,
case 'PostCreated': {
if (dataKey !== -1 && subject: transactionsHistory[dataKey].userInputs.topicSubject,
transactionsHistory[dataKey] && content: transactionsHistory[dataKey].userInputs.topicMessage
transactionsHistory[dataKey].state === 'initialized') { });
transactionsHistory[dataKey].state = 'success'; }
//Gets orbit break;
const orbit = yield select((state) => state.orbit); case 'PostCreated':
//And saves the topic if (dataKey !== -1
yield call(orbitSagaPut, orbit.postsDB, action.event.returnValues.postID, && transactionsHistory[dataKey]
{subject: transactionsHistory[dataKey].userInputs.postSubject, && transactionsHistory[dataKey].state === 'initialized') {
content: transactionsHistory[dataKey].userInputs.postMessage }); transactionsHistory[dataKey].state = 'success';
} // Gets orbit
break; const orbit = yield select(state => state.orbit);
default: // And saves the topic
//Nothing to do here yield call(orbitSagaPut, orbit.postsDB,
return; action.event.returnValues.postID,
} {
subject: transactionsHistory[dataKey].userInputs.postSubject,
content: transactionsHistory[dataKey].userInputs.postMessage
});
}
break;
default:
// Nothing to do here
}
} }
function* handleError() { function* handleError() {
var transactionStack = yield select((state) => state.transactionStack); const transactionStack = yield select(state => state.transactionStack);
transactionStack.forEach((transaction, index) => { transactionStack.forEach((transaction, index) => {
if (transaction.startsWith('TEMP_')) { if (transaction.startsWith('TEMP_')) {
transactionsHistory[index].state = 'error'; transactionsHistory[index].state = 'error';
} }
}) });
} }
function* transactionsSaga() { function* transactionsSaga() {
yield take(DRIZZLE_UTILS_SAGA_INITIALIZED); yield take(DRIZZLE_UTILS_SAGA_INITIALIZED);
yield takeEvery("INIT_TRANSACTION", initTransaction); yield takeEvery('INIT_TRANSACTION', initTransaction);
yield takeEvery("EVENT_FIRED", handleEvent); yield takeEvery('EVENT_FIRED', handleEvent);
yield takeEvery("TX_ERROR", handleError); yield takeEvery('TX_ERROR', handleError);
} }
export default transactionsSaga; export default transactionsSaga;

84
app/src/redux/sagas/userSaga.js

@ -1,53 +1,61 @@
import {call, put, select, take, takeEvery} from 'redux-saga/effects' import { call, put, select, take, takeEvery } from 'redux-saga/effects';
import { contract, getCurrentAccount } from './drizzleUtilsSaga'; import { contract, getCurrentAccount } from './drizzleUtilsSaga';
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from "../actions/drizzleUtilsActions"; import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions';
let account; let account;
function* updateUserData() { function* updateUserData() {
const currentAccount = yield call(getCurrentAccount); const currentAccount = yield call(getCurrentAccount);
if(currentAccount!==account) { if (currentAccount !== account) {
account = currentAccount; account = currentAccount;
yield put({type: 'ACCOUNT_CHANGED', ...[]}); yield put({
} type: 'ACCOUNT_CHANGED', ...[]
const txObj1 = yield call(contract.methods["hasUserSignedUp"], ...[account]); });
try { }
const userState = yield call(getUserState); const txObj1 = yield call(contract.methods.hasUserSignedUp, ...[account]);
const callResult = yield call(txObj1.call, {address:account}); try {
if(callResult) { const userState = yield call(getUserState);
const txObj2 = yield call(contract.methods["getUsername"], ...[account]); const callResult = yield call(txObj1.call, {
const username = yield call(txObj2.call, {address:account}); address: account
if(account!==userState.address || username!==userState.username){ });
const dispatchArgs = { if (callResult) {
address: account, const txObj2 = yield call(contract.methods.getUsername, ...[account]);
username: username const username = yield call(txObj2.call, {
}; address: account
yield put({type: 'USER_DATA_UPDATED_(AUTHENTICATED)', ...dispatchArgs}); });
} if (account !== userState.address || username !== userState.username) {
} const dispatchArgs = {
else{ address: account,
if(account!==userState.address){ username
const dispatchArgs = { };
address: account yield put({
}; type: 'USER_DATA_UPDATED_(AUTHENTICATED)', ...dispatchArgs
yield put({type: 'USER_DATA_UPDATED_(GUEST)', ...dispatchArgs}); });
} }
} } else if (account !== userState.address) {
} const dispatchArgs = {
catch (error) { address: account
console.error(error); };
yield put({type: 'USER_FETCHING_ERROR', ...[]}) yield put({
type: 'USER_DATA_UPDATED_(GUEST)', ...dispatchArgs
});
} }
} catch (error) {
console.error(error);
yield put({
type: 'USER_FETCHING_ERROR', ...[]
});
}
} }
function* getUserState(){ function* getUserState() {
return yield select((state) => state.user); return yield select(state => state.user);
} }
function* userSaga() { function* userSaga() {
yield take(DRIZZLE_UTILS_SAGA_INITIALIZED); yield take(DRIZZLE_UTILS_SAGA_INITIALIZED);
yield takeEvery("ACCOUNTS_FETCHED", updateUserData); yield takeEvery('ACCOUNTS_FETCHED', updateUserData);
} }
export default userSaga; export default userSaga;

25
app/src/redux/store.js

@ -1,32 +1,35 @@
import { createStore, applyMiddleware, compose } from 'redux'; import { applyMiddleware, compose, createStore } from 'redux';
import { createBrowserHistory } from 'history' import { createBrowserHistory } from 'history';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import {generateContractsInitialState} from 'drizzle'; import { generateContractsInitialState } from 'drizzle';
import {routerMiddleware} from 'connected-react-router'; import { routerMiddleware } from 'connected-react-router';
import rootSaga from './sagas/rootSaga'; import rootSaga from './sagas/rootSaga';
import drizzleOptions from '../config/drizzleOptions'; import drizzleOptions from '../config/drizzleOptions';
import createRootReducer from './reducers/rootReducer'; import createRootReducer from './reducers/rootReducer';
export const history = createBrowserHistory(); export const history = createBrowserHistory();
const rootReducer = createRootReducer(history); const rootReducer = createRootReducer(history);
const initialState = { contracts: generateContractsInitialState(drizzleOptions) }; const initialState = {
contracts: generateContractsInitialState(drizzleOptions)
};
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const sagaMiddleware = createSagaMiddleware(); const sagaMiddleware = createSagaMiddleware();
const routingMiddleware = routerMiddleware(history); const routingMiddleware = routerMiddleware(history);
const composedEnhancers = composeEnhancers(applyMiddleware(sagaMiddleware, routingMiddleware)); const composedEnhancers = composeEnhancers(
applyMiddleware(sagaMiddleware, routingMiddleware),
);
const store = createStore( const store = createStore(
rootReducer, rootReducer,
initialState, initialState,
composedEnhancers composedEnhancers,
); );
sagaMiddleware.run(rootSaga); sagaMiddleware.run(rootSaga);
export default store; export default store;

43
app/src/router/PrivateRoute.js

@ -1,28 +1,27 @@
import React from 'react' import React from 'react';
import {connect} from 'react-redux'; import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom' import { Redirect, Route } from 'react-router-dom';
const PrivateRoute = ({ component: Component, ...rest }) => ( const PrivateRoute = ({ component: Component, ...rest }) => (
<Route <Route
{...rest} {...rest}
render={props => render={props => (props.hasSignedUp ? (
props.hasSignedUp ? ( <Component {...props} />
<Component {...props} /> ) : (
) : ( <Redirect to={{
<Redirect to={{ pathname: '/signup',
pathname: "/signup", state: {
state: { from: props.location } from: props.location
}} }
/> }}
) />
} ))
/> }
/>
); );
const mapStateToProps = state => { const mapStateToProps = state => ({
return { hasSignedUp: state.user.hasSignedUp
hasSignedUp: state.user.hasSignedUp, });
}
};
export default connect(mapStateToProps)(PrivateRoute); export default connect(mapStateToProps)(PrivateRoute);

49
app/src/router/routes.js

@ -1,28 +1,31 @@
import React from 'react' import React from 'react';
import { Route, Redirect, Switch } from 'react-router-dom' import { Redirect, Route, Switch } from 'react-router-dom';
import CoreLayoutContainer from '../containers/CoreLayoutContainer'; import CoreLayoutContainer from '../containers/CoreLayoutContainer';
import HomeContainer from '../containers/HomeContainer' import HomeContainer from '../containers/HomeContainer';
import SignUpContainer from '../containers/SignUpContainer' import SignUpContainer from '../containers/SignUpContainer';
import StartTopicContainer from '../containers/StartTopicContainer' import StartTopicContainer from '../containers/StartTopicContainer';
import TopicContainer from '../containers/TopicContainer' import TopicContainer from '../containers/TopicContainer';
import ProfileContainer from '../containers/ProfileContainer' import ProfileContainer from '../containers/ProfileContainer';
import NotFound from '../components/NotFound' import NotFound from '../components/NotFound';
const routes = ( const routes = (
<div> <div>
<CoreLayoutContainer> <CoreLayoutContainer>
<Switch> <Switch>
<Route exact path="/" component={HomeContainer} /> <Route exact path="/" component={HomeContainer} />
<Redirect from='/home' to="/" /> <Redirect from="/home" to="/" />
<Route path="/signup" component={SignUpContainer} /> <Route path="/signup" component={SignUpContainer} />
<Route path="/startTopic" component={StartTopicContainer} /> <Route path="/startTopic" component={StartTopicContainer} />
<Route path="/topic/:topicId/:postId?" component={TopicContainer} /> <Route path="/topic/:topicId/:postId?" component={TopicContainer} />
<Route path='/profile/:address?/:username?' component={ProfileContainer} /> <Route
<Route path='/404' component={NotFound} /> path="/profile/:address?/:username?"
<Route component={NotFound} /> component={ProfileContainer}
</Switch> />
</CoreLayoutContainer> <Route path="/404" component={NotFound} />
</div> <Route component={NotFound} />
</Switch>
</CoreLayoutContainer>
</div>
); );
export default routes export default routes;

140
app/src/utils/drizzleUtils.js

@ -1,88 +1,86 @@
// See also: https://github.com/trufflesuite/drizzle-utils // See also: https://github.com/trufflesuite/drizzle-utils
const Web3 = require("web3"); const Web3 = require('web3');
const resolveWeb3 = (resolve, options, isBrowser) => { const resolveWeb3 = (resolve, options, isBrowser) => {
let provider; let provider;
if (options.customProvider) { if (options.customProvider) {
// use custom provider from options object // use custom provider from options object
provider = options.customProvider; provider = options.customProvider;
} else if (isBrowser && window.ethereum) { } else if (isBrowser && window.ethereum) {
// use `ethereum` object injected by MetaMask // use `ethereum` object injected by MetaMask
provider = window.ethereum; provider = window.ethereum;
} else if (isBrowser && typeof window.web3 !== "undefined") { } else if (isBrowser && typeof window.web3 !== 'undefined') {
// use injected web3 object by legacy dapp browsers // use injected web3 object by legacy dapp browsers
provider = window.web3.currentProvider; provider = window.web3.currentProvider;
} else if (options.fallbackProvider) { } else if (options.fallbackProvider) {
// use fallback provider from options object // use fallback provider from options object
provider = options.fallbackProvider; provider = options.fallbackProvider;
} else { } else {
// connect to development blockchain from `truffle develop` // connect to development blockchain from `truffle develop`
provider = new Web3.providers.HttpProvider("http://127.0.0.1:9545"); provider = new Web3.providers.HttpProvider('http://127.0.0.1:9545');
} }
const web3 = new Web3(provider); const web3 = new Web3(provider);
resolve(web3); resolve(web3);
}; };
const getWeb3 = (options = {}) => const getWeb3 = (options = {
new Promise(resolve => { }) => new Promise((resolve) => {
// handle server-side and React Native environments // handle server-side and React Native environments
const isReactNative = const isReactNative = typeof navigator !== 'undefined' && navigator.product
typeof navigator !== "undefined" && navigator.product === "ReactNative"; === 'ReactNative';
const isNode = typeof window === "undefined"; const isNode = typeof window === 'undefined';
if (isNode || isReactNative) { if (isNode || isReactNative) {
return resolveWeb3(resolve, options, false); return resolveWeb3(resolve, options, false);
} }
// if page is ready, resolve for web3 immediately // if page is ready, resolve for web3 immediately
if (document.readyState === `complete`) { if (document.readyState === 'complete') {
return resolveWeb3(resolve, options, true); return resolveWeb3(resolve, options, true);
} }
// otherwise, resolve for web3 when page is done loading // otherwise, resolve for web3 when page is done loading
return window.addEventListener("load", () => return window.addEventListener('load', () => resolveWeb3(resolve, options, true));
resolveWeb3(resolve, options, true), });
);
});
const getContractInstance = (options = {}) => const getContractInstance = (options = {
new Promise(async (resolve, reject) => { }) => new Promise(async (resolve, reject) => {
if (!options.web3) { if (!options.web3) {
return reject(new Error("The options object with web3 is required.")); return reject(new Error('The options object with web3 is required.'));
} }
const { web3 } = options; const { web3 } = options;
let instance; let instance;
try { try {
if (options.artifact) { if (options.artifact) {
// if artifact exists, attempt to get network ID and the deployed address // if artifact exists, attempt to get network ID and the deployed address
const { artifact } = options; const { artifact } = options;
const networkId = await web3.eth.net.getId(); // web3 v1.0.0-beta.47 breaks here const networkId = await web3.eth.net.getId(); // web3 v1.0.0-beta.47 breaks here
const deployedNetwork = artifact.networks[networkId]; const deployedNetwork = artifact.networks[networkId];
// if no deployed address is found, instantiate without the address // if no deployed address is found, instantiate without the address
const address = deployedNetwork && deployedNetwork.address; const address = deployedNetwork && deployedNetwork.address;
instance = new web3.eth.Contract(artifact.abi, address); instance = new web3.eth.Contract(artifact.abi, address);
} else if (options.abi) { } else if (options.abi) {
// otherwise, use passed-in ABI and deployed address (optional) // otherwise, use passed-in ABI and deployed address (optional)
const { abi, address } = options; const { abi, address } = options;
instance = new web3.eth.Contract(abi, address); instance = new web3.eth.Contract(abi, address);
} else { } else {
return reject( return reject(
new Error( new Error(
"You must pass in a contract artifact or the ABI of a deployed contract.", 'You must pass in a contract artifact or the ABI of a deployed contract.',
), ),
); );
} }
return resolve(instance); return resolve(instance);
} catch (err) { } catch (err) {
return reject(err); return reject(err);
} }
}); });
export { getWeb3, getContractInstance }; export { getWeb3, getContractInstance };

92
app/src/utils/orbitUtils.js

@ -1,69 +1,77 @@
import OrbitDB from 'orbit-db'; import OrbitDB from 'orbit-db';
import Keystore from 'orbit-db-keystore'; import Keystore from 'orbit-db-keystore';
import path from 'path'; import path from 'path';
import IPFS from 'ipfs';
import store from '../redux/store'; import store from '../redux/store';
import { DATABASES_CREATED, DATABASES_LOADED, IPFS_INITIALIZED, updateDatabases } from '../redux/actions/orbitActions'; import { DATABASES_CREATED, DATABASES_LOADED, IPFS_INITIALIZED, updateDatabases } from '../redux/actions/orbitActions';
import IPFS from "ipfs"; import ipfsOptions from '../config/ipfsOptions';
import ipfsOptions from "../config/ipfsOptions";
function initIPFS(){ function initIPFS() {
const ipfs = new IPFS(ipfsOptions); const ipfs = new IPFS(ipfsOptions);
ipfs.on('ready', async () => { ipfs.on('ready', async () => {
store.dispatch({type: IPFS_INITIALIZED, ipfs}); store.dispatch({
console.log("IPFS initialized."); type: IPFS_INITIALIZED, ipfs
}); });
console.log('IPFS initialized.');
});
} }
async function createDatabases() { async function createDatabases() {
console.log("Creating databases..."); console.log('Creating databases...');
const ipfs = getIPFS(); const ipfs = getIPFS();
const orbitdb = await new OrbitDB(ipfs); const orbitdb = await new OrbitDB(ipfs);
const topicsDB = await orbitdb.keyvalue('topics'); const topicsDB = await orbitdb.keyvalue('topics');
const postsDB = await orbitdb.keyvalue('posts'); const postsDB = await orbitdb.keyvalue('posts');
store.dispatch(updateDatabases(DATABASES_CREATED, orbitdb, topicsDB, postsDB)); store.dispatch(
updateDatabases(DATABASES_CREATED, orbitdb, topicsDB, postsDB),
);
const orbitKey = orbitdb.keystore.getKey(orbitdb.id); const orbitKey = orbitdb.keystore.getKey(orbitdb.id);
return { return {
identityId: "Tempus", identityId: 'Tempus',
identityPublicKey: "edax", identityPublicKey: 'edax',
identityPrivateKey: "rerum", identityPrivateKey: 'rerum',
orbitId: orbitdb.id, orbitId: orbitdb.id,
orbitPublicKey: orbitKey.getPublic('hex'), orbitPublicKey: orbitKey.getPublic('hex'),
orbitPrivateKey: orbitKey.getPrivate('hex'), orbitPrivateKey: orbitKey.getPrivate('hex'),
topicsDB: topicsDB.address.root, topicsDB: topicsDB.address.root,
postsDB: postsDB.address.root postsDB: postsDB.address.root
}; };
} }
async function loadDatabases(identityId, identityPublicKey, identityPrivateKey, async function loadDatabases(identityId, identityPublicKey, identityPrivateKey,
orbitId, orbitPublicKey, orbitPrivateKey, topicsDBId, postsDBId) { orbitId, orbitPublicKey, orbitPrivateKey,
console.log("Loading databases..."); topicsDBId, postsDBId) {
let directory = "./orbitdb"; console.log('Loading databases...');
let keystore = Keystore.create(path.join(directory, orbitId, '/keystore')); const directory = './orbitdb';
const keystore = Keystore.create(path.join(directory, orbitId, '/keystore'));
keystore._storage.setItem(orbitId, JSON.stringify({ keystore._storage.setItem(orbitId, JSON.stringify({
publicKey: orbitPublicKey, publicKey: orbitPublicKey,
privateKey: orbitPrivateKey privateKey: orbitPrivateKey
})); }));
const ipfs = getIPFS(); const ipfs = getIPFS();
const orbitdb = await new OrbitDB(ipfs, directory, { peerId:orbitId, keystore:keystore}); const orbitdb = await new OrbitDB(ipfs, directory,
const topicsDB = await orbitdb.keyvalue('/orbitdb/' + topicsDBId +'/topics'); {
const postsDB = await orbitdb.keyvalue('/orbitdb/' + postsDBId +'/posts'); peerId: orbitId, keystore
});
const topicsDB = await orbitdb.keyvalue(`/orbitdb/${topicsDBId}/topics`);
const postsDB = await orbitdb.keyvalue(`/orbitdb/${postsDBId}/posts`);
await topicsDB.load(); await topicsDB.load();
await postsDB.load(); await postsDB.load();
store.dispatch(updateDatabases(DATABASES_LOADED, orbitdb, topicsDB, postsDB)); store.dispatch(updateDatabases(DATABASES_LOADED, orbitdb, topicsDB, postsDB));
} }
function getIPFS(){ function getIPFS() {
return store.getState().orbit.ipfs; return store.getState().orbit.ipfs;
} }
async function orbitSagaPut(db, key, value) { async function orbitSagaPut(db, key, value) {
db.put(key, value); db.put(key, value);
} }
export { initIPFS, createDatabases, loadDatabases, orbitSagaPut }; export { initIPFS, createDatabases, loadDatabases, orbitSagaPut };

129
app/src/utils/serviceWorker.js

@ -11,13 +11,13 @@
// opt-in, read http://bit.ly/CRA-PWA // opt-in, read http://bit.ly/CRA-PWA
const isLocalhost = Boolean( const isLocalhost = Boolean(
window.location.hostname === 'localhost' || window.location.hostname === 'localhost'
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' || || window.location.hostname === '[::1]'
// 127.0.0.1/8 is considered localhost for IPv4. // 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match( || window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
) ),
); );
export function register(config) { export function register(config) {
@ -42,8 +42,8 @@ export function register(config) {
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + 'This web app is being served cache-first by a service '
'worker. To learn more, visit http://bit.ly/CRA-PWA' + 'worker. To learn more, visit http://bit.ly/CRA-PWA',
); );
}); });
} else { } else {
@ -55,80 +55,75 @@ export function register(config) {
} }
function registerValidSW(swUrl, config) { function registerValidSW(swUrl, config) {
navigator.serviceWorker navigator.serviceWorker.register(swUrl).then((registration) => {
.register(swUrl) registration.onupdatefound = () => {
.then(registration => { const installingWorker = registration.installing;
registration.onupdatefound = () => { if (installingWorker == null) {
const installingWorker = registration.installing; return;
if (installingWorker == null) { }
return; installingWorker.onstatechange = () => {
} if (installingWorker.state === 'installed') {
installingWorker.onstatechange = () => { if (navigator.serviceWorker.controller) {
if (installingWorker.state === 'installed') { // At this point, the updated precached content has been fetched,
if (navigator.serviceWorker.controller) { // but the previous service worker will still serve the older
// At this point, the updated precached content has been fetched, // content until all client tabs are closed.
// but the previous service worker will still serve the older console.log(
// content until all client tabs are closed. 'New content is available and will be used when all '
console.log( + 'tabs for this page are closed. See http://bit.ly/CRA-PWA.',
'New content is available and will be used when all ' + );
'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
);
// Execute callback // Execute callback
if (config && config.onUpdate) { if (config && config.onUpdate) {
config.onUpdate(registration); config.onUpdate(registration);
} }
} else { } else {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
console.log('Content is cached for offline use.'); console.log('Content is cached for offline use.');
// Execute callback // Execute callback
if (config && config.onSuccess) { if (config && config.onSuccess) {
config.onSuccess(registration); config.onSuccess(registration);
}
} }
} }
}; }
}; };
}) };
.catch(error => { }).catch((error) => {
console.error('Error during service worker registration:', error); console.error('Error during service worker registration:', error);
}); });
} }
function checkValidServiceWorker(swUrl, config) { function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page. // Check if the service worker can be found. If it can't reload the page.
fetch(swUrl) fetch(swUrl).then((response) => {
.then(response => { // Ensure service worker exists, and that we really are getting a JS file.
// Ensure service worker exists, and that we really are getting a JS file. const contentType = response.headers.get('content-type');
const contentType = response.headers.get('content-type'); if (
if ( response.status === 404
response.status === 404 || || (contentType != null && contentType.indexOf('javascript') === -1)
(contentType != null && contentType.indexOf('javascript') === -1) ) {
) { // No service worker found. Probably a different app. Reload the page.
// No service worker found. Probably a different app. Reload the page. navigator.serviceWorker.ready.then((registration) => {
navigator.serviceWorker.ready.then(registration => { registration.unregister().then(() => {
registration.unregister().then(() => { window.location.reload();
window.location.reload();
});
}); });
} else { });
// Service worker found. Proceed as normal. } else {
registerValidSW(swUrl, config); // Service worker found. Proceed as normal.
} registerValidSW(swUrl, config);
}) }
.catch(() => { }).catch(() => {
console.log( console.log(
'No internet connection found. App is running in offline mode.' 'No internet connection found. App is running in offline mode.',
); );
}); });
} }
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister(); registration.unregister();
}); });
} }

7
package.json

@ -13,5 +13,12 @@
}, },
"dependencies": { "dependencies": {
"openzeppelin-solidity": "^2.1.2" "openzeppelin-solidity": "^2.1.2"
},
"devDependencies": {
"eslint": "5.15.1",
"eslint-config-airbnb": "17.1.0",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.12.4"
} }
} }

40
truffle-config.js

@ -1,25 +1,25 @@
const path = require("path"); const path = require('path');
module.exports = { module.exports = {
// See <http://truffleframework.com/docs/advanced/configuration> // See <http://truffleframework.com/docs/advanced/configuration>
// to customize your Truffle configuration! // to customize your Truffle configuration!
contracts_build_directory: path.join(__dirname, "app/src/contracts"), contracts_build_directory: path.join(__dirname, 'app/src/contracts'),
networks: { networks: {
development: { development: {
host: "localhost", host: 'localhost',
port: 8545, port: 8545,
network_id: "*" // Match any network id network_id: '*' // Match any network id
} }
}, },
compilers:{ compilers: {
solc: { solc: {
version: "0.5.5", version: '0.5.5',
settings:{ settings: {
optimizer: { optimizer: {
enabled: true, enabled: true,
runs: 500 runs: 500
}
}
} }
}
} }
}
}; };

Loading…
Cancel
Save