Browse Source

Merge branch 'rewrite' into develop

develop
Ezerous 6 years ago
parent
commit
2d66b68ae9
  1. 15
      .eslintrc.json
  2. 1
      .eslintrignore
  3. 6
      .gitattributes
  4. 10
      .gitignore
  5. 3
      app/.eslintrc.json
  6. 1
      app/.eslintrignore
  7. 45
      app/package.json
  8. 0
      app/public/favicon.ico
  9. 39
      app/public/index.html
  10. 15
      app/public/manifest.json
  11. 11
      app/src/CustomPropTypes.js
  12. 187
      app/src/assets/css/App.css
  13. 30
      app/src/assets/css/board-container.css
  14. 4
      app/src/assets/css/index.css
  15. 4
      app/src/assets/css/profile-container.css
  16. 12
      app/src/assets/css/sign-up-container.css
  17. 6
      app/src/assets/css/start-topic-container.css
  18. 51
      app/src/assets/css/topic-container.css
  19. 12016
      app/src/assets/fonts/fontawesome-free-5.7.2/all.js
  20. 0
      app/src/assets/images/PageNotFound.jpg
  21. 0
      app/src/assets/images/ipfs_logo.png
  22. 0
      app/src/assets/images/logo.png
  23. 17
      app/src/components/FloatingButton.js
  24. 23
      app/src/components/LoadingSpinner.js
  25. 232
      app/src/components/NewPost.js
  26. 68
      app/src/components/NewTopicPreview.js
  27. 13
      app/src/components/NotFound.js
  28. 310
      app/src/components/Post.js
  29. 97
      app/src/components/PostList.js
  30. 162
      app/src/components/ProfileInformation.js
  31. 139
      app/src/components/Topic.js
  32. 88
      app/src/components/TopicList.js
  33. 20
      app/src/config/drizzleOptions.js
  34. 22
      app/src/config/ipfsOptions.js
  35. 139
      app/src/containers/BoardContainer.js
  36. 45
      app/src/containers/CoreLayoutContainer.js
  37. 26
      app/src/containers/HomeContainer.js
  38. 107
      app/src/containers/LoadingContainer.js
  39. 66
      app/src/containers/NavBarContainer.js
  40. 221
      app/src/containers/ProfileContainer.js
  41. 55
      app/src/containers/SignUpContainer.js
  42. 167
      app/src/containers/StartTopicContainer.js
  43. 212
      app/src/containers/TopicContainer.js
  44. 153
      app/src/containers/TransactionsMonitorContainer.js
  45. 216
      app/src/containers/UsernameFormContainer.js
  46. 12
      app/src/helpers/EpochTimeConverter.js
  47. 33
      app/src/index.js
  48. 1
      app/src/redux/actions/drizzleUtilsActions.js
  49. 37
      app/src/redux/actions/orbitActions.js
  50. 53
      app/src/redux/actions/transactionsActions.js
  51. 10
      app/src/redux/actions/userInterfaceActions.js
  52. 82
      app/src/redux/reducers/orbitReducer.js
  53. 14
      app/src/redux/reducers/rootReducer.js
  54. 18
      app/src/redux/reducers/userInterfaceReducer.js
  55. 27
      app/src/redux/reducers/userReducer.js
  56. 37
      app/src/redux/sagas/drizzleUtilsSaga.js
  57. 22
      app/src/redux/sagas/eventSaga.js
  58. 106
      app/src/redux/sagas/orbitSaga.js
  59. 20
      app/src/redux/sagas/rootSaga.js
  60. 82
      app/src/redux/sagas/transactionsSaga.js
  61. 61
      app/src/redux/sagas/userSaga.js
  62. 35
      app/src/redux/store.js
  63. 27
      app/src/router/PrivateRoute.js
  64. 31
      app/src/router/routes.js
  65. 86
      app/src/utils/drizzleUtils.js
  66. 109
      app/src/utils/orbitUtils.js
  67. 130
      app/src/utils/serviceWorker.js
  68. 113
      contracts/Forum.sol
  69. 12
      contracts/Migrations.sol
  70. 81
      contracts/Posting.sol
  71. 2
      migrations/1_initial_migration.js
  72. 4
      migrations/2_deploy_contracts.js
  73. 43
      package.json
  74. 44
      public/index.html
  75. 186
      src/assets/css/App.css
  76. 30
      src/assets/css/board-container.css
  77. 4
      src/assets/css/index.css
  78. 17
      src/assets/css/loading-container.css
  79. 108
      src/assets/css/progress-bar.css
  80. 12
      src/assets/css/sign-up-container.css
  81. 6
      src/assets/css/start-topic-container.css
  82. 51
      src/assets/css/topic-container.css
  83. 3271
      src/assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js
  84. 14
      src/components/FloatingButton.js
  85. 16
      src/components/LoadingSpinner.js
  86. 63
      src/components/NavBar.js
  87. 175
      src/components/NewPost.js
  88. 65
      src/components/NewTopicPreview.js
  89. 11
      src/components/NotFoundView.js
  90. 173
      src/components/Post.js
  91. 36
      src/components/PostList.js
  92. 59
      src/components/ProfileInformation.js
  93. 100
      src/components/Topic.js
  94. 30
      src/components/TopicList.js
  95. 80
      src/components/WithBlockchainData.js
  96. 117
      src/containers/BoardContainer.js
  97. 35
      src/containers/HomeContainer.js
  98. 88
      src/containers/LoadingContainer.js
  99. 195
      src/containers/ProfileContainer.js
  100. 50
      src/containers/SignUpContainer.js

15
.eslintrc.json

@ -0,0 +1,15 @@
{
"extends": "airbnb",
"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"]
}
}

1
.eslintrignore

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

6
.gitattributes

@ -1 +1,7 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.ico binary

10
.gitignore

@ -1,7 +1,6 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# Node
/node_modules
/app/node_modules
package-lock.json
# Yarn
@ -10,9 +9,14 @@ yarn.lock
# Testing
/coverage
# Production
# Build Directories
/build
/src/build
/app/src/build
/app/build
# Contract Artifacts
/app/src/contracts
# Logs
npm-debug.log*

3
app/.eslintrc.json

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

1
app/.eslintrignore

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

45
app/package.json

@ -0,0 +1,45 @@
{
"name": "apella",
"version": "0.1.0",
"private": true,
"repository": {
"type": "git",
"url": "https://gitlab.com/Ezerous/Apella.git"
},
"dependencies": {
"connected-react-router": "6.3.2",
"drizzle": "1.3.3",
"history": "4.9.0",
"ipfs": "github:ipfs/js-ipfs#e849dbcab4a313f7ffc1532a389097ee76344067",
"lodash.isequal": "4.5.0",
"orbit-db": "0.19.9",
"orbit-db-keystore": "0.1.0",
"prop-types": "15.7.2",
"react": "16.8.4",
"react-content-loader": "4.2.1",
"react-dom": "16.8.4",
"react-markdown": "4.0.6",
"react-redux": "6.0.1",
"react-router-dom": "5.0.0",
"react-scripts": "2.1.8",
"react-timeago": "4.4.0",
"react-user-avatar": "1.10.0",
"redux": "4.0.1",
"redux-saga": "0.16.2",
"semantic-ui-react": "0.86.0",
"uuid": "3.3.2",
"web3": "1.0.0-beta.50"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

0
public/favicon.ico → app/public/favicon.ico

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

39
app/public/index.html

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" />
<title>Apella</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

15
app/public/manifest.json

@ -0,0 +1,15 @@
{
"short_name": "Apella",
"name": "Apella",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

11
app/src/CustomPropTypes.js

@ -0,0 +1,11 @@
import PropTypes from 'prop-types';
const GetTopicResult = PropTypes.PropTypes.shape({
0: PropTypes.string,
1: PropTypes.string,
2: PropTypes.string,
3: PropTypes.string,
4: PropTypes.arrayOf(PropTypes.number)
});
export { GetTopicResult };

187
app/src/assets/css/App.css

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

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

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

4
app/src/assets/css/index.css

@ -0,0 +1,4 @@
body {
margin: 10em;
padding: 0;
}

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

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

0
src/resources/PageNotFound.jpg → app/src/assets/images/PageNotFound.jpg

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

0
src/resources/ipfs_logo.png → app/src/assets/images/ipfs_logo.png

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

0
src/resources/logo.png → app/src/assets/images/logo.png

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

17
app/src/components/FloatingButton.js

@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Icon } from 'semantic-ui-react';
const FloatingButton = ({ onClick }) => (
<div className="action-button" role="button" onClick={onClick} onKeyUp={onClick} tabIndex={0}>
<Button icon color="teal" size="large">
<Icon name="add" />
</Button>
</div>
);
FloatingButton.propTypes = {
onClick: PropTypes.func.isRequired
};
export default FloatingButton;

23
app/src/components/LoadingSpinner.js

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

232
app/src/components/NewPost.js

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

68
app/src/components/NewTopicPreview.js

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

13
app/src/components/NotFound.js

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

310
app/src/components/Post.js

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

97
app/src/components/PostList.js

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

162
app/src/components/ProfileInformation.js

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

139
app/src/components/Topic.js

@ -0,0 +1,139 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { GetTopicResult } from '../CustomPropTypes'
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import ContentLoader from 'react-content-loader';
import { Card } from 'semantic-ui-react';
import TimeAgo from 'react-timeago';
import epochTimeConverter from '../helpers/EpochTimeConverter';
import { addPeerDatabase } from '../redux/actions/orbitActions';
class Topic extends Component {
constructor(props) {
super(props);
this.state = {
askedForReplication: false,
fetchedSubject: false
};
}
componentDidUpdate() {
const { dispatch, topicData, topicSubject, orbitDB } = this.props;
const { askedForReplication } = this.state;
if(!askedForReplication && orbitDB.ipfsInitialized && orbitDB.orbitdb && dispatch && !topicSubject && topicData) {
this.setState({ askedForReplication: true });
dispatch(addPeerDatabase(`/orbitdb/${topicData.value[0]}/topics`));
}
}
render() {
const { history, topicID, topicData, topicSubject } = this.props;
return (
<Card
link
className="card"
onClick={() => {
history.push(`/topic/${topicID}`);
}}
>
<Card.Content>
<div className={`topic-subject${
topicSubject ? '' : ' grey-text'}`}
>
<p>
<strong>
{(topicSubject) ? 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${
topicData !== null ? '' : ' grey-text'}`}
>
{topicData !== null
? topicData.value[2]
: 'Username'
}
</p>
<p className={`no-margin${
topicData !== null ? '' : ' grey-text'}`}
>
{`Number of replies: ${topicData !== null
? topicData.value[4].length
: ''}`
}
</p>
<p className="topic-date grey-text">
{topicData !== null
&& (
<TimeAgo
date={epochTimeConverter(topicData.value[3])}
/>
)
}
</p>
</div>
</Card.Content>
</Card>
);
}
}
Topic.propTypes = {
user: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
topicData: GetTopicResult.isRequired,
orbitDB: PropTypes.object.isRequired,
topicID: PropTypes.number.isRequired
};
function getTopicSubject(state, props){
const { user, orbit } = state;
if (orbit.ipfsInitialized && orbit.orbitdb) {
const { topicData, topicID } = props;
if (topicData){
if(user && topicData.value[1] === user.address) {
const orbitData = orbit.topicsDB.get(topicID);
if(orbitData && orbitData.subject)
return orbitData.subject;
}
const db = orbit.replicatedDatabases.find(db => db.fullAddress === `/orbitdb/${topicData.value[0]}/topics`);
if(db && db.ready && db.store){
const localOrbitData = db.store.get(topicID);
if (localOrbitData){
return localOrbitData.subject;
}
}
}
}
return null;
}
function mapStateToProps(state, ownProps) {
return {
user: state.user,
orbitDB: state.orbit,
topicSubject: getTopicSubject(state, ownProps)
}
}
export default withRouter(connect(mapStateToProps)(Topic));

88
app/src/components/TopicList.js

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

20
app/src/config/drizzleOptions.js

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

22
app/src/config/ipfsOptions.js

@ -0,0 +1,22 @@
// OrbitDB uses Pubsub which is an experimental feature
// and need to be turned on manually.
const ipfsOptions = {
EXPERIMENTAL: {
pubsub: true
},
config: {
Addresses: {
Swarm: [
'/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/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'
]
}
},
preload: {
enabled: false
}
};
export default ipfsOptions;

139
app/src/containers/BoardContainer.js

@ -0,0 +1,139 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { Header } from 'semantic-ui-react';
import { drizzle } from '../index';
import TopicList from '../components/TopicList';
import FloatingButton from '../components/FloatingButton';
/* import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions'; */
const contract = 'Forum';
const getNumberOfTopicsMethod = 'getNumberOfTopics';
class BoardContainer extends Component {
constructor(props) {
super(props);
/* this.props.store.dispatch(showProgressBar()); */
this.getBlockchainData = this.getBlockchainData.bind(this);
this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this);
this.state = {
pageStatus: 'initialized'
};
}
componentDidMount() {
this.getBlockchainData();
}
componentDidUpdate() {
this.getBlockchainData();
}
getBlockchainData() {
const { pageStatus } = this.state;
const { drizzleStatus, contracts } = this.props;
if (pageStatus === 'initialized'
&& drizzleStatus.initialized) {
this.dataKey = drizzle.contracts[contract].methods[getNumberOfTopicsMethod].cacheCall();
this.setState({
pageStatus: 'loading'
});
}
if (pageStatus === 'loading'
&& contracts[contract][getNumberOfTopicsMethod][this.dataKey]) {
this.setState({
pageStatus: 'loaded'
});
/* this.props.store.dispatch(hideProgressBar()); */
}
}
handleCreateTopicClick() {
const { history } = this.props;
history.push('/startTopic');
}
render() {
const { pageStatus } = this.state;
const { contracts, hasSignedUp } = this.props;
let boardContents;
if (pageStatus === 'loaded') {
const numberOfTopics = contracts[contract][getNumberOfTopicsMethod][this.dataKey].value;
if (numberOfTopics !== '0') {
this.topicIDs = [];
for (let i = 0; i < numberOfTopics; i++) {
this.topicIDs.push(i);
}
boardContents = ([
<TopicList topicIDs={this.topicIDs} key="topicList" />,
<div className="bottom-overlay-pad" key="pad" />,
hasSignedUp
&& (
<FloatingButton
onClick={this.handleCreateTopicClick}
key="createTopicButton"
/>
)
]);
} else if (!hasSignedUp) {
boardContents = (
<div className="vertical-center-in-parent">
<Header color="teal" textAlign="center" as="h2">
There are no topics yet!
</Header>
<Header color="teal" textAlign="center" as="h4">
Sign up to be the first to post.
</Header>
</div>
);
} else {
boardContents = (
<div className="vertical-center-in-parent">
<Header color="teal" textAlign="center" as="h2">
There are no topics yet!
</Header>
<Header color="teal" textAlign="center" as="h4">
Click the add button at the bottom of the page to be the first
to post.
</Header>
<FloatingButton
onClick={this.handleCreateTopicClick}
key="createTopicButton"
/>
</div>
);
}
}
return (
<div className="fill">
{boardContents}
</div>
);
}
}
BoardContainer.propTypes = {
drizzleStatus: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
contracts: PropTypes.objectOf(PropTypes.object).isRequired,
hasSignedUp: PropTypes.bool.isRequired
};
const mapStateToProps = state => ({
contracts: state.contracts,
drizzleStatus: state.drizzleStatus,
hasSignedUp: state.user.hasSignedUp
});
export default withRouter(connect(mapStateToProps)(BoardContainer));

45
app/src/containers/CoreLayoutContainer.js

@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import NavBarContainer from './NavBarContainer';
import RightSideBarContainer from './TransactionsMonitorContainer';
// 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/css/App.css';
import '../assets/css/sign-up-container.css';
import '../assets/css/board-container.css';
import '../assets/css/start-topic-container.css';
import '../assets/css/topic-container.css';
import '../assets/css/profile-container.css';
/* import TransactionsMonitorContainer from '../../containers/TransactionsMonitorContainer'; */
const CoreLayout = ({ children }) => (
<div className="App">
<NavBarContainer />
{/* <div className="progress-bar-container"
style={{display: this.props.isProgressBarVisible ? "block" : "none"}}>
<div className="progress">
<div className="indeterminate"></div>
</div>
</div> */}
<div className="page-container">
<aside className="left-side-panel" />
<div className="main-panel">
<div className="view-container">
{children}
</div>
</div>
<aside className="right-side-panel">
<RightSideBarContainer />
</aside>
</div>
</div>
);
CoreLayout.propTypes = {
children: PropTypes.objectOf(PropTypes.object)
};
export default CoreLayout;

26
app/src/containers/HomeContainer.js

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

107
app/src/containers/LoadingContainer.js

@ -0,0 +1,107 @@
import React, { Children, Component } from 'react';
import { connect } from 'react-redux';
import ipfs_logo from '../assets/images/ipfs_logo.png';
//TODO: Add OrbitDB Loading thingy
class LoadingContainer extends Component {
render() {
if (this.props.web3.status === 'failed' || !this.props.web3.networkId) {
//TODO: wtf is this
if (this.props.errorComp)
return this.props.errorComp;
return (
<main className="container loading-screen">
<div className="pure-g">
<div className="pure-u-1-1">
<h1><span role="img" aria-label="Fox Face">🦊</span></h1>
<p><strong>This browser has no connection to the Ethereum network.</strong></p>
<p>
Please make sure that:
<ul>
<li>You use MetaMask or a dedicated Ethereum browser (e.g. Mist or Parity)</li>
<li>They are pointed to the correct network</li>
<li>Your account is unlocked and the app has the rights to access it</li>
</ul>
</p>
</div>
</div>
</main>
);
}
if (this.props.web3.status === 'initialized' && Object.keys(this.props.accounts).length === 0) {
return(
<main className="loading-screen">
<div>
<div>
<h1><span role="img" aria-label="Fox Face">🦊</span></h1>
<p><strong>We can't find any Ethereum accounts!</strong>.</p>
</div>
</div>
</main>
)
}
if (!this.props.contractInitialized) {
return(
<main className="loading-screen">
<div>
<div>
<h1><span role="img" aria-label="Gear"></span></h1>
<p><strong>Initializing contracts...</strong></p>
<p>If this takes too long please make sure they are deployed to the network.</p>
</div>
</div>
</main>
)
}
if (!this.props.ipfsInitialized) {
return(
<main className="loading-screen">
<div>
<div>
<img src={ipfs_logo} alt="ipfs_logo" height="50"/>
<p><strong>Initializing IPFS...</strong></p>
</div>
</div>
</main>
)
}
//TODO: wtf is this
if (this.props.drizzleStatus.initialized)
return Children.only(this.props.children);
//TODO: wtf is this
if (this.props.loadingComp)
return this.props.loadingComp;
return(
<main className="container loading-screen">
<div>
<div>
<h1><span role="img" aria-label="Gear"></span></h1>
<p>Loading dapp...</p>
</div>
</div>
</main>
)
}
}
const mapStateToProps = state => {
return {
accounts: state.accounts,
drizzleStatus: state.drizzleStatus,
web3: state.web3,
ipfsInitialized: state.orbit.ipfsInitialized,
contractInitialized: state.contracts.Forum.initialized
}
};
export default connect(mapStateToProps)(LoadingContainer);

66
app/src/containers/NavBarContainer.js

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

221
app/src/containers/ProfileContainer.js

@ -0,0 +1,221 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { push } from 'connected-react-router';
import { connect } from 'react-redux';
import { Tab } from 'semantic-ui-react';
import { drizzle } from '../index';
import ProfileInformation from '../components/ProfileInformation';
import TopicList from '../components/TopicList';
import PostList from '../components/PostList';
import LoadingSpinner from '../components/LoadingSpinner';
import { setNavBarTitle } from '../redux/actions/userInterfaceActions';
const callsInfo = [
{
contract: 'Forum',
method: 'getUsername'
}, {
contract: 'Forum',
method: 'getUserTopics'
}, {
contract: 'Forum',
method: 'getUserPosts'
}
];
class ProfileContainer extends Component {
constructor(props) {
super(props);
const { match, user } = this.props;
this.getBlockchainData = this.getBlockchainData.bind(this);
this.dataKey = [];
const address = match.params.address
? match.params.address
: user.address;
this.state = {
pageStatus: 'initialized',
userAddress: address,
username: '',
topicIDs: null,
postIDs: null
};
}
componentDidMount() {
this.getBlockchainData();
}
componentDidUpdate() {
this.getBlockchainData();
}
componentWillUnmount() {
const { setNavBarTitle } = this.props;
setNavBarTitle('');
}
getBlockchainData() {
const { userAddress, pageStatus, username, topicIDs, postIDs } = this.state;
const { drizzleStatus, setNavBarTitle, contracts } = this.props;
if (pageStatus === 'initialized'
&& drizzleStatus.initialized) {
callsInfo.forEach((call, index) => {
this.dataKey[index] = drizzle.contracts[call.contract].methods[call.method].cacheCall(
userAddress,
);
});
this.setState({
pageStatus: 'loading'
});
}
if (pageStatus === 'loading') {
let pageStatusUpdate = 'loaded';
callsInfo.forEach((call, index) => {
if (!contracts[call.contract][call.method][this.dataKey[index]]) {
pageStatusUpdate = 'loading';
}
});
if (pageStatusUpdate === 'loaded') {
this.setState({
pageStatus: pageStatusUpdate
});
}
}
if (pageStatus === 'loaded') {
if (username === '') {
const transaction = contracts[callsInfo[0].contract][callsInfo[0].method][this.dataKey[0]];
if (transaction) {
const username = transaction.value;
setNavBarTitle(username);
this.setState({
username
});
}
}
if (topicIDs === null) {
const transaction = contracts[callsInfo[1].contract][callsInfo[1].method][this.dataKey[1]];
if (transaction) {
this.setState({
topicIDs: transaction.value
});
}
}
if (postIDs === null) {
const transaction = contracts[callsInfo[2].contract][callsInfo[2].method][this.dataKey[2]];
if (transaction) {
this.setState({
postIDs: transaction.value
});
}
}
/* this.props.store.dispatch(hideProgressBar()); */
}
}
render() {
const { userAddress, username, topicIDs, postIDs } = this.state;
const { navigateTo, user } = this.props;
if (!user.hasSignedUp) {
navigateTo('/signup');
return (null);
}
const infoTab = (
<ProfileInformation
address={userAddress}
username={username}
numberOfTopics={topicIDs && topicIDs.length}
numberOfPosts={postIDs && postIDs.length}
self={userAddress === user.address}
key="profileInfo"
/>
);
const topicsTab = (
<div className="profile-tab">
{topicIDs
? <TopicList topicIDs={topicIDs} />
: <LoadingSpinner />
}
</div>
);
const postsTab = (
<div className="profile-tab">
{postIDs
? <PostList postIDs={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>
);
}
}
ProfileContainer.propTypes = {
match: PropTypes.object.isRequired,
drizzleStatus: PropTypes.object.isRequired,
contracts: PropTypes.array.isRequired,
navigateTo: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
setNavBarTitle: PropTypes.func.isRequired
};
const mapDispatchToProps = dispatch => bindActionCreators({
navigateTo: location => push(location),
setNavBarTitle: navBarTitle => setNavBarTitle(navBarTitle)
}, dispatch);
const mapStateToProps = state => ({
user: state.user,
drizzleStatus: state.drizzleStatus,
contracts: state.contracts,
orbitDB: state.orbitDB
});
export default connect(mapStateToProps, mapDispatchToProps)(ProfileContainer);

55
app/src/containers/SignUpContainer.js

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

167
app/src/containers/StartTopicContainer.js

@ -0,0 +1,167 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button, Form, Icon, TextArea } from 'semantic-ui-react';
import NewTopicPreview from '../components/NewTopicPreview';
import { createTopic } from '../redux/actions/transactionsActions';
class StartTopicContainer extends Component {
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
this.handlePreviewToggle = this.handlePreviewToggle.bind(this);
this.validateAndPost = this.validateAndPost.bind(this);
this.state = {
topicSubjectInput: '',
topicMessageInput: '',
topicSubjectInputEmptySubmit: false,
topicMessageInputEmptySubmit: false,
previewEnabled: false,
previewDate: ''
};
}
async validateAndPost() {
const { topicSubjectInput, topicMessageInput } = this.state;
const { dispatch, history } = this.props;
if (topicSubjectInput === '' || topicMessageInput
=== '') {
this.setState({
topicSubjectInputEmptySubmit: topicSubjectInput === '',
topicMessageInputEmptySubmit: topicMessageInput === ''
});
return;
}
dispatch(
createTopic(
{
topicSubject: topicSubjectInput,
topicMessage: topicMessageInput
},
),
);
history.push('/home');
}
handleInputChange(event) {
this.setState({
[event.target.name]: event.target.value
});
}
handlePreviewToggle() {
this.setState((prevState) => ({
previewEnabled: !prevState.previewEnabled,
previewDate: this.getDate()
}));
}
getDate() {
const currentdate = new Date();
return (`${currentdate.getMonth() + 1} ${
currentdate.getDate()}, ${
currentdate.getFullYear()}, ${
currentdate.getHours()}:${
currentdate.getMinutes()}:${
currentdate.getSeconds()}`);
}
render() {
const {
previewDate, previewEnabled, topicSubjectInputEmptySubmit, topicSubjectInput,
topicMessageInputEmptySubmit, topicMessageInput
} = this.state;
const { user, history } = this.props;
if (!user.hasSignedUp) {
history.push('/signup');
return (null);
}
const previewEditText = previewEnabled ? 'Edit' : 'Preview';
return (
<div>
{previewEnabled
&& (
<NewTopicPreview
date={previewDate}
subject={topicSubjectInput}
content={topicMessageInput}
/>
)
}
<Form>
{!previewEnabled
&& [
<Form.Field key="topicSubjectInput">
<Form.Input
name="topicSubjectInput"
error={topicSubjectInputEmptySubmit}
type="text"
value={topicSubjectInput}
placeholder="Subject"
id="topicSubjectInput"
onChange={this.handleInputChange}
/>
</Form.Field>,
<TextArea
key="topicMessageInput"
name="topicMessageInput"
className={topicMessageInputEmptySubmit
? 'form-textarea-required'
: ''}
value={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>
);
}
}
StartTopicContainer.propTypes = {
dispatch: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
user: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
orbitDB: state.orbitDB,
user: state.user
});
export default connect(mapStateToProps)(StartTopicContainer);

212
app/src/containers/TopicContainer.js

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

153
app/src/containers/TransactionsMonitorContainer.js

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

216
app/src/containers/UsernameFormContainer.js

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

12
app/src/helpers/EpochTimeConverter.js

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

33
app/src/index.js

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

1
app/src/redux/actions/drizzleUtilsActions.js

@ -0,0 +1 @@
export const DRIZZLE_UTILS_SAGA_INITIALIZED = 'DRIZZLE_UTILS_SAGA_INITIALIZED';

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

@ -0,0 +1,37 @@
const IPFS_INITIALIZED = 'IPFS_INITIALIZED';
const DATABASES_CREATED = 'DATABASES_CREATED';
const DATABASES_LOADED = 'DATABASES_LOADED';
const DATABASES_NOT_READY = 'DATABASES_NOT_READY';
const ADD_PEER_DATABASE = 'ADD_PEER_DATABASE';
const OPENING_PEER_DATABASE = 'OPENING_PEER_DATABASE';
const PEER_DATABASE_LOADED = 'PEER_DATABASE_LOADED';
const UPDATE_PEERS = 'UPDATE_PEERS';
function updateDatabases(type, orbitdb, topicsDB, postsDB) {
return {
type,
orbitdb,
topicsDB,
postsDB,
id: orbitdb.id
};
}
function addPeerDatabase(fullAddress) {
return {
type: ADD_PEER_DATABASE,
fullAddress
};
}
export { DATABASES_CREATED,
DATABASES_LOADED,
DATABASES_NOT_READY,
IPFS_INITIALIZED,
UPDATE_PEERS,
ADD_PEER_DATABASE,
OPENING_PEER_DATABASE,
PEER_DATABASE_LOADED,
addPeerDatabase,
updateDatabases
};

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

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

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

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

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

@ -0,0 +1,82 @@
import {
DATABASES_CREATED,
DATABASES_LOADED,
DATABASES_NOT_READY, OPENING_PEER_DATABASE,
IPFS_INITIALIZED, UPDATE_PEERS, PEER_DATABASE_LOADED
} from '../actions/orbitActions';
const initialState = {
ipfs: null,
ipfsInitialized: false,
ready: false,
orbitdb: null,
topicsDB: null,
postsDB: null,
pubsubPeers: {topicsDBPeers:[], postsDBPeers:[]},
replicatedDatabases: [],
id: null
};
const orbitReducer = (state = initialState, action) => {
switch (action.type) {
case IPFS_INITIALIZED:
return {
...state,
ipfs: action.ipfs,
ipfsInitialized: true
};
case DATABASES_CREATED:
return {
...state,
ready: true,
orbitdb: action.orbitdb,
topicsDB: action.topicsDB,
postsDB: action.postsDB,
id: action.id
};
case DATABASES_LOADED:
return {
...state,
ready: true,
orbitdb: action.orbitdb,
topicsDB: action.topicsDB,
postsDB: action.postsDB,
id: action.id
};
case DATABASES_NOT_READY:
return {
...state,
ready: false,
orbitdb: null,
topicsDB: null,
postsDB: null,
id: null
};
case OPENING_PEER_DATABASE:
if(state.replicatedDatabases.find(db => db.fullAddress === action.fullAddress))
return state;
return {
...state,
replicatedDatabases:[...state.replicatedDatabases,
{fullAddress: action.fullAddress, ready: false, store: null}]
};
case PEER_DATABASE_LOADED:
return {
...state,
replicatedDatabases: [...state.replicatedDatabases.map((db) => {
if (db.fullAddress !== action.fullAddress)
return db; // This isn't the item we care about - keep it as-is
return { ...db, ready: true, store: action.store} // Otherwise return an updated value
})]
};
case UPDATE_PEERS:
return {
...state,
pubsubPeers: {topicsDBPeers:action.topicsDBPeers, postsDBPeers:action.postsDBPeers}
};
default:
return state;
}
};
export default orbitReducer;

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

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

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

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

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

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

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

@ -0,0 +1,37 @@
import { call, put, select, takeLatest } from 'redux-saga/effects';
import { getContractInstance, getWeb3 } from '../../utils/drizzleUtils';
import Forum from '../../contracts/Forum';
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions';
const accounts = state => state.accounts;
let initFlag, web3, forumContract;
function* init() {
if (!initFlag) {
web3 = yield call(getWeb3);
forumContract = yield call(getContractInstance, {
web3, artifact: Forum
});
initFlag = true;
yield put({
type: DRIZZLE_UTILS_SAGA_INITIALIZED, ...[]
});
}
else console.warn('Attempted to reinitialize drizzleUtilsSaga!');
}
// 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
// with (yield call(getAccounts, {web3}))[0];
function* getCurrentAccount() {
return (yield select(accounts))[0];
}
function* drizzleUtilsSaga() {
yield takeLatest('DRIZZLE_INITIALIZED', init);
}
export { web3, forumContract, getCurrentAccount };
export default drizzleUtilsSaga;

22
app/src/redux/sagas/eventSaga.js

@ -0,0 +1,22 @@
import { put, takeEvery } from 'redux-saga/effects';
const EVENT_FIRED = 'EVENT_FIRED'; // This is fired internally by drizzle
const CONTRACT_EVENT_FIRED = 'CONTRACT_EVENT_FIRED';
let eventSet = new Set();
// Entire purpose of this saga is to bypass a strange bug where EVENT_FIRED is called
// multiple times for the same event
function* sanitizeEvent(action) {
const size = eventSet.size;
eventSet.add(action.event.transactionHash + action.name);
if(eventSet.size>size)
yield put({ ...action, type: CONTRACT_EVENT_FIRED });
}
function* eventSaga() {
yield takeEvery(EVENT_FIRED, sanitizeEvent);
}
export default eventSaga;
export { CONTRACT_EVENT_FIRED }

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

@ -0,0 +1,106 @@
import { all, call, put, select, take, takeEvery, takeLatest } from 'redux-saga/effects';
import isEqual from 'lodash.isequal';
import { forumContract, getCurrentAccount } from './drizzleUtilsSaga';
import { loadDatabases, orbitSagaOpen } from '../../utils/orbitUtils';
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions';
import {
ADD_PEER_DATABASE, PEER_DATABASE_LOADED,
DATABASES_NOT_READY,
IPFS_INITIALIZED, OPENING_PEER_DATABASE,
UPDATE_PEERS
} from '../actions/orbitActions';
let latestAccount;
function* getOrbitDBInfo() {
yield put({
type: 'ORRBIT_GETTING_INFO', ...[]
});
const account = yield call(getCurrentAccount);
if (account !== latestAccount) {
const txObj1 = yield call(forumContract.methods.hasUserSignedUp,
...[account]);
try {
const callResult = yield call(txObj1.call, {
address: account
});
if (callResult) {
const txObj2 = yield call(forumContract.methods.getOrbitIdentityInfo,
...[account]);
const orbitIdentityInfo = yield call(txObj2.call, {
address: account
});
const txObj3 = yield call(forumContract.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;
} catch (error) {
console.error(error);
yield put({
type: 'ORBIT_SAGA_ERROR', ...[]
});
}
}
}
let peerOrbitAddresses = new Set();
function* addPeerDatabase(action) {
const fullAddress = action.fullAddress;
const size = peerOrbitAddresses.size;
peerOrbitAddresses.add(fullAddress);
if(peerOrbitAddresses.size>size){
const { orbitdb } = yield select(state => state.orbit);
if(orbitdb){
yield put.resolve({
type: OPENING_PEER_DATABASE, fullAddress
});
const store = yield call(orbitSagaOpen, orbitdb, fullAddress);
yield put({
type: PEER_DATABASE_LOADED, fullAddress, store: store
});
}
}
}
//Keeps track of connected pubsub peers in Redux store
function* updatePeersState() {
const orbit = yield select(state => state.orbit);
if(orbit.ready){
const topicsDBAddress = orbit.topicsDB.address.toString();
const postsDBAddress = orbit.postsDB.address.toString();
const topicsDBPeers = yield call(orbit.ipfs.pubsub.peers, topicsDBAddress);
const postsDBPeers = yield call(orbit.ipfs.pubsub.peers, postsDBAddress);
if(!isEqual(topicsDBPeers.sort(), orbit.pubsubPeers.topicsDBPeers.sort()) ||
!isEqual(postsDBPeers.sort(), orbit.pubsubPeers.postsDBPeers.sort())){
yield put({
type: UPDATE_PEERS, topicsDBPeers, postsDBPeers
});
}
}
}
function* orbitSaga() {
yield all([
take(DRIZZLE_UTILS_SAGA_INITIALIZED),
take(IPFS_INITIALIZED)
]);
yield takeLatest('ACCOUNT_CHANGED', getOrbitDBInfo);
yield takeEvery(ADD_PEER_DATABASE, addPeerDatabase);
yield takeEvery('ACCOUNTS_FETCHED', updatePeersState);
}
export default orbitSaga;

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

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

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

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

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

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

35
app/src/redux/store.js

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

27
app/src/router/PrivateRoute.js

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

31
app/src/router/routes.js

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

86
app/src/utils/drizzleUtils.js

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

109
app/src/utils/orbitUtils.js

@ -0,0 +1,109 @@
import OrbitDB from 'orbit-db';
import Keystore from 'orbit-db-keystore';
import path from 'path';
import IPFS from 'ipfs';
import store from '../redux/store';
import { DATABASES_CREATED, DATABASES_LOADED, IPFS_INITIALIZED, updateDatabases } from '../redux/actions/orbitActions';
import ipfsOptions from '../config/ipfsOptions';
function initIPFS() {
const ipfs = new IPFS(ipfsOptions);
ipfs.on('error', (error) => console.error(`IPFS error: ${error}`));
ipfs.on('ready', async () => {
store.dispatch({
type: IPFS_INITIALIZED, ipfs
});
console.debug('IPFS initialized.');
});
}
async function createDatabases() {
console.debug("Deleting local storage..."); // Else we are in danger of reusing an existing orbit
localStorage.clear(); // Perhaps not needed at all when orbit ids are used in Orbit 0.20.x+
console.debug('Creating databases...');
const ipfs = getIPFS();
const orbitdb = await new OrbitDB(ipfs);
const topicsDB = await orbitdb.keyvalue('topics');
const postsDB = await orbitdb.keyvalue('posts');
store.dispatch(
updateDatabases(DATABASES_CREATED, orbitdb, topicsDB, postsDB),
);
const orbitKey = orbitdb.keystore.getKey(orbitdb.id);
return {
identityId: 'Tempus',
identityPublicKey: 'edax',
identityPrivateKey: 'rerum',
orbitId: orbitdb.id,
orbitPublicKey: orbitKey.getPublic('hex'),
orbitPrivateKey: orbitKey.getPrivate('hex'),
topicsDB: topicsDB.address.root,
postsDB: postsDB.address.root
};
}
async function loadDatabases(identityId, identityPublicKey, identityPrivateKey,
orbitId, orbitPublicKey, orbitPrivateKey,
topicsDBId, postsDBId) {
const directory = './orbitdb';
const keystore = Keystore.create(path.join(directory, orbitId, '/keystore'));
keystore._storage.setItem(orbitId, JSON.stringify({
publicKey: orbitPublicKey,
privateKey: orbitPrivateKey
}));
const ipfs = getIPFS();
const orbitdb = await new OrbitDB(ipfs, directory,
{
peerId: orbitId, keystore
});
const topicsDB = await orbitdb.keyvalue(`/orbitdb/${topicsDBId}/topics`)
.catch((error) => console.error(`TopicsDB init error: ${error}`));
const postsDB = await orbitdb.keyvalue(`/orbitdb/${postsDBId}/posts`)
.catch((error) => console.error(`PostsDB init error: ${error}`));
await topicsDB.load().catch((error) => console.error(`TopicsDB loading error: ${error}`));
await postsDB.load().catch((error) => console.error(`PostsDB loading error: ${error}`));
//It is possible that we lack our own data and need to replicate them from somewhere else
topicsDB.events.on('replicate', (address) => {
console.log(`TopicsDB Replicating (${address}).`);
});
topicsDB.events.on('replicated', (address) => {
console.log(`TopicsDB replicated (${address}).`);
});
postsDB.events.on('replicate', (address) => {
console.log(`PostsDB replicating (${address}).`);
});
postsDB.events.on('replicated', (address) => {
console.log(`PostsDB replicated (${address}).`);
});
console.debug('Orbit databases loaded successfully.');
store.dispatch(updateDatabases(DATABASES_LOADED, orbitdb, topicsDB, postsDB));
}
function getIPFS() {
return store.getState().orbit.ipfs;
}
async function orbitSagaPut(db, key, value) {
await db.put(key, value).catch((error) => console.error(`Orbit put error: ${error}`));
}
async function orbitSagaOpen(orbitdb, address) {
const store = await orbitdb.keyvalue(address)
.catch((error) => console.error(`Error opening a peer's db: ${error}`));
await store.load().catch((error) => console.log(error));
store.events.on('replicate', (address) => {
console.log(`A peer's DB is being replicated (${address}).`);
});
store.events.on('replicated', (address) => {
console.log(`A peer's DB was replicated (${address}).`);
});
return store;
}
export { initIPFS, createDatabases, loadDatabases, orbitSagaPut, orbitSagaOpen };

130
app/src/utils/serviceWorker.js

@ -0,0 +1,130 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read http://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost'
// [::1] is the IPv6 localhost address.
|| window.location.hostname === '[::1]'
// 127.0.0.1/8 is considered localhost for IPv4.
|| window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
),
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service '
+ 'worker. To learn more, visit http://bit.ly/CRA-PWA',
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all '
+ 'tabs for this page are closed. See http://bit.ly/CRA-PWA.',
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
}).catch((error) => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl).then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404
|| (contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
}).catch(() => {
console.log(
'No internet connection found. App is running in offline mode.',
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}

113
contracts/Forum.sol

@ -1,4 +1,4 @@
pragma solidity ^0.4.25;
pragma solidity >=0.5.6 <0.6.0;
contract Forum {
@ -18,18 +18,22 @@ contract Forum {
event UserSignedUp(string username, address userAddress);
event UsernameUpdated(string newName, string oldName,address userAddress);
function signUp(string username, string orbitDBId, string orbitTopicsDB, string orbitPostsDB, string orbitPublicKey, string orbitPrivateKey) public returns (bool) {
function signUp(string memory username, string memory orbitIdentityId,
string memory orbitIdentityPublicKey, string memory orbitIdentityPrivateKey,
string memory orbitId, string memory orbitPublicKey, string memory orbitPrivateKey,
string memory orbitTopicsDB, string memory orbitPostsDB) public returns (bool) {
require (!hasUserSignedUp(msg.sender), "User has already signed up.");
require(!isUserNameTaken(username), "Username is already taken.");
users[msg.sender] = User(username,
OrbitDB(orbitDBId,orbitTopicsDB, orbitPostsDB, orbitPublicKey, orbitPrivateKey),
OrbitDB(orbitIdentityId, orbitIdentityPublicKey, orbitIdentityPrivateKey,
orbitId, orbitPublicKey, orbitPrivateKey, orbitTopicsDB, orbitPostsDB),
new uint[](0), new uint[](0), block.timestamp, true);
userAddresses[username] = msg.sender;
emit UserSignedUp(username, msg.sender);
return true;
}
function updateUsername(string newUsername) public returns (bool) {
function updateUsername(string memory newUsername) public returns (bool) {
require (hasUserSignedUp(msg.sender), "User hasn't signed up yet.");
require(!isUserNameTaken(newUsername), "Username is already taken.");
string memory oldUsername = getUsername(msg.sender);
@ -40,12 +44,12 @@ contract Forum {
return true;
}
function getUsername(address userAddress) public view returns (string) {
function getUsername(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
return users[userAddress].username;
}
function getUserAddress(string username) public view returns (address) {
function getUserAddress(string memory username) public view returns (address) {
return userAddresses[username];
}
@ -53,18 +57,18 @@ contract Forum {
return users[userAddress].signedUp;
}
function isUserNameTaken(string username) public view returns (bool) {
function isUserNameTaken(string memory username) public view returns (bool) {
if (getUserAddress(username)!=address(0))
return true;
return false;
}
function getUserTopics(address userAddress) public view returns (uint[]) {
function getUserTopics(address userAddress) public view returns (uint[] memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
return users[userAddress].topicIDs;
}
function getUserPosts(address userAddress) public view returns (uint[]) {
function getUserPosts(address userAddress) public view returns (uint[] memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
return users[userAddress].postIDs;
}
@ -75,52 +79,81 @@ contract Forum {
}
//----------------------------------------OrbitDB----------------------------------------
// TODO: set upper bounds to strings (instead of being of arbitrary length)
// TODO: not sure if topicsDB//postsDB are actually needed
struct OrbitDB {
string id; // TODO: set an upper bound instead of arbitrary string
string topicsDB; //TODO: not sure yet which of these are actually needed
string identityId;
string identityPublicKey;
string identityPrivateKey;
string orbitId;
string orbitPublicKey;
string orbitPrivateKey;
string topicsDB;
string postsDB;
string publicKey;
string privateKey;
}
function getOrbitIdentityId(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.identityId;
}
function getOrbitDBId(address userAddress) public view returns (string) {
function getOrbitIdentityPublicKey(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.id;
return users[userAddress].orbitdb.identityPublicKey;
}
function getOrbitTopicsDB(address userAddress) public view returns (string) {
function getOrbitIdentityPrivateKey(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.topicsDB;
return users[userAddress].orbitdb.identityPrivateKey;
}
function getOrbitPostsDB(address userAddress) public view returns (string) {
function getOrbitDBId(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.postsDB;
return users[userAddress].orbitdb.orbitId;
}
function getOrbitPublicKey(address userAddress) public view returns (string) {
function getOrbitPublicKey(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.publicKey;
return users[userAddress].orbitdb.orbitPublicKey;
}
//TODO: encrypt using Metamask in the future
function getOrbitPrivateKey(address userAddress) public view returns (string) {
function getOrbitPrivateKey(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.privateKey;
return users[userAddress].orbitdb.orbitPrivateKey;
}
function getOrbitTopicsDB(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.topicsDB;
}
function getOrbitDBInfo(address userAddress) public view returns (string, string, string, string, string) {
function getOrbitPostsDB(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.postsDB;
}
function getOrbitIdentityInfo(address userAddress) public view returns (string memory, string memory, string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return (
users[userAddress].orbitdb.id,
users[userAddress].orbitdb.topicsDB,
users[userAddress].orbitdb.postsDB,
users[userAddress].orbitdb.publicKey,
users[userAddress].orbitdb.privateKey
users[userAddress].orbitdb.identityId,
users[userAddress].orbitdb.identityPublicKey,
users[userAddress].orbitdb.identityPrivateKey
);
}
function getOrbitDBInfo(address userAddress) public view returns (string memory, string memory,
string memory, string memory, string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return (
users[userAddress].orbitdb.orbitId,
users[userAddress].orbitdb.orbitPublicKey,
users[userAddress].orbitdb.orbitPrivateKey,
users[userAddress].orbitdb.topicsDB,
users[userAddress].orbitdb.postsDB
);
}
//----------------------------------------POSTING----------------------------------------
struct Topic {
@ -178,30 +211,30 @@ contract Forum {
return numTopics;
}
function getTopic(uint topicID) public view returns (string, address, string, uint, uint[]) {
function getTopic(uint topicID) public view returns (string memory, address, string memory, uint, uint[] memory) {
//require(hasUserSignedUp(msg.sender)); needed?
require(topicID<numTopics);
return (getOrbitTopicsDB(topics[topicID].author),
topics[topicID].author,
users[topics[topicID].author].username,
topics[topicID].timestamp,
topics[topicID].postIDs
topics[topicID].author,
users[topics[topicID].author].username,
topics[topicID].timestamp,
topics[topicID].postIDs
);
}
function getTopicPosts(uint topicID) public view returns (uint[]) {
function getTopicPosts(uint topicID) public view returns (uint[] memory) {
require(topicID<numTopics); // Topic should exist
return topics[topicID].postIDs;
}
function getPost(uint postID) public view returns (string, address, string, uint, uint) {
function getPost(uint postID) public view returns (string memory, address, string memory, uint, uint) {
//require(hasUserSignedUp(msg.sender)); needed?
require(postID<numPosts);
return (getOrbitPostsDB(posts[postID].author),
posts[postID].author,
users[posts[postID].author].username,
posts[postID].timestamp,
posts[postID].topicID
posts[postID].author,
users[posts[postID].author].username,
posts[postID].timestamp,
posts[postID].topicID
);
}
}

12
contracts/Migrations.sol

@ -1,17 +1,17 @@
pragma solidity ^0.4.25;
pragma solidity >=0.5.6 <0.6.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) _;
}
constructor() public {
owner = msg.sender;
}
modifier restricted() {
if (msg.sender == owner) _;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
@ -20,4 +20,4 @@ contract Migrations {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}
}

81
contracts/Posting.sol

@ -0,0 +1,81 @@
pragma solidity >=0.5.6 <0.6.0;
contract Posting {
address forumContractAddress;
function setForumContractAddress() public{
require(forumContractAddress==address(0));
forumContractAddress = msg.sender;
}
struct Topic {
uint topicID;
address author;
uint timestamp;
uint[] postIDs;
}
struct Post {
uint postID;
address author;
uint timestamp;
uint topicID;
}
uint numTopics; // Total number of topics
uint numPosts; // Total number of posts
mapping (uint => Topic) topics;
mapping (uint => Post) posts;
function createTopic(address author) public returns (uint, uint) {
require(msg.sender==forumContractAddress);
//Creates topic
uint topicID = numTopics++;
topics[topicID] = Topic(topicID, author, block.timestamp, new uint[](0));
//Adds first post to topic
uint postID = numPosts++;
posts[postID] = Post(postID, author, block.timestamp, topicID);
topics[topicID].postIDs.push(postID);
return (topicID, postID);
}
function createPost(uint topicID, address author) public returns (uint) {
require(msg.sender==forumContractAddress);
require(topicID<numTopics); // Only allow posting to a topic that exists
uint postID = numPosts++;
posts[postID] = Post(postID, author, block.timestamp, topicID);
topics[topicID].postIDs.push(postID);
return postID;
}
function getNumberOfTopics() public view returns (uint) {
return numTopics;
}
function getTopicInfo(uint topicID) public view returns (address, uint, uint[] memory) {
require(topicID<numTopics);
return (topics[topicID].author,
topics[topicID].timestamp,
topics[topicID].postIDs
);
}
function getTopicPosts(uint topicID) public view returns (uint[] memory) {
require(topicID<numTopics); // Topic should exist
return topics[topicID].postIDs;
}
function getPostInfo(uint postID) public view returns (address, uint, uint) {
require(postID<numPosts);
return (
posts[postID].author,
posts[postID].timestamp,
posts[postID].topicID
);
}
}

2
migrations/1_initial_migration.js

@ -1,4 +1,4 @@
var Migrations = artifacts.require("./Migrations.sol");
const Migrations = artifacts.require("./Migrations.sol");
module.exports = function(deployer) {
deployer.deploy(Migrations);

4
migrations/2_deploy_contracts.js

@ -1,5 +1,5 @@
var Forum = artifacts.require("Forum");
const Forum = artifacts.require("Forum");
module.exports = function(deployer) {
deployer.deploy(Forum);
};
};

43
package.json

@ -1,40 +1,27 @@
{
"name": "apella",
"name": "apella-box",
"version": "0.1.0",
"description": "",
"private": true,
"repository": {
"type": "git",
"url": "https://gitlab.com/Ezerous/Apella.git"
},
"main": "truffle-config.js",
"directories": {
"test": "test"
},
"dependencies": {
"drizzle": "^1.1.5",
"drizzle-react": "^1.1.1",
"drizzle-react-components": "^1.1.0",
"eth-block-tracker-es5": "^2.3.2",
"ipfs": "^0.30.0",
"orbit-db": "^0.19.9",
"orbit-db-keystore": "^0.1.0",
"prop-types": "^15.6.1",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-markdown": "^3.3.2",
"react-redux": "^5.0.7",
"react-router": "^3.2.1",
"react-router-dom": "^4.2.2",
"react-router-redux": "^4.0.8",
"react-scripts": "^1.1.4",
"react-timeago": "^4.1.9",
"react-user-avatar": "^1.10.0",
"redux": "^3.7.2",
"redux-saga": "0.16.0",
"semantic-ui-react": "^0.81.1",
"uuid": "^3.2.1",
"web3": "^1.0.0-beta.34"
"openzeppelin-solidity": "^2.2.0"
},
"devDependencies": {
"eslint": "5.12.0",
"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"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
"lint": "eslint app/src --format table"
}
}

44
public/index.html

@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<!-- Import Google Icon Font -->
<!-- <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> -->
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css"></link>
<title>Apella</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

186
src/assets/css/App.css

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

30
src/assets/css/board-container.css

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

4
src/assets/css/index.css

@ -1,4 +0,0 @@
body {
margin: 0;
padding: 0;
}

17
src/assets/css/loading-container.css

@ -1,17 +0,0 @@
/* LOADING SCREEN */
.loading-screen {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
opacity: 1;
visibility: visible;
transition: all .25s ease-in-out;
}
.loading-screen.loaded {
opacity: 0;
visibility: hidden;
}

108
src/assets/css/progress-bar.css

@ -1,108 +0,0 @@
/* Progress Bar */
.progress-bar-container {
position: absolute;
top: 54px;
left: 0px;
width: 100%;
}
.progress {
position: relative;
height: 4px;
display: block;
width: 100%;
background-color: #acece6;
border-radius: 2px;
background-clip: padding-box;
margin: 0.5rem 0 1rem 0;
overflow: hidden;
}
.progress .indeterminate {
background-color: #00b5ad;
}
.progress .indeterminate:before {
content: '';
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
}
.progress .indeterminate:after {
content: '';
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
-webkit-animation-delay: 1.15s;
animation-delay: 1.15s;
}
@-webkit-keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@-webkit-keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}
@keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}

12
src/assets/css/sign-up-container.css

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

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

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

51
src/assets/css/topic-container.css

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

3271
src/assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js

File diff suppressed because one or more lines are too long

14
src/components/FloatingButton.js

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

16
src/components/LoadingSpinner.js

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

63
src/components/NavBar.js

@ -1,63 +0,0 @@
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import { Image, Menu } from 'semantic-ui-react'
class NavBar extends Component {
constructor(props){
super(props);
this.handleItemClick = this.handleItemClick.bind(this);
this.navRef = React.createRef();
}
handleItemClick(to) {
this.context.router.push(to);
}
render() {
return (
<Menu fixed='top' inverted>
<Menu.Item header onClick={() => {this.handleItemClick("/")}}>
<Image
size='mini'
src={require('../resources/logo.png')}
style={{ marginRight: '1.5em' }}
/>
Apella
</Menu.Item>
<Menu.Item onClick={() => {this.handleItemClick("/")}}>
Home
</Menu.Item>
{this.props.hasSignedUp
? <Menu.Item onClick={() => {this.handleItemClick("/profile")}}>
Profile
</Menu.Item>
:<Menu.Menu position='right' style={{backgroundColor: '#00b5ad'}}>
<Menu.Item onClick={() => {this.handleItemClick("/signup")}}>
Sign Up
</Menu.Item>
</Menu.Menu>
}
<div className="navBarText">
{this.props.navBarTitle !== '' && <span>{this.props.navBarTitle}</span>}
</div>
</Menu>
);
}
};
NavBar.contextTypes = {
router: PropTypes.object
};
const mapStateToProps = state => {
return {
hasSignedUp: state.user.hasSignedUp,
navBarTitle: state.interface.navBarTitle
}
};
export default drizzleConnect(NavBar, mapStateToProps);

175
src/components/NewPost.js

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

65
src/components/NewTopicPreview.js

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

11
src/components/NotFoundView.js

@ -1,11 +0,0 @@
import React from 'react';
const NotFoundView = (props) => {
return (
<div style={{textAlign: "center"}}>
<img src={require('../resources/PageNotFound.jpg')} alt="Page not found!"/>
</div>
);
};
export default NotFoundView;

173
src/components/Post.js

@ -1,173 +0,0 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router';
import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import { Transition } from 'semantic-ui-react'
import { Grid, Divider, Button, Icon, Label } from 'semantic-ui-react'
import TimeAgo from 'react-timeago';
import epochTimeConverter from '../helpers/EpochTimeConverter';
import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown';
class Post extends Component {
constructor(props, context) {
super(props);
this.fetchPost = this.fetchPost.bind(this);
if (props.getFocus){
this.postRef = React.createRef();
}
this.orbitPostData = {
content: "",
subject: ""
};
this.orbitPostDataFetchStatus = "pending";
this.state = {
animateOnToggle: true
}
}
async fetchPost(postID) {
this.orbitPostDataFetchStatus = "fetching";
if (this.props.blockchainData[0].returnData[1] === this.props.user.address) {
this.orbitPostData = this.props.orbitDB.postsDB.get(postID);
} else {
const fullAddress = "/orbitdb/" + this.props.blockchainData[0].returnData[0] + "/posts";
const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress);
await store.load();
let localOrbitData = store.get(postID);
if (localOrbitData) {
this.orbitPostData = localOrbitData;
} else {
// Wait until we have received something from the network
store.events.on('replicated', () => {
this.orbitPostData = store.get(postID);
})
}
}
this.orbitPostDataFetchStatus = "fetched";
this.readyForAnimation = true;
}
render(){
let avatarView = (this.props.blockchainData[0].returnData
? <UserAvatar
size="52"
className="inline"
src={this.props.avatarUrl}
name={this.props.blockchainData[0].returnData[2]}/>
: <div></div>
);
return (
<Transition animation='tada' duration={500} visible={this.state.animateOnToggle}>
<div className="post" ref={this.postRef ? this.postRef : null}>
<Divider horizontal>
<span className="grey-text">#{this.props.postIndex}</span>
</Divider>
<Grid>
<Grid.Row columns={16} stretched>
<Grid.Column width={1} className="user-avatar">
{this.props.blockchainData[0].returnData !== null
?<Link to={"/profile/" + this.props.blockchainData[0].returnData[1]
+ "/" + this.props.blockchainData[0].returnData[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.blockchainData[0].returnData !== null ? "" : "grey-text"}>
<strong>
{this.props.blockchainData[0].returnData !== null
?this.props.blockchainData[0].returnData[2]
:"Username"
}
</strong>
</span>
<span className="grey-text">
{this.props.blockchainData[0].returnData !== null &&
<TimeAgo date={epochTimeConverter(this.props.blockchainData[0].returnData[3])}/>
}
</span>
</div>
<div className="stretch-space-between">
<span className={this.orbitPostData.subject ? "" : "grey-text"}>
<strong>
Subject: {this.orbitPostData.subject}
</strong>
</span>
</div>
<div className="post-content">
{this.orbitPostData.content
? <ReactMarkdown source={this.orbitPostData.content} />
: <p className="grey-text">Post content...</p>
}
</div>
</div>
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column floated="right" textAlign="right">
<Button icon size='mini' style={{marginRight: "0px"}}>
<Icon name='chevron up' />
</Button>
<Label color="teal">8000</Label>
<Button icon size='mini'>
<Icon name='chevron down' />
</Button>
<Button icon size='mini'
onClick={this.props.blockchainData[0].returnData
? () => { this.context.router.push("/topic/"
+ this.props.blockchainData[0].returnData[4] + "/"
+ this.props.postID)}
: () => {}}>
<Icon name='linkify' />
</Button>
</Grid.Column>
</Grid.Row>
</Grid>
</div>
</Transition>
);
}
componentDidUpdate() {
if (this.props.blockchainData[0].status === "success"
&& this.orbitPostDataFetchStatus === "pending") {
this.fetchPost(this.props.postID);
}
if (this.readyForAnimation){
if (this.postRef){
setTimeout(() => {
this.postRef.current.scrollIntoView({ block: 'start', behavior: 'smooth' });
setTimeout(() => {
this.setState({ animateOnToggle: false });
}, 300);
}, 100);
this.readyForAnimation = false;
}
}
}
};
Post.contextTypes = {
router: PropTypes.object
};
const mapStateToProps = state => {
return {
user: state.user,
orbitDB: state.orbitDB
}
};
export default drizzleConnect(withRouter(Post), mapStateToProps);

36
src/components/PostList.js

@ -1,36 +0,0 @@
import React from 'react';
import WithBlockchainData from './WithBlockchainData';
import Post from './Post';
const PostList = (props) => {
const posts = props.postIDs.map((postID, index) => {
return (
<WithBlockchainData
component={Post}
callsInfo={[{
contract: 'Forum',
method: 'getPost',
params: [postID]
}]}
avatarUrl={""}
postIndex={index}
postID={postID}
getFocus={props.focusOnPost === postID ? true : false}
key={postID}
/>
);
});
return (
<div>
{props.recentToTheTop
?posts.slice(0).reverse()
:posts
}
</div>
);
};
export default PostList;

59
src/components/ProfileInformation.js

@ -1,59 +0,0 @@
import React from 'react';
import UserAvatar from 'react-user-avatar';
import epochTimeConverter from '../helpers/EpochTimeConverter';
import UsernameFormContainer from '../containers/UsernameFormContainer';
const ProfileInformation = (props) => {
let transaction = props.blockchainData
.find(transaction => transaction.callInfo.method === "getUserDateOfRegister");
let dateOfRegister = transaction ? transaction.returnData : "";
transaction = props.blockchainData
.find(transaction => transaction.callInfo.method === "getOrbitDBId")
let orbitDBId = transaction ? transaction.returnData : "";
return (
<div className="user-info">
{props.avatarUrl && <UserAvatar
size="40"
className="inline user-avatar"
src={props.avatarUrl}
name={props.username}/>}
<table className="highlight centered responsive-table">
<tbody>
<tr>
<td><strong>Username:</strong></td>
<td>{props.username}</td>
</tr>
<tr>
<td><strong>Account address:</strong></td>
<td>{props.address}</td>
</tr>
<tr>
<td><strong>OrbitDB:</strong></td>
<td>{orbitDBId}</td>
</tr>
<tr>
<td><strong>Number of topics created:</strong></td>
<td>{props.numberOfTopics}</td>
</tr>
<tr>
<td><strong>Number of posts:</strong></td>
<td>{props.numberOfPosts}</td>
</tr>
{dateOfRegister &&
<tr>
<td><strong>Member since:</strong></td>
<td>{epochTimeConverter(dateOfRegister)}</td>
</tr>
}
</tbody>
</table>
{props.self && <UsernameFormContainer/>}
</div>
);
};
export default ProfileInformation;

100
src/components/Topic.js

@ -1,100 +0,0 @@
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import { Card } from 'semantic-ui-react'
import TimeAgo from 'react-timeago';
import epochTimeConverter from '../helpers/EpochTimeConverter'
class Topic extends Component {
constructor(props){
super(props);
this.fetchSubject = this.fetchSubject.bind(this);
this.topicSubject = null;
this.topicSubjectFetchStatus = "pending";
}
async fetchSubject(topicID) {
this.topicSubjectFetchStatus = "fetching";
if (this.props.blockchainData[0].returnData[1] === this.props.user.address) {
let orbitData = this.props.orbitDB.topicsDB.get(topicID);
this.topicSubject = orbitData['subject'];
this.topicSubjectFetchStatus = "fetched";
} else {
const fullAddress = "/orbitdb/" + this.props.blockchainData[0].returnData[0] + "/topics";
const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress);
await store.load();
let localOrbitData = store.get(topicID);
if (localOrbitData) {
this.topicSubject = localOrbitData['subject'];
} else {
// Wait until we have received something from the network
store.events.on('replicated', () => {
this.topicSubject = store.get(topicID)['subject'];
})
}
this.topicSubjectFetchStatus = "fetched";
}
}
render(){
return (
<Card link className="card"
onClick={() => {this.context.router.push("/topic/" + this.props.topicID)}}>
<Card.Content>
<div className={"topic-subject" + (this.topicSubject ? "" : " grey-text")}>
<p><strong>
{this.topicSubject !== null ? this.topicSubject : "Subject"}
</strong></p>
</div>
<hr/>
<div className="topic-meta">
<p className={"no-margin" +
(this.props.blockchainData[0].returnData !== null ? "" : " grey-text")}>
{this.props.blockchainData[0].returnData !== null
?this.props.blockchainData[0].returnData[2]
:"Username"
}
</p>
<p className={"no-margin" +
(this.props.blockchainData[0].returnData !== null ? "" : " grey-text")}>
{"Number of replies: " + (this.props.blockchainData[0].returnData !== null
?this.props.blockchainData[0].returnData[4].length
:"")
}
</p>
<p className="topic-date grey-text">
{this.props.blockchainData[0].returnData !== null &&
<TimeAgo date={epochTimeConverter(this.props.blockchainData[0].returnData[3])}/>
}
</p>
</div>
</Card.Content>
</Card>
);
}
componentDidUpdate(){
if (this.props.blockchainData[0].returnData !== null && this.topicSubjectFetchStatus === "pending") {
this.fetchSubject(this.props.topicID);
}
}
};
Topic.contextTypes = {
router: PropTypes.object
};
const mapStateToProps = state => {
return {
user: state.user,
orbitDB: state.orbitDB
}
}
export default drizzleConnect(Topic, mapStateToProps);

30
src/components/TopicList.js

@ -1,30 +0,0 @@
import React from 'react';
import WithBlockchainData from './WithBlockchainData';
import Topic from './Topic';
const TopicList = (props) => {
const topics = props.topicIDs.map((topicID) => {
return (
<WithBlockchainData
component={Topic}
callsInfo={[{
contract: 'Forum',
method: 'getTopic',
params: [topicID]
}]}
topicID={topicID}
key={topicID}
/>
);
});
return (
<div className="topics-list">
{topics.slice(0).reverse()}
</div>
);
};
export default TopicList;

80
src/components/WithBlockchainData.js

@ -1,80 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class WithBlockchainData extends Component {
constructor(props, context) {
super(props);
{
let {component, callsInfo, ...rest } = this.props;
this.component = component;
this.callsInfo = callsInfo;
this.forwardedProps = rest;
}
this.drizzle = context.drizzle;
this.dataKeys = [];
let blockchainData = this.callsInfo.map((call) => {
return ({
callInfo: call,
status: "initialized",
returnData: null
});
});
//Initial call
for (var i = 0; i < this.callsInfo.length; ++i){
this.dataKeys[i] = this.drizzle
.contracts[this.callsInfo[i].contract]
.methods[this.callsInfo[i].method]
.cacheCall(...(this.callsInfo[i].params));
blockchainData[i].status = "pending";
}
this.state = {
callState: new Array(this.callsInfo.length).fill("pending"),
blockchainData: blockchainData
}
}
render() {
let {component, callsInfo, ...rest } = this.props; //Update rest arguments
return (
<this.component blockchainData={this.state.blockchainData} {...rest}/>
);
}
componentWillUpdate(){
let currentDrizzleState = this.drizzle.store.getState();
for (var i = 0; i < this.callsInfo.length; ++i){
let dataFetched = (currentDrizzleState
.contracts[this.callsInfo[i].contract][this.callsInfo[i].method][this.dataKeys[i]]);
if (dataFetched && dataFetched.value !== this.state.blockchainData[i].returnData){
/* There are new data in the blockchain*/
//Immutable update
let newBlockchainData = this.state.blockchainData.map((callData, index) => {
if (index !== i) return callData;
return {
...callData,
returnData: dataFetched.value,
status: "success"
}
})
let newStates = this.state.callState.slice();
newStates[i] = "success"
this.setState({
callState: newStates,
blockchainData: newBlockchainData
});
}
}
}
}
WithBlockchainData.contextTypes = {
drizzle: PropTypes.object
};
export default WithBlockchainData;

117
src/containers/BoardContainer.js

@ -1,117 +0,0 @@
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import { Header } from 'semantic-ui-react';
import WithBlockchainData from '../components/WithBlockchainData';
import TopicList from '../components/TopicList';
import FloatingButton from '../components/FloatingButton';
import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions';
class Board extends Component {
constructor(props) {
super(props);
this.props.store.dispatch(showProgressBar());
this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this);
this.state = {
pageLoaded: false
}
}
handleCreateTopicClick() {
this.context.router.push("/startTopic");
}
render() {
var boardContents;
if (this.props.blockchainData[0].returnData !== '0'){
this.topicIDs = [];
for (var i = 0; i < this.props.blockchainData[0].returnData; i++) {
this.topicIDs.push(i);
}
boardContents = ([
<TopicList topicIDs={this.topicIDs} key="topicList"/>,
<div className="bottom-overlay-pad" key="pad"></div>,
this.props.user.hasSignedUp &&
<FloatingButton onClick={this.handleCreateTopicClick}
key="createTopicButton"/>
]);
} else {
if (!this.props.user.hasSignedUp){
boardContents = (
<div className="vertical-center-in-parent">
<Header color='teal' textAlign='center' as='h2'>
There are no topics yet!
</Header>
<Header color='teal' textAlign='center' as='h4'>
Sign up to be the first to post.
</Header>
</div>
);
} else {
boardContents = (
<div className="vertical-center-in-parent">
<Header color='teal' textAlign='center' as='h2'>
There are no topics yet!
</Header>
<Header color='teal' textAlign='center' as='h4'>
Click the add button at the bottom of the page to be the first to post.
</Header>
<FloatingButton onClick={this.handleCreateTopicClick}
key="createTopicButton"/>
</div>
);
}
}
return (
<div className="fill">
{boardContents}
</div>
);
}
componentDidUpdate(){
if (!this.state.pageLoaded && this.props.blockchainData[0].returnData){
this.props.store.dispatch(hideProgressBar());
this.setState({ pageLoaded: true });
}
}
}
Board.contextTypes = {
drizzle: PropTypes.object,
router: PropTypes.object
};
const mapStateToProps = state => {
return {
user: state.user
}
};
class BoardContainer extends Component {
constructor(props){
super(props);
this.board = <WithBlockchainData
component={drizzleConnect(Board, mapStateToProps)}
callsInfo={[{
contract: 'Forum',
method: 'getNumberOfTopics',
params: []
}]}
/>;
}
render() {
return(this.board);
}
}
export default BoardContainer;

35
src/containers/HomeContainer.js

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

88
src/containers/LoadingContainer.js

@ -1,88 +0,0 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
import ipfs_logo from './../resources/ipfs_logo.png';
class LoadingContainer extends Component {
render() {
if (this.props.web3.status === 'failed')
{
if (this.props.errorComp) {
return this.props.errorComp
}
return(
<main className="loading-screen">
<div>
<div>
<h1><span role="img" aria-label="Warning Sign"></span></h1>
<p>This browser has no connection to the Ethereum network. Please use the Chrome/FireFox extension MetaMask, or dedicated Ethereum browsers Mist or Parity.</p>
</div>
</div>
</main>
)
}
if (this.props.web3.status === 'initialized' && Object.keys(this.props.accounts).length === 0)
{
return(
<main className="loading-screen">
<div>
<div>
<h1><span role="img" aria-label="Fox Face">🦊</span></h1>
<p><strong>We can't find any Ethereum accounts!</strong> Please check and make sure Metamask or you browser are pointed at the correct network and your account is unlocked.</p>
</div>
</div>
</main>
)
}
if (!this.props.orbitDB.ipfsInitialized)
{
return(
<main className="loading-screen">
<div>
<div>
<img src={ipfs_logo} alt="ipfs_logo" height="50"/>
<p><strong>Initializing IPFS...</strong></p>
</div>
</div>
</main>
)
}
if (this.props.drizzleStatus.initialized)
return Children.only(this.props.children);
if (this.props.loadingComp)
return this.props.loadingComp;
return(
<main className="container loading-screen">
<div>
<div>
<h1><span role="img" aria-label="Gear"></span></h1>
<p>Loading dapp...</p>
</div>
</div>
</main>
)
}
}
LoadingContainer.contextTypes = {
drizzle: PropTypes.object
};
const mapStateToProps = state => {
return {
accounts: state.accounts,
drizzleStatus: state.drizzleStatus,
web3: state.web3,
orbitDB: state.orbitDB
}
};
export default drizzleConnect(LoadingContainer, mapStateToProps)

195
src/containers/ProfileContainer.js

@ -1,195 +0,0 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Tab } from 'semantic-ui-react'
import WithBlockchainData from '../components/WithBlockchainData';
import ProfileInformation from '../components/ProfileInformation';
import TopicList from '../components/TopicList';
import PostList from '../components/PostList';
import LoadingSpinner from '../components/LoadingSpinner';
import {
showProgressBar,
hideProgressBar,
setNavBarTitle
} from '../redux/actions/userInterfaceActions';
class Profile extends Component {
constructor(props, context) {
super(props);
this.props.store.dispatch(showProgressBar());
this.propsToView = this.propsToView.bind(this);
this.drizzle = context.drizzle;
this.state = {
userAddress: this.props.params.address ? this.props.params.address : this.props.user.address
};
}
render() {
if (!this.props.user.hasSignedUp) {
this.context.router.push("/signup");
return(null);
}
this.propsToView();
var infoTab =
(<WithBlockchainData
component={ProfileInformation}
callsInfo={[{
contract: 'Forum',
method: 'getUserDateOfRegister',
params: [this.state.userAddress]
},{
contract: 'Forum',
method: 'getOrbitDBId',
params: [this.state.userAddress]
}]}
address={this.state.userAddress}
username={this.username}
numberOfTopics={this.topicIDs && this.topicIDs.length}
numberOfPosts={this.postIDs && this.postIDs.length}
self={this.state.userAddress === this.props.user.address}
key="profileInfo"
/>);
var topicsTab =
(<div className="profile-tab">
{this.topicIDs
? <TopicList topicIDs={this.topicIDs} />
: <LoadingSpinner />
}
</div>);
var postsTab =
(<div className="profile-tab">
{this.postIDs
? <PostList postIDs={this.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>
);
}
propsToView(){
if (!this.username){
let transaction = this.props.blockchainData
.find(transaction => transaction.callInfo.method === "getUsername");
if (transaction.returnData){
this.username = transaction.returnData;
}
}
if (!this.topicIDs){
let transaction = this.props.blockchainData
.find(transaction => transaction.callInfo.method === "getUserTopics");
if (transaction.returnData){
this.topicIDs = transaction.returnData;
}
}
if (!this.postIDs){
let transaction = this.props.blockchainData
.find(transaction => transaction.callInfo.method === "getUserPosts");
if (transaction.returnData){
this.postIDs = transaction.returnData;
}
}
}
componentDidUpdate(){
if (this.username){
this.props.store.dispatch(setNavBarTitle(this.username));
if (this.topicIDs && this.postIDs){
this.props.store.dispatch(hideProgressBar());
}
}
}
}
Profile.contextTypes = {
drizzle: PropTypes.object,
router: PropTypes.object
};
const mapStateToProps = state => {
return {
user: state.user,
orbitDB: state.orbitDB
}
};
class ProfileContainer extends Component {
constructor(props){
super(props);
let userAddress;
if (this.props.params.address){
userAddress = this.props.params.address;
} else {
userAddress = this.props.user.address;
}
this.profile = <WithBlockchainData
component={drizzleConnect(Profile, mapStateToProps)}
callsInfo={[{
contract: 'Forum',
method: 'getUsername',
params: [userAddress]
},{
contract: 'Forum',
method: 'getUserTopics',
params: [userAddress]
},{
contract: 'Forum',
method: 'getUserPosts',
params: [userAddress]
}]}
params={this.props.params}
/>
}
render() {
return(this.profile);
}
}
const containerProps = state => {
return {
user: state.user
}
};
export default drizzleConnect(ProfileContainer, containerProps);

50
src/containers/SignUpContainer.js

@ -1,50 +0,0 @@
import { drizzleConnect } from 'drizzle-react';
import React, { Component } from 'react';
import UsernameFormContainer from './UsernameFormContainer';
import { Header } from 'semantic-ui-react';
class SignUp extends Component {
constructor(props){
super(props);
this.signedUp = this.signedUp.bind(this);
}
signedUp(){
this.props.router.push("/home");
}
render() {
return (
this.props.user.hasSignedUp
?(<div className="vertical-center-in-parent">
<Header color='teal' textAlign='center' as='h2'>
There is already an account for this addresss.
</Header>
<Header color='teal' textAlign='center' as='h4'>
If you want to create another account please change your address.
</Header>
</div>)
:(<div className="sign-up-container">
<div>
<h1>Sign Up</h1>
<p className="no-margin">
<strong>Account address:</strong> {this.props.user.address}
</p>
<UsernameFormContainer signedUp={this.signedUp}/>
</div>
</div>)
);
}
}
const mapStateToProps = state => {
return {
user: state.user
}
};
const SignUpContainer = drizzleConnect(SignUp, mapStateToProps);
export default SignUpContainer;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save