Browse Source

Merge branch 'develop-ultimate' into 'develop'

Develop ultimate

See merge request ecentrics/apella!4
develop
Apostolos Fanakis 4 years ago
parent
commit
a146a1470b
  1. 7
      .dockerignore
  2. 15
      .eslintrc.json
  3. 1
      .eslintrignore
  4. 3
      .gitattributes
  5. 33
      .gitignore
  6. 48
      Dockerfile
  7. 10
      Makefile
  8. 3
      README.md
  9. 3
      app/.eslintrc.json
  10. 1
      app/.eslintrignore
  11. 53
      app/package.json
  12. 26
      app/src/CustomPropTypes.js
  13. 187
      app/src/assets/css/App.css
  14. 30
      app/src/assets/css/board-container.css
  15. 8
      app/src/assets/css/loading-container.css
  16. 5
      app/src/assets/css/profile-container.css
  17. 12
      app/src/assets/css/sign-up-container.css
  18. 6
      app/src/assets/css/start-topic-container.css
  19. 51
      app/src/assets/css/topic-container.css
  20. 12016
      app/src/assets/fonts/fontawesome-free-5.7.2/all.js
  21. 139
      app/src/components/BoardContainer.js
  22. 43
      app/src/components/CoreLayoutContainer.js
  23. 17
      app/src/components/FloatingButton.js
  24. 26
      app/src/components/HomeContainer.js
  25. 107
      app/src/components/LoadingContainer.js
  26. 23
      app/src/components/LoadingSpinner.js
  27. 66
      app/src/components/NavBarContainer.js
  28. 225
      app/src/components/NewPost.js
  29. 68
      app/src/components/NewTopicPreview.js
  30. 167
      app/src/components/PlaceholderContainer.js
  31. 229
      app/src/components/Post.js
  32. 112
      app/src/components/PostList.js
  33. 220
      app/src/components/ProfileContainer.js
  34. 194
      app/src/components/ProfileInformation.js
  35. 55
      app/src/components/SignUpContainer.js
  36. 166
      app/src/components/StartTopicContainer.js
  37. 105
      app/src/components/Topic.js
  38. 214
      app/src/components/TopicContainer.js
  39. 104
      app/src/components/TopicList.js
  40. 153
      app/src/components/TransactionsMonitorContainer.js
  41. 201
      app/src/components/UsernameFormContainer.js
  42. 15
      app/src/config/drizzleOptions.js
  43. 28
      app/src/config/ipfsOptions.js
  44. 12
      app/src/helpers/EpochTimeConverter.js
  45. 33
      app/src/index.js
  46. 6
      app/src/redux/actions/drizzleActions.js
  47. 4
      app/src/redux/actions/drizzleUtilsActions.js
  48. 38
      app/src/redux/actions/orbitActions.js
  49. 53
      app/src/redux/actions/transactionsActions.js
  50. 6
      app/src/redux/actions/userActions.js
  51. 10
      app/src/redux/actions/userInterfaceActions.js
  52. 73
      app/src/redux/reducers/orbitReducer.js
  53. 14
      app/src/redux/reducers/rootReducer.js
  54. 18
      app/src/redux/reducers/userInterfaceReducer.js
  55. 29
      app/src/redux/reducers/userReducer.js
  56. 53
      app/src/redux/sagas/drizzleUtilsSaga.js
  57. 22
      app/src/redux/sagas/eventSaga.js
  58. 107
      app/src/redux/sagas/orbitSaga.js
  59. 21
      app/src/redux/sagas/rootSaga.js
  60. 83
      app/src/redux/sagas/transactionsSaga.js
  61. 68
      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. 64
      app/src/utils/ethereumIdentityProvider.js
  66. 109
      app/src/utils/orbitUtils.js
  67. 130
      app/src/utils/serviceWorker.js
  68. 23
      contracts/Migrations.sol
  69. 96
      docker-compose.yml
  70. 23
      env/apella.env.example
  71. 5
      ganache/Dockerfile
  72. 13
      ganache/docker-compose.yml
  73. 9
      migrateAndStart.sh
  74. 5
      migrations/1_initial_migration.js
  75. 5
      migrations/2_deploy_contracts.js
  76. 27
      package.json
  77. 52
      packages/concordia-app/package.json
  78. 15
      packages/concordia-app/patches/web3-eth+1.3.0.patch
  79. 0
      packages/concordia-app/public/favicon.ico
  80. 2
      packages/concordia-app/public/index.html
  81. 0
      packages/concordia-app/public/manifest.json
  82. 2
      packages/concordia-app/public/robots.txt
  83. 3
      packages/concordia-app/src/assets/css/app.css
  84. 0
      packages/concordia-app/src/assets/css/index.css
  85. 28
      packages/concordia-app/src/assets/css/loading-component.css
  86. 0
      packages/concordia-app/src/assets/images/PageNotFound.jpg
  87. 0
      packages/concordia-app/src/assets/images/app_logo.png
  88. 0
      packages/concordia-app/src/assets/images/ethereum_logo.svg
  89. 0
      packages/concordia-app/src/assets/images/ipfs_logo.svg
  90. 0
      packages/concordia-app/src/assets/images/orbitdb_logo.png
  91. 34
      packages/concordia-app/src/components/App.jsx
  92. 72
      packages/concordia-app/src/components/AppContext.js
  93. 19
      packages/concordia-app/src/components/CoreLayoutContainer.jsx
  94. 9
      packages/concordia-app/src/components/HomeContainer.jsx
  95. 73
      packages/concordia-app/src/components/LoadingComponent.jsx
  96. 136
      packages/concordia-app/src/components/LoadingContainer.jsx
  97. 38
      packages/concordia-app/src/components/MenuComponent.jsx
  98. 12
      packages/concordia-app/src/components/NotFound.jsx
  99. 139
      packages/concordia-app/src/components/SignUpForm.jsx
  100. 29
      packages/concordia-app/src/index.js

7
.dockerignore

@ -1,7 +0,0 @@
env/
node_modules/
package-lock.json
yarn.lock
app/node_modules/
app/package-lock.json
app/yarn.lock

15
.eslintrc.json

@ -1,15 +0,0 @@
{
"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

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

3
.gitattributes

@ -5,3 +5,6 @@
*.png binary *.png binary
*.jpg binary *.jpg binary
*.ico binary *.ico binary
# Solidity
*.sol linguist-language=Solidity

33
.gitignore

@ -1,22 +1,17 @@
# Node # Node
/node_modules /node_modules
/app/node_modules packages/*/node_modules
package-lock.json packages/concordia-contracts/build
# Yarn # IDE
yarn.lock .DS_Store
.idea
# Testing
/coverage
# Build Directories # Build Directories
/build /build
/src/build /src/build
/app/src/build /packages/concordia-app/build
/app/build /packages/concordia-contracts/build
# Contract Artifacts
/app/src/contracts
# Logs # Logs
/log /log
@ -24,16 +19,16 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# Docker volumes
docker/volumes
docker/reports
docker/env/concordia.env
# Misc # Misc
.DS_Store
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
/compileAndRun.sh
# Jetbrains
.idea
/env/*.env # Lerna
/volumes *.lerna_backup

48
Dockerfile

@ -1,48 +0,0 @@
# ----- Base image -----
FROM tarampampam/node:11-alpine as base
LABEL maintainer="apotwohd@gmail.com"
ENV DOCKER true
# Installs a couple (dozen) more tools like python, c++, make and others
RUN apk --no-cache add build-base \
python3 && \
if [ ! -e /usr/bin/python ]; then ln -sf python3 /usr/bin/python ; fi
# Installs truffle
RUN yarn global add truffle
WORKDIR /usr/apella
COPY ./package.json ./
COPY ./app/package.json ./app/
# ----- Dependencies -----
FROM base as dependencies
# Installs node packages from ./package.json
RUN yarn install
# Installs node packages from ./app/package.json
RUN cd app/ && yarn install
# ----- Test -----
#FROM dependencies AS test
# Preps directories
#COPY . .
# Runs linters and tests
#RUN npm run lint && npm run test
# ----- Runtime -----
FROM base as runtime
# Copies node_modules
COPY --from=dependencies /usr/apella/node_modules ./node_modules
COPY --from=dependencies /usr/apella/app/node_modules ./app/node_modules
# Preps directories
COPY . .
RUN ["chmod", "+x", "/usr/apella/migrateAndStart.sh"]
ENTRYPOINT ["/usr/apella/migrateAndStart.sh"]

10
Makefile

@ -1,10 +0,0 @@
build:
@docker-compose -p apella build;
run:
@docker-compose -p apella up -d
stop:
@docker-compose -p apella down
clean-data:
@docker-compose -p apella down -v
clean-images:
@docker rmi `docker images -q -f "dangling=true"`

3
README.md

@ -1,3 +0,0 @@
# Apella
*Note: This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).*

3
app/.eslintrc.json

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

1
app/.eslintrignore

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

53
app/package.json

@ -1,53 +0,0 @@
{
"name": "apella",
"version": "0.1.0",
"private": true,
"repository": {
"type": "git",
"url": "https://gitlab.com/Ezerous/Apella.git"
},
"homepage": ".",
"dependencies": {
"@drizzle-utils/get-contract-instance": "0.2.0",
"@drizzle-utils/get-web3": "0.2.1",
"connected-react-router": "6.4.0",
"drizzle": "1.4.0",
"history": "4.9.0",
"ipfs": "0.35.0",
"level": "5.0.1",
"lodash.isequal": "4.5.0",
"orbit-db": "0.21.0",
"orbit-db-identity-provider": "0.1.0",
"prop-types": "15.7.2",
"react": "16.8.6",
"react-content-loader": "4.2.1",
"react-dom": "16.8.6",
"react-markdown": "4.0.8",
"react-redux": "7.0.3",
"react-router-dom": "5.0.0",
"react-scripts": "3.0.1",
"react-timeago": "4.4.0",
"react-user-avatar": "1.10.0",
"redux": "4.0.1",
"redux-saga": "0.16.2",
"semantic-ui-react": "0.87.1",
"uuid": "3.3.2",
"web3": "1.0.0-beta.55"
},
"devDependencies": {
"libp2p-websocket-star-rendezvous": "0.3.0"
},
"scripts": {
"start": "react-scripts start",
"rendezvous": "rendezvous --port=9090 --host=83.212.109.171",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

26
app/src/CustomPropTypes.js

@ -1,26 +0,0 @@
import PropTypes from 'prop-types';
//TODO: Move this file
const GetTopicResult = PropTypes.PropTypes.shape({
userAddress: PropTypes.string.isRequired,
userName: PropTypes.string.isRequired,
timestamp: PropTypes.number.isRequired,
numberOfReplies: PropTypes.number.isRequired
});
const GetPostResult = PropTypes.PropTypes.shape({
userAddress: PropTypes.string.isRequired,
userName: PropTypes.string.isRequired,
timestamp: PropTypes.number.isRequired,
topicID: PropTypes.string.isRequired
});
const TopicPlaceholderExtra = PropTypes.PropTypes.shape({
topicID: PropTypes.number.isRequired,
});
const PostPlaceholderExtra = PropTypes.PropTypes.shape({
postIndex: PropTypes.number.isRequired,
});
export { GetTopicResult, GetPostResult, TopicPlaceholderExtra, PostPlaceholderExtra };

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

@ -1,187 +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;
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

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

8
app/src/assets/css/loading-container.css

@ -1,8 +0,0 @@
.loading-screen {
text-align: center;
}
.loading-img {
margin-bottom: 30px;
height: 100px;
}

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

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

12
app/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
app/src/assets/css/start-topic-container.css

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

51
app/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;
}

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

File diff suppressed because one or more lines are too long

139
app/src/components/BoardContainer.js

@ -1,139 +0,0 @@
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 './TopicList';
import FloatingButton from './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));

43
app/src/components/CoreLayoutContainer.js

@ -1,43 +0,0 @@
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';
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;

17
app/src/components/FloatingButton.js

@ -1,17 +0,0 @@
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;

26
app/src/components/HomeContainer.js

@ -1,26 +0,0 @@
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/components/LoadingContainer.js

@ -1,107 +0,0 @@
import React, { Children, Component } from 'react';
import { connect } from 'react-redux';
import '../assets/css/loading-container.css';
import ethereum_logo from '../assets/images/ethereum_logo.svg';
import ipfs_logo from '../assets/images/ipfs_logo.svg';
import orbitdb_logo from '../assets/images/orbitdb_logo.png';
import logo from '../assets/images/logo.png';
class LoadingContainer extends Component {
render() {
if (this.props.web3.status === 'failed' || !this.props.web3.networkId) {
return (
<main className="loading-screen">
<div className="pure-g">
<div className="pure-u-1-1">
<img src={ethereum_logo} alt="ethereum_logo" className="loading-img"/>
<p><strong>This browser has no connection to the Ethereum network.</strong></p>
Please make sure that:
<ul>
<li>MetaMask is unlocked and pointed to the correct network</li>
<li>The app has been granted the right to connect to your account</li>
</ul>
</div>
</div>
</main>
);
}
if (this.props.web3.status === 'initialized' && Object.keys(this.props.accounts).length === 0) {
return(
<main className="loading-screen">
<div>
<img src={ethereum_logo} alt="ethereum_logo" className="loading-img"/>
<p><strong>We can't find any Ethereum accounts!</strong></p>
<p>Please make sure that MetaMask is unlocked.</p>
</div>
</main>
)
}
if (!this.props.contractInitialized) {
return(
<main className="loading-screen">
<div>
<img src={ethereum_logo} alt="ethereum_logo" className="loading-img"/>
<p><strong>Initializing contracts...</strong></p>
<p>If this takes too long please make sure they are deployed to the network
and you are connected to the correct one.
</p>
</div>
</main>
)
}
if (!this.props.ipfsInitialized) {
return(
<main className="loading-screen">
<div>
<img src={ipfs_logo} alt="ipfs_logo" className="loading-img"/>
<p><strong>Initializing IPFS...</strong></p>
</div>
</main>
)
}
if (!this.props.orbitReady) {
const message = process.env.NODE_ENV ==='development' ?
"If needed, please sign the transaction in MetaMask to create the databases." :
"Please sign the transaction in MetaMask to create the databases.";
return(
<main className="loading-screen">
<div>
<img src={orbitdb_logo} alt="orbitdb_logo" className="loading-img"/>
<p><strong>Preparing OrbitDB...</strong></p>
<p>{message}</p>
</div>
</main>
)
}
if (this.props.drizzleStatus.initialized)
return Children.only(this.props.children);
return(
<main className="loading-screen">
<div>
<img src={logo} alt="app_logo" className="loading-img"/>
<p><strong>Loading dapp...</strong></p>
</div>
</main>
)
}
}
const mapStateToProps = state => {
return {
accounts: state.accounts,
drizzleStatus: state.drizzleStatus,
web3: state.web3,
ipfsInitialized: state.orbit.ipfsInitialized,
orbitReady: state.orbit.ready,
contractInitialized: state.contracts.Forum.initialized
}
};
export default connect(mapStateToProps)(LoadingContainer);

23
app/src/components/LoadingSpinner.js

@ -1,23 +0,0 @@
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;

66
app/src/components/NavBarContainer.js

@ -1,66 +0,0 @@
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);

225
app/src/components/NewPost.js

@ -1,225 +0,0 @@
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, username, 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={username}
/>
</Grid.Column>
<Grid.Column width={15}>
<div className="">
<div className="stretch-space-between">
<span><strong>{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,
username: PropTypes.string.isRequired,
onCancelClick: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
onPostCreated: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
username: state.user.username
});
export default connect(mapStateToProps)(NewPost);

68
app/src/components/NewTopicPreview.js

@ -1,68 +0,0 @@
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);

167
app/src/components/PlaceholderContainer.js

@ -1,167 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { TopicPlaceholderExtra, PostPlaceholderExtra } from '../CustomPropTypes'
import ContentLoader from 'react-content-loader';
import { Card, Button, Divider, Grid, Icon, Label } from 'semantic-ui-react';
class PlaceholderContainer extends Component {
render() {
const { placeholderType, extra, history } = this.props;
switch (placeholderType) {
case 'Topic':
return(
<Card
link
className="card"
onClick={() => {
history.push(`/topic/${extra.topicID}`);
}}
>
<Card.Content>
<div>
<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>
</div>
<hr />
<div className="topic-meta">
<p className="no-margin">
<ContentLoader
height={5.8}
width={300}
speed={2}
primaryColor="#b2e8e6"
secondaryColor="#00b5ad"
>
<rect x="0" y="0" rx="3" ry="3" width="60" height="5.5" />
</ContentLoader>
</p>
<p className="no-margin">
<ContentLoader
height={5.8}
width={300}
speed={2}
primaryColor="#b2e8e6"
secondaryColor="#00b5ad"
>
<rect x="0" y="0" rx="3" ry="3" width="70" height="5.5" />
</ContentLoader>
</p>
<p className="topic-date grey-text">
<ContentLoader
height={5.8}
width={300}
speed={2}
primaryColor="#b2e8e6"
secondaryColor="#00b5ad"
>
<rect x="260" y="0" rx="3" ry="3" width="40" height="5.5" />
</ContentLoader>
</p>
</div>
</Card.Content>
</Card>
);
case 'Post':
return(
<div className="post">
<Divider horizontal>
<span className="grey-text">
#
{extra.postIndex}
</span>
</Divider>
<Grid>
<Grid.Row columns={16} stretched>
<Grid.Column width={1} className="user-avatar">
<div className="user-avatar">
<ContentLoader
height={52}
width={52}
speed={2}
primaryColor="#b2e8e6"
secondaryColor="#00b5ad"
>
<circle cx="26" cy="26" r="26" />
</ContentLoader>
</div>
</Grid.Column>
<Grid.Column width={15}>
<div className="">
<div className="stretch-space-between">
<span className='grey-text'>
<strong>Username</strong>
</span>
<span className="grey-text">
<ContentLoader height={5.8} width={300} speed={2} primaryColor="#b2e8e6"
secondaryColor="#00b5ad" >
<rect x="280" y="0" rx="3" ry="3" width="20" height="5.5" />
</ContentLoader>
</span>
</div>
<div className="stretch-space-between">
<span className='grey-text' >
<strong>
<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>
</strong>
</span>
</div>
<div className="post-content">
<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">
{/* TODO
<Button icon size="mini" disabled
style={{
marginRight: '0px'
}}
>
<Icon name="chevron up" />
</Button>
<Label color="teal">Loading...</Label>
<Button icon size="mini" disabled >
<Icon name="chevron down" />
</Button>
*/
}
<Button icon size="mini" disabled >
<Icon name="linkify" />
</Button>
</Grid.Column>
</Grid.Row>
</Grid>
</div>
);
}
}
}
PlaceholderContainer.propTypes = {
placeholderType: PropTypes.string.isRequired,
extra: PropTypes.oneOfType([
TopicPlaceholderExtra.isRequired,
PostPlaceholderExtra.isRequired
])
};
export default withRouter(PlaceholderContainer);

229
app/src/components/Post.js

@ -1,229 +0,0 @@
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 } from 'semantic-ui-react';
import TimeAgo from 'react-timeago';
import UserAvatar from 'react-user-avatar';
import ReactMarkdown from 'react-markdown';
import { GetPostResult } from '../CustomPropTypes'
import { addPeerDatabase } from '../redux/actions/orbitActions';
class Post extends Component {
constructor(props) {
super(props);
if (props.getFocus)
this.postRef = React.createRef();
}
componentDidMount() {
const { addPeerDB, userAddress, postData } = this.props;
if(postData.userAddress !== userAddress )
addPeerDB(postData.userAddress, 'posts');
}
render() {
const { avatarUrl, postIndex, navigateTo, postData, postID, postSubject, postContent } = this.props;
return (
<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">
<Link
to={`/profile/${postData.userAddress}/${postData.userName}`}
onClick={(event) => { event.stopPropagation(); }} >
<UserAvatar
size="52"
className="inline"
src={avatarUrl}
name={postData.userName}
/>
</Link>
</Grid.Column>
<Grid.Column width={15}>
<div className="">
<div className="stretch-space-between">
<span>
<strong>
{postData.userName}
</strong>
</span>
<span className="grey-text">
<TimeAgo date={postData.timestamp} />
</span>
</div>
<div className="stretch-space-between">
<span>
<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">
{/* TODO (also in PlaceHolderContainer)
<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.topicID}/${
postID}`);
}
: () => {}}
>
<Icon name="linkify" />
</Button>
</Grid.Column>
</Grid.Row>
</Grid>
</div>
);
}
}
Post.propTypes = {
getFocus: PropTypes.bool.isRequired,
userAddress: PropTypes.string.isRequired,
avatarUrl: PropTypes.string,
postIndex: PropTypes.number.isRequired,
navigateTo: PropTypes.func.isRequired,
postData: GetPostResult.isRequired,
postID: PropTypes.string.isRequired
};
function getPostSubject(state, props){
const { user: {address: userAddress}, orbit } = state;
const { postData, postID } = props;
if(userAddress === postData.userAddress) {
const orbitData = orbit.postsDB.get(postID);
if(orbitData && orbitData.subject)
return orbitData.subject;
}
else{
const db = orbit.peerDatabases.find(db =>
(db.userAddress === postData.userAddress) && (db.name === 'posts'));
if(db && db.store){
const localOrbitData = db.store.get(postID);
if (localOrbitData)
return localOrbitData.subject;
}
}
return '';
}
function getPostContent(state, props){
const { user: {address: userAddress}, orbit } = state;
const { postData, postID } = props;
if(userAddress === postData.userAddress) {
const orbitData = orbit.postsDB.get(postID);
if(orbitData && orbitData.content)
return orbitData.content;
}
else{
const db = orbit.peerDatabases.find(db =>
(db.userAddress === postData.userAddress) && (db.name === 'posts'));
if(db && db.store){
const localOrbitData = db.store.get(postID);
if (localOrbitData)
return localOrbitData.content;
}
}
return '';
}
const mapDispatchToProps = dispatch => bindActionCreators({
navigateTo: location => push(location),
addPeerDB: (userAddress, name) => addPeerDatabase(userAddress, name)
}, dispatch);
function mapStateToProps(state, ownProps) {
return {
userAddress: state.user.address,
postSubject: getPostSubject(state, ownProps),
postContent: getPostContent(state, ownProps)
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Post));

112
app/src/components/PostList.js

@ -1,112 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { drizzle } from '../index';
import Post from './Post';
import PlaceholderContainer from './PlaceholderContainer';
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(async (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) => {
let fetchedPostData;
if(dataKeys[postID])
fetchedPostData = contracts[contract][getPostMethod][dataKeys[postID]];
if(fetchedPostData) {
const postData = {
userAddress: fetchedPostData.value[0], //Also works as an Orbit Identity ID
userName: fetchedPostData.value[1],
timestamp: fetchedPostData.value[2]*1000,
topicID: fetchedPostData.value[3]
};
return(
<Post
postData={postData}
avatarUrl=""
postIndex={index}
postID={postID}
getFocus={focusOnPost === postID}
key={postID}
/>
)
}
return (<PlaceholderContainer placeholderType='Post'
extra={{postIndex: index}} key={postID} />);
});
return (
<div>
{recentToTheTop
? posts.slice(0).reverse()
: posts
}
</div>
);
}
}
PostList.propTypes = {
drizzleStatus: PropTypes.object.isRequired,
postIDs: PropTypes.array.isRequired,
contracts: PropTypes.PropTypes.objectOf(PropTypes.object).isRequired,
focusOnPost: PropTypes.number,
recentToTheTop: PropTypes.bool
};
const mapStateToProps = state => ({
contracts: state.contracts,
drizzleStatus: state.drizzleStatus
});
export default connect(mapStateToProps)(PostList);

220
app/src/components/ProfileContainer.js

@ -1,220 +0,0 @@
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 './ProfileInformation';
import TopicList from './TopicList';
import PostList from './PostList';
import LoadingSpinner from './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 : -1}
numberOfPosts={postIDs ? postIDs.length : -1}
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.PropTypes.objectOf(PropTypes.object).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
});
export default connect(mapStateToProps, mapDispatchToProps)(ProfileContainer);

194
app/src/components/ProfileInformation.js

@ -1,194 +0,0 @@
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 ContentLoader from 'react-content-loader';
import UsernameFormContainer from './UsernameFormContainer';
import { Table } from 'semantic-ui-react'
import { determineDBAddress } from '../utils/orbitUtils';
//TODO: No array needed unless we add more calls
const callsInfo = [
{
contract: 'Forum',
method: 'getUserDateOfRegister'
}
];
class ProfileInformation extends Component {
constructor(props) {
super(props);
this.getBlockchainData = this.getBlockchainData.bind(this);
this.dataKey = [];
this.state = {
pageStatus: 'initialized',
dateOfRegister: '',
topicsDBId: '',
postsDBId: ''
};
}
componentDidMount() {
this.getBlockchainData();
}
componentDidUpdate() {
this.getBlockchainData();
}
getBlockchainData() {
const { pageStatus, dateOfRegister } = 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'
});
determineDBAddress('topics', address).then(topicsDBAddress => {
this.setState({
topicsDBId: topicsDBAddress
});}
).catch(() => {});
determineDBAddress('posts', address).then(postsDBAddress => {
this.setState({
postsDBId: postsDBAddress
});}
).catch(() => {});
}
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
});
}
}
}
}
render() {
const { topicsDBId, postsDBId, 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 basic='very' singleLine>
<Table.Body>
<Table.Row>
<Table.Cell><strong>Username:</strong></Table.Cell>
<Table.Cell>{username}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Account address:</strong></Table.Cell>
<Table.Cell>{address}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>TopicsDB:</strong></Table.Cell>
<Table.Cell>{topicsDBId ? topicsDBId
: <ContentLoader height={5.8} width={300} speed={2}
primaryColor="#b2e8e6" secondaryColor="#00b5ad"
>
<rect x="0" y="0" rx="3" ry="3" width="80" height="5.5" />
</ContentLoader>
}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>PostsDB:</strong></Table.Cell>
<Table.Cell>{postsDBId ? postsDBId
: <ContentLoader height={5.8} width={300} speed={2}
primaryColor="#b2e8e6" secondaryColor="#00b5ad"
>
<rect x="0" y="0" rx="3" ry="3" width="80" height="5.5" />
</ContentLoader>
}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Number of topics created:</strong></Table.Cell>
<Table.Cell>{numberOfTopics !== -1 ? numberOfTopics
: <ContentLoader height={5.8} width={300} speed={2}
primaryColor="#b2e8e6" secondaryColor="#00b5ad"
>
<rect x="0" y="0" rx="3" ry="3" width="15" height="5.5" />
</ContentLoader>
}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Number of posts:</strong></Table.Cell>
<Table.Cell>{numberOfPosts !== -1 ? numberOfPosts
: <ContentLoader height={5.8} width={300} speed={2}
primaryColor="#b2e8e6" secondaryColor="#00b5ad"
>
<rect x="0" y="0" rx="3" ry="3" width="15" height="5.5" />
</ContentLoader>
}</Table.Cell>
</Table.Row>
{dateOfRegister
&& (
<Table.Row>
<Table.Cell><strong>Member since:</strong></Table.Cell>
<Table.Cell>{epochTimeConverter(dateOfRegister)}</Table.Cell>
</Table.Row>
)
}
</Table.Body>
</Table>
{self && <UsernameFormContainer />}
</div>
);
}
}
ProfileInformation.propTypes = {
drizzleStatus: PropTypes.object.isRequired,
contracts: PropTypes.PropTypes.objectOf(PropTypes.object).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
});
export default connect(mapStateToProps)(ProfileInformation);

55
app/src/components/SignUpContainer.js

@ -1,55 +0,0 @@
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);

166
app/src/components/StartTopicContainer.js

@ -1,166 +0,0 @@
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 './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 { hasSignedUp, history } = this.props;
if (!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="true"
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,
hasSignedUp: PropTypes.bool.isRequired
};
const mapStateToProps = state => ({
hasSignedUp: state.user.hasSignedUp
});
export default connect(mapStateToProps)(StartTopicContainer);

105
app/src/components/Topic.js

@ -1,105 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { GetTopicResult } from '../CustomPropTypes'
import ContentLoader from 'react-content-loader';
import { Card } from 'semantic-ui-react';
import TimeAgo from 'react-timeago';
import { addPeerDatabase } from '../redux/actions/orbitActions';
class Topic extends Component {
componentDidMount() {
const { dispatch, userAddress, topicData } = this.props;
if(topicData.userAddress !== userAddress )
dispatch(addPeerDatabase(topicData.userAddress, '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.userName}
</p>
<p className="no-margin">
Number of Replies: {topicData.numberOfReplies}
</p>
<p className="topic-date grey-text">
<TimeAgo date={topicData.timestamp}/>
</p>
</div>
</Card.Content>
</Card>
);
}
}
Topic.propTypes = {
userAddress: PropTypes.string.isRequired,
history: PropTypes.object.isRequired,
topicData: GetTopicResult.isRequired,
topicID: PropTypes.number.isRequired
};
function getTopicSubject(state, props){
const { user: {address: userAddress}, orbit } = state;
const { topicData, topicID } = props;
if(userAddress === topicData.userAddress) {
const orbitData = orbit.topicsDB.get(topicID);
if(orbitData && orbitData.subject)
return orbitData.subject;
}
else{
const db = orbit.peerDatabases.find(db =>
(db.userAddress === topicData.userAddress) && (db.name === 'topics'));
if(db && db.store){
const localOrbitData = db.store.get(topicID);
if (localOrbitData)
return localOrbitData.subject;
}
}
return null;
}
function mapStateToProps(state, ownProps) {
return {
userAddress: state.user.address,
topicSubject: getTopicSubject(state, ownProps)
}
}
export default withRouter(connect(mapStateToProps)(Topic));

214
app/src/components/TopicContainer.js

@ -1,214 +0,0 @@
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 './PostList';
import NewPost from './NewPost';
import FloatingButton from './FloatingButton';
import { setNavBarTitle } from '../redux/actions/userInterfaceActions.js';
import { determineDBAddress } from '../utils/orbitUtils';
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(userAddress) {
const { topicID } = this.state;
const { user, orbitDB, setNavBarTitle } = this.props;
let orbitData;
if (userAddress === user.address) {
orbitData = orbitDB.topicsDB.get(topicID);
} else {
const dbAddress = await determineDBAddress('topics', userAddress);
const fullAddress = `/orbitdb/${dbAddress}/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);
});
}
}
if(orbitData && orbitData.subject){
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[3]}
focusOnPost={postFocus
? postFocus
: null}
/>
{posting
&& (
<NewPost
topicID={topicID}
subject={topicSubject}
postIndex={contracts[contract][getTopicMethod][this.dataKey].value[3].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.PropTypes.objectOf(PropTypes.object).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);

104
app/src/components/TopicList.js

@ -1,104 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { drizzle } from '../index';
import Topic from './Topic';
import PlaceholderContainer from './PlaceholderContainer';
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(async (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 => {
let fetchedTopicData;
if(dataKeys[topicID])
fetchedTopicData = contracts[contract][getTopicMethod][dataKeys[topicID]];
if(fetchedTopicData) {
const topicData = {
userAddress: fetchedTopicData.value[0], //Also works as an Orbit Identity ID
userName: fetchedTopicData.value[1],
timestamp: fetchedTopicData.value[2]*1000,
numberOfReplies: fetchedTopicData.value[3].length
};
return(
<Topic
topicData={topicData}
topicID={topicID}
key={topicID}
/>
)
}
return (<PlaceholderContainer placeholderType='Topic'
extra={{topicID: topicID}} key={topicID} />);
});
//TODO: Return loading indicator instead of topics when not fully loaded (?)
return (
<div className="topics-list">
{topics.slice(0).reverse()}
</div>
);
}
}
TopicList.propTypes = {
topicIDs: PropTypes.arrayOf(PropTypes.number).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);

153
app/src/components/TransactionsMonitorContainer.js

@ -1,153 +0,0 @@
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(
(transactionIndex, index) => {
if (isTransactionMessageDismissed[index])
return null;
let color = 'black';
const message = [];
const transaction = transactions[transactionIndex];
if(!transaction)
message.push('New transaction has been queued and is waiting your confirmation.');
if (transaction && transaction.status === 'pending') {
message.push('New transaction has been queued and is waiting your confirmation.');
message.push(<br key="confirmed" />);
message.push('- transaction confirmed');
}
if (transaction && transaction.status === 'success') {
/* Transaction completed successfully */
message.push('New transaction has been queued and is waiting your confirmation.');
message.push(<br key="confirmed" />);
message.push('- transaction confirmed');
message.push(<br key="mined" />);
message.push('- transaction mined');
color = 'green';
message.push(<br key="success" />);
message.push('- transaction completed successfully');
} else if (transaction && transaction.status === 'error') {
/* Transaction failed to complete */
color = 'red';
message.push('Transaction failed!');
}
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;

201
app/src/components/UsernameFormContainer.js

@ -1,201 +0,0 @@
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 { 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
});
this.stackId = drizzle.contracts[contract].methods[signUpMethod].cacheSend(
...[usernameInput], { 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.PropTypes.objectOf(PropTypes.object).isRequired,
contracts: PropTypes.PropTypes.objectOf(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);

15
app/src/config/drizzleOptions.js

@ -1,15 +0,0 @@
import Forum from '../contracts/Forum.json';
// Docs: https://truffleframework.com/docs/drizzle/reference/drizzle-options
const drizzleOptions = {
contracts: [Forum],
events: {
Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated']
},
polls: {
accounts: 2000,
blocks: 2000
}
};
export default drizzleOptions;

28
app/src/config/ipfsOptions.js

@ -1,28 +0,0 @@
const REACT_APP_RENDEZVOUS_HOST = process.env.REACT_APP_RENDEZVOUS_HOST || '127.0.0.1';
const REACT_APP_RENDEZVOUS_PORT = process.env.REACT_APP_RENDEZVOUS_PORT || '9090';
// 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 signaling server (see also rendezvous script in package.json)
// For more information: https://github.com/libp2p/js-libp2p-websocket-star-rendezvous
'/dns4/' + REACT_APP_RENDEZVOUS_HOST + '/tcp/' + REACT_APP_RENDEZVOUS_PORT + '/ws/p2p-websocket-star',
]
}
},
preload: {
enabled: false
},
init:{
emptyRepo: true
}
};
export default ipfsOptions;

12
app/src/helpers/EpochTimeConverter.js

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

33
app/src/index.js

@ -1,33 +0,0 @@
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 './components/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

6
app/src/redux/actions/drizzleActions.js

@ -1,6 +0,0 @@
// Actions that are fired internally by Drizzle
const DRIZZLE_INITIALIZED = 'DRIZZLE_INITIALIZED';
const ACCOUNTS_FETCHED = 'ACCOUNTS_FETCHED';
const EVENT_FIRED = 'EVENT_FIRED';
export { ACCOUNTS_FETCHED, DRIZZLE_INITIALIZED, EVENT_FIRED }

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

@ -1,4 +0,0 @@
const DRIZZLE_UTILS_SAGA_INITIALIZED = 'DRIZZLE_UTILS_SAGA_INITIALIZED';
const DRIZZLE_UTILS_SAGA_ERROR = 'DRIZZLE_UTILS_SAGA_ERROR';
export { DRIZZLE_UTILS_SAGA_INITIALIZED, DRIZZLE_UTILS_SAGA_ERROR }

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

@ -1,38 +0,0 @@
const IPFS_INITIALIZED = 'IPFS_INITIALIZED';
const DATABASES_CREATED = 'DATABASES_CREATED';
const DATABASES_LOADED = 'DATABASES_LOADED';
const ADD_PEER_DATABASE = 'ADD_PEER_DATABASE';
const PEER_DATABASE_ADDED = 'PEER_DATABASE_ADDED';
const UPDATE_PEERS = 'UPDATE_PEERS';
const ORBIT_INIT = 'ORBIT_INIT';
const ORBIT_SAGA_ERROR = 'ORBIT_SAGA_ERROR';
function updateDatabases(type, orbitdb, topicsDB, postsDB) {
return {
type,
orbitdb,
topicsDB,
postsDB,
id: orbitdb.id
};
}
function addPeerDatabase(userAddress, dbName) {
return {
type: ADD_PEER_DATABASE,
userAddress, //User's Ethereum address - it's also his Orbit Identity Id
dbName //e.g. topics or posts
};
}
export { DATABASES_CREATED,
DATABASES_LOADED,
IPFS_INITIALIZED,
UPDATE_PEERS,
ADD_PEER_DATABASE,
PEER_DATABASE_ADDED,
ORBIT_INIT,
ORBIT_SAGA_ERROR,
addPeerDatabase,
updateDatabases
};

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

@ -1,53 +0,0 @@
// 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
};
}

6
app/src/redux/actions/userActions.js

@ -1,6 +0,0 @@
const ACCOUNT_CHANGED = 'ACCOUNT_CHANGED';
const AUTH_USER_DATA_UPDATED = 'AUTH_USER_DATA_UPDATED';
const GUEST_USER_DATA_UPDATED = 'GUEST_USER_DATA_UPDATED';
const USER_FETCHING_ERROR = 'USER_FETCHING_ERROR';
export { ACCOUNT_CHANGED, AUTH_USER_DATA_UPDATED, GUEST_USER_DATA_UPDATED, USER_FETCHING_ERROR };

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

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

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

@ -1,73 +0,0 @@
import {
DATABASES_CREATED,
DATABASES_LOADED,
IPFS_INITIALIZED, UPDATE_PEERS, PEER_DATABASE_ADDED, ORBIT_INIT
} from '../actions/orbitActions';
const initialState = {
ipfs: null,
ipfsInitialized: false,
ready: false,
orbitdb: null,
topicsDB: null,
postsDB: null,
pubsubPeers: {topicsDBPeers:[], postsDBPeers:[]},
peerDatabases: [],
id: null
};
const orbitReducer = (state = initialState, action) => {
switch (action.type) {
case IPFS_INITIALIZED:
return {
...state,
ipfs: action.ipfs,
ipfsInitialized: true
};
case DATABASES_CREATED:
case DATABASES_LOADED:
return {
...state,
ready: true,
orbitdb: action.orbitdb,
topicsDB: action.topicsDB,
postsDB: action.postsDB,
id: action.id
};
case ORBIT_INIT:
return {
...state,
ready: false,
orbitdb: null,
topicsDB: null,
postsDB: null,
pubsubPeers: {topicsDBPeers:[], postsDBPeers:[]},
peerDatabases: [],
id: null
};
case PEER_DATABASE_ADDED:
if(state.peerDatabases.find(db => db.fullAddress === action.fullAddress))
return state;
console.debug(`Added peer database ${action.fullAddress}`);
return {
...state,
peerDatabases:[...state.peerDatabases,
{
fullAddress: action.fullAddress,
userAddress: action.userAddress,
name: action.fullAddress.split('/')[3],
store: action.store
}
]
};
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

@ -1,14 +0,0 @@
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

@ -1,18 +0,0 @@
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;

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

@ -1,29 +0,0 @@
import { AUTH_USER_DATA_UPDATED, GUEST_USER_DATA_UPDATED } from '../actions/userActions';
const initialState = {
username: '',
address: '0x0',
avatarUrl: '',
hasSignedUp: false
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case AUTH_USER_DATA_UPDATED:
return {
username: action.username,
address: action.address,
hasSignedUp: true
};
case GUEST_USER_DATA_UPDATED:
return {
username: '',
address: action.address,
hasSignedUp: false
};
default:
return state;
}
};
export default userReducer;

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

@ -1,53 +0,0 @@
import { call, put, select } from 'redux-saga/effects';
import getContractInstance from "@drizzle-utils/get-contract-instance";
import getWeb3 from "@drizzle-utils/get-web3";
import Web3 from "web3";
import Forum from '../../contracts/Forum';
import {
DRIZZLE_UTILS_SAGA_ERROR,
DRIZZLE_UTILS_SAGA_INITIALIZED
} from '../actions/drizzleUtilsActions';
import { DRIZZLE_INITIALIZED } from '../actions/drizzleActions';
import { fork, take } from 'redux-saga/effects';
const accounts = state => state.accounts;
let initFlag, web3, forumContract;
function* init() {
if (!initFlag) {
try{
const host = "http://127.0.0.1:8545"; //Ganache development blockchain
const fallbackProvider = new Web3.providers.HttpProvider(host);
web3 = yield call(getWeb3, { fallbackProvider });
forumContract = yield call(getContractInstance, { web3, artifact: Forum });
initFlag = true;
yield put({
type: DRIZZLE_UTILS_SAGA_INITIALIZED, ...[]
});
}
catch (error) {
console.error(`Error while initializing drizzleUtilsSaga: ${error}`);
yield put({
type: DRIZZLE_UTILS_SAGA_ERROR, ...[]
});
}
}
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
function* getCurrentAccount() {
return (yield select(accounts))[0];
}
function* drizzleUtilsSaga() {
yield take(DRIZZLE_INITIALIZED);
yield fork(init);
}
export { web3, forumContract, getCurrentAccount };
export default drizzleUtilsSaga;

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

@ -1,22 +0,0 @@
import { put, takeEvery } from 'redux-saga/effects';
import { EVENT_FIRED } from '../actions/drizzleActions';
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 }

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

@ -1,107 +0,0 @@
import { all, call, put, select, take, takeEvery, takeLatest } from 'redux-saga/effects';
import isEqual from 'lodash.isequal';
import { forumContract, getCurrentAccount } from './drizzleUtilsSaga';
import {
createDatabases, determineDBAddress,
loadDatabases,
orbitSagaOpen
} from '../../utils/orbitUtils';
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions';
import {
ADD_PEER_DATABASE, PEER_DATABASE_ADDED,
DATABASES_CREATED,
IPFS_INITIALIZED,
UPDATE_PEERS, ORBIT_INIT, ORBIT_SAGA_ERROR, updateDatabases
} from '../actions/orbitActions';
import { ACCOUNT_CHANGED } from '../actions/userActions';
import { ACCOUNTS_FETCHED } from '../actions/drizzleActions';
let latestAccount;
function* getOrbitDBInfo() {
yield put.resolve({
type: ORBIT_INIT, ...[]
});
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) {
yield call(loadDatabases, account);
} else {
const orbit = yield select(state => state.orbit);
if(!orbit.ready){
const { orbitdb, topicsDB, postsDB } = yield call(createDatabases, account);
yield put(updateDatabases(DATABASES_CREATED, orbitdb, topicsDB, postsDB ));
}
}
latestAccount = account;
} catch (error) {
console.error(error);
yield put({
type: ORBIT_SAGA_ERROR, ...[]
});
}
}
}
let peerOrbitAddresses = new Set();
function* addPeerDatabase(action) {
const userAddress = action.userAddress;
const dbName = action.dbName;
const size = peerOrbitAddresses.size;
peerOrbitAddresses.add(userAddress + '/' + dbName);
if(peerOrbitAddresses.size>size){
const { orbitdb } = yield select(state => state.orbit);
if(orbitdb){
const dbAddress = yield call(determineDBAddress,dbName, userAddress);
const fullAddress = `/orbitdb/${dbAddress}/${dbName}`;
const store = yield call(orbitSagaOpen, orbitdb, fullAddress);
yield put({
type: PEER_DATABASE_ADDED, fullAddress, userAddress, store
});
}
}
}
//Keeps track of currently connected pubsub peers in Redux store (for debugging purposes)
//Feel free to disable it anytime
function* updatePeersState() {
const orbit = yield select(state => state.orbit);
if(orbit.ready){
// This try is here to ignore concurrency errors that arise from times to times
try{
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
});
}
} catch (e) {
// No need to catch anything
}
}
}
function* orbitSaga() {
yield all([
take(DRIZZLE_UTILS_SAGA_INITIALIZED),
take(IPFS_INITIALIZED)
]);
yield takeLatest(ACCOUNT_CHANGED, getOrbitDBInfo);
yield takeEvery(ADD_PEER_DATABASE, addPeerDatabase);
if(process.env.NODE_ENV==='development')
yield takeEvery(ACCOUNTS_FETCHED, updatePeersState);
}
export default orbitSaga;

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

@ -1,21 +0,0 @@
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)),
);
}

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

@ -1,83 +0,0 @@
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';
import { getCurrentAccount } from './drizzleUtilsSaga';
const transactionsHistory = Object.create(null);
function* initTransaction(action) {
const account = yield call(getCurrentAccount);
const dataKey = drizzle.contracts[action.transactionDescriptor.contract]
.methods[action.transactionDescriptor.method].cacheSend(
...action.transactionDescriptor.params, { from: account }
);
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;

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

@ -1,68 +0,0 @@
import { call, put, select, take, takeEvery } from 'redux-saga/effects';
import { forumContract, getCurrentAccount } from './drizzleUtilsSaga';
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions';
import {
ACCOUNT_CHANGED,
AUTH_USER_DATA_UPDATED,
GUEST_USER_DATA_UPDATED,
USER_FETCHING_ERROR
} from '../actions/userActions';
import { ACCOUNTS_FETCHED } from '../actions/drizzleActions';
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: AUTH_USER_DATA_UPDATED, ...dispatchArgs
});
}
} else if (account !== userState.address) {
const dispatchArgs = {
address: account
};
yield put({
type: GUEST_USER_DATA_UPDATED, ...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

@ -1,35 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,31 +0,0 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import CoreLayoutContainer from '../components/CoreLayoutContainer';
import HomeContainer from '../components/HomeContainer';
import SignUpContainer from '../components/SignUpContainer';
import StartTopicContainer from '../components/StartTopicContainer';
import TopicContainer from '../components/TopicContainer';
import ProfileContainer from '../components/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;

64
app/src/utils/ethereumIdentityProvider.js

@ -1,64 +0,0 @@
import { web3 } from '../redux/sagas/drizzleUtilsSaga';
import { getIdentitySignaturePubKey, storeIdentitySignaturePubKey } from './levelUtils';
class EthereumIdentityProvider {
constructor (options = {}) { // Orbit's Identity Id (equals user's Ethereum address)
this.id = options.id; // web3.eth.getAccounts())[0]
}
static get type () { return 'ethereum'; }
async getId () { return this.id; }
async signIdentity (data) {
if(process.env.NODE_ENV==='development') {
console.debug("Attempting to find stored Orbit identity data...");
const signaturePubKey = await getIdentitySignaturePubKey(data);
if (signaturePubKey) {
if (EthereumIdentityProvider.verifyIdentityInfo({
id: this.id,
pubKeySignId: data,
signaturePubKey
})) {
console.debug("Found and verified stored Orbit identity data!");
return signaturePubKey;
}
console.debug("Stored Orbit identity data couldn't be verified.");
}
else
console.debug("No stored Orbit identity data were found.");
}
while(true){ //Insist (e.g. if user dismisses dialog)
try{
const signaturePubKey = await web3.eth.personal.sign(data, this.id,"");
if(process.env.NODE_ENV==='development')
storeIdentitySignaturePubKey(data, signaturePubKey)
.then(()=>{
console.debug("Successfully stored current Orbit identity data.");
})
.catch(()=>{
console.warn("Couldn't store current Orbit identity data...");
});
return signaturePubKey; //Password not required for MetaMask
}
catch (e) {
console.error("Failed to sign data.");
}
}
}
static async verifyIdentity (identity) {
// Verify that identity was signed by the ID
return web3.eth.accounts.recover(identity.publicKey + identity.signatures.id,
identity.signatures.publicKey) === identity.id;
}
static async verifyIdentityInfo (identityInfo) {
// Verify that identity was signed by the ID
return web3.eth.accounts.recover(identityInfo.pubKeySignId,
identityInfo.signaturePubKey) === identityInfo.id;
}
}
export default EthereumIdentityProvider;

109
app/src/utils/orbitUtils.js

@ -1,109 +0,0 @@
import OrbitDB from 'orbit-db';
import Identities from 'orbit-db-identity-provider';
import IPFS from 'ipfs';
import store from '../redux/store';
import { DATABASES_LOADED, IPFS_INITIALIZED, updateDatabases } from '../redux/actions/orbitActions';
import ipfsOptions from '../config/ipfsOptions';
import EthereumIdentityProvider from './ethereumIdentityProvider';
function initIPFS() {
Identities.addIdentityProvider(EthereumIdentityProvider);
const ipfs = new IPFS(ipfsOptions);
ipfs.on('error', (error) => console.error(`IPFS error: ${error}`));
ipfs.on('ready', async () => {
store.dispatch({
type: IPFS_INITIALIZED, ipfs
});
ipfs.id(function (error, identity) {
if (error)
console.error(`IPFS id() error: ${error}`);
console.debug(`IPFS initialized with id ${identity.id}`);
})
});
}
async function createDatabases(identityId) {
console.debug('Creating databases...');
const databases = await createDBs(identityId);
console.debug('Databases created successfully.');
return databases;
}
async function loadDatabases(identityId) {
console.debug('Loading databases...');
const { orbitdb, topicsDB, postsDB } = await createDBs(identityId);
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('Databases loaded successfully.');
store.dispatch(updateDatabases(DATABASES_LOADED, orbitdb, topicsDB, postsDB));
}
async function determineDBAddress(dbName, identityId){
return (await getOrbitDB().determineAddress(dbName, 'keyvalue', {
accessController: {
write: [identityId]}
}
)).root;
}
function getIPFS() {
return store.getState().orbit.ipfs;
}
function getOrbitDB() {
return store.getState().orbit.orbitdb;
}
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;
}
async function createDBs(identityId){
const ipfs = getIPFS();
const identity = await Identities.createIdentity({id: identityId, type: 'ethereum'});
const orbitdb = await OrbitDB.createInstance(ipfs, {identity});
const topicsDB = await orbitdb.keyvalue('topics')
.catch((error) => console.error(`TopicsDB init error: ${error}`));
const postsDB = await orbitdb.keyvalue('posts')
.catch((error) => console.error(`PostsDB init error: ${error}`));
return { orbitdb, topicsDB, postsDB };
}
export {
initIPFS,
createDatabases,
loadDatabases,
orbitSagaPut,
orbitSagaOpen,
determineDBAddress
};

130
app/src/utils/serviceWorker.js

@ -1,130 +0,0 @@
// 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();
});
}
}

23
contracts/Migrations.sol

@ -1,23 +0,0 @@
pragma solidity >=0.5.8 <0.6.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
constructor() public {
owner = msg.sender;
}
modifier restricted() {
if (msg.sender == owner) _;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

96
docker-compose.yml

@ -1,96 +0,0 @@
version: '3.7'
services:
ganache:
build: ./ganache
container_name: ganache
restart: always
ports:
- "8545:8545"
user: root
volumes:
- ./volumes/ganache_db:/home/ganache_db
- ./volumes/ganache_keys:/home/ganache_keys
# Simple rendezvous server image
# Reference:
# https://hub.docker.com/r/libp2p/websocket-star-rendezvous
rendezvous:
image: libp2p/websocket-star-rendezvous:release
container_name: rendezvous
restart: always
ports:
- "9090:9090"
apella:
build: ./
container_name: apella-app
restart: always
env_file:
- ./env/apella.env
networks:
- apella-net
expose:
- "3000"
# Nginx reverse proxy container
# Reference:
# https://github.com/jwilder/nginx-proxy
nginx-proxy: # TODO: maybe split this to the two underlying images?
image: jwilder/nginx-proxy
container_name: apella-nginx-proxy
restart: always
environment:
- DEFAULT_HOST=apella.tk
labels:
com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
logging:
options:
max-size: '4m'
max-file: '10'
networks:
- apella-net
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- dhparam:/etc/nginx/dhparam
- certs:/etc/nginx/certs:ro
# Letsencrypt automated creation, renewal and use of Let's Encrypt certificates
# Reference:
# https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion
letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: apella-proxy-le
restart: always
depends_on:
- nginx-proxy
logging:
options:
max-size: '4m'
max-file: '10'
networks:
- apella-net
volumes:
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- dhparam:/etc/nginx/dhparam
- certs:/etc/nginx/certs:rw
- /var/run/docker.sock:/var/run/docker.sock:ro
# Networks in use
networks:
apella-net:
driver: bridge
volumes:
conf:
vhost:
html:
dhparam:
certs:

23
env/apella.env.example

@ -1,23 +0,0 @@
# Docker compose variables
VIRTUAL_HOST=example.com
VIRTUAL_PORT=3000
# If you uncomment the lines below, Apella will become available through https BUT the rendezvous
# server will stop working and IPFS initialization won't complete
#LETSENCRYPT_HOST=example.com
#LETSENCRYPT_EMAIL=someemail.email.com
# Variables needed in build time
# to_never_do: change APELLA_HOST to localhost
APELLA_HOST=0.0.0.0
APELLA_PORT=3000
GANACHE_HOST=xx.xxx.xxx.xxx
GANACHE_PORT=8545
# react-scripts host and port vars
HOST=0.0.0.0
PORT=3000
# Variables needed in runtime (in browser)
REACT_APP_RENDEZVOUS_HOST=xx.xxx.xxx.xxx
REACT_APP_RENDEZVOUS_PORT=9090

5
ganache/Dockerfile

@ -1,5 +0,0 @@
FROM trufflesuite/ganache-cli:latest
RUN mkdir /home/ganache_db /home/ganache_keys
ENTRYPOINT ["node", "/app/ganache-core.docker.cli.js", "-a", "10", "-e", "1000", "-d", "-m", "measure tree magic expire dad extend famous offer slight glory inherit weekend", "-p", "8545", "-i", "5778", "--db", "/home/ganache_db/", "-v", "--account_keys_path", "/home/ganache_keys/keys", "--allowUnlimitedContractSize", "--noVMErrorsOnRPCResponse"]

13
ganache/docker-compose.yml

@ -1,13 +0,0 @@
version: '3.7'
services:
ganache:
build: ./
container_name: ganache
restart: always
ports:
- "8545:8545"
user: root
volumes:
- ./volumes/ganache_db:/home/ganache_db
- ./volumes/ganache_keys:/home/ganache_keys

9
migrateAndStart.sh

@ -1,9 +0,0 @@
#!/bin/bash
# Migrates contracts
rm -f /usr/apella/app/src/contracts/*
cd /usr/apella/
truffle migrate
cd /usr/apella/app/
yarn start

5
migrations/1_initial_migration.js

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

5
migrations/2_deploy_contracts.js

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

27
package.json

@ -1,27 +1,8 @@
{ {
"name": "apella-box", "name": "apella",
"version": "0.1.0",
"description": "",
"private": true, "private": true,
"repository": { "workspaces": {
"type": "git", "packages": ["packages/*"],
"url": "https://gitlab.com/Ezerous/Apella.git" "nohoist": ["**/web3", "**/web3/**"]
},
"main": "truffle-config.js",
"directories": {
"test": "test"
},
"dependencies": {
"openzeppelin-solidity": "^2.2.0"
},
"devDependencies": {
"eslint": "5.16.0",
"eslint-config-airbnb": "17.1.0",
"eslint-plugin-import": "2.17.2",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.13.0"
},
"scripts": {
"lint": "eslint app/src --format table"
} }
} }

52
packages/concordia-app/package.json

@ -0,0 +1,52 @@
{
"name": "concordia-app",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"postinstall": "patch-package",
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@ezerous/breeze": "~0.2.0",
"@ezerous/drizzle": "~0.4.0",
"@reduxjs/toolkit": "~1.4.0",
"concordia-contracts": "~0.1.0",
"level": "~6.0.1",
"orbit-db-identity-provider": "~0.3.1",
"prop-types": "~15.7.2",
"react": "~16.13.1",
"react-dom": "~16.13.1",
"react-redux": "~7.2.1",
"react-router": "~5.2.0",
"react-router-dom": "~5.2.0",
"react-scripts": "~3.4.3",
"redux-saga": "~1.1.3",
"semantic-ui-css": "~2.4.1",
"semantic-ui-react": "~1.2.1",
"web3": "1.3.0"
},
"devDependencies": {
"patch-package": "~6.2.2",
"postinstall-postinstall": "~2.1.0",
"source-map-explorer": "~2.5.0"
}
}

15
packages/concordia-app/patches/web3-eth+1.3.0.patch

@ -0,0 +1,15 @@
diff --git a/node_modules/web3-eth/lib/index.js b/node_modules/web3-eth/lib/index.js
index da8a65f..06d5f83 100644
--- a/node_modules/web3-eth/lib/index.js
+++ b/node_modules/web3-eth/lib/index.js
@@ -288,8 +288,8 @@ var Eth = function Eth() {
this.Iban = Iban;
// add ABI
this.abi = abi;
- // add ENS
- this.ens = new ENS(this);
+ // add ENS (Removed because of https://github.com/ethereum/web3.js/issues/2665#issuecomment-687164093)
+ // this.ens = new ENS(this);
var methods = [
new Method({
name: 'getNodeInfo',

0
app/public/favicon.ico → packages/concordia-app/public/favicon.ico

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

2
app/public/index.html → packages/concordia-app/public/index.html

@ -20,7 +20,7 @@
Learn how to configure a non-root public URL by running `npm run build`. 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" /> <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" />
<title>Apella</title> <title>Concordia</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

0
app/public/manifest.json → packages/concordia-app/public/manifest.json

2
packages/concordia-app/public/robots.txt

@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

3
packages/concordia-app/src/assets/css/app.css

@ -0,0 +1,3 @@
body {
margin: 1em !important;
}

0
app/src/assets/css/index.css → packages/concordia-app/src/assets/css/index.css

28
packages/concordia-app/src/assets/css/loading-component.css

@ -0,0 +1,28 @@
body {
overflow: hidden;
}
ul {
list-style-position: inside;
}
.loading-screen {
margin-top: 10em;
text-align: center;
font-size: large;
}
.loading-img {
margin-bottom: 3em;
height: 12em;
}
.ui.container {
height: 26em;
}
.ui.progress {
width: 40vw;
margin-left: auto !important;
margin-right: auto !important;
}

0
app/src/assets/images/PageNotFound.jpg → packages/concordia-app/src/assets/images/PageNotFound.jpg

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

0
app/src/assets/images/logo.png → packages/concordia-app/src/assets/images/app_logo.png

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

0
app/src/assets/images/ethereum_logo.svg → packages/concordia-app/src/assets/images/ethereum_logo.svg

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

0
app/src/assets/images/ipfs_logo.svg → packages/concordia-app/src/assets/images/ipfs_logo.svg

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

0
app/src/assets/images/orbitdb_logo.png → packages/concordia-app/src/assets/images/orbitdb_logo.png

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

34
packages/concordia-app/src/components/App.jsx

@ -0,0 +1,34 @@
import React from 'react'
import { Provider } from 'react-redux'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import LoadingContainer from './LoadingContainer'
import PropTypes from 'prop-types'
// CSS
import '../assets/css/app.css';
import CoreLayoutContainer from './CoreLayoutContainer';
import HomeContainer from './HomeContainer';
import NotFound from '../components/NotFound';
const App = ({ store }) => (
<Provider store={store}>
<LoadingContainer>
<Router>
<CoreLayoutContainer>
<Switch>
<Route exact path="/" component={HomeContainer} />
<Route component={NotFound} />
</Switch>
</CoreLayoutContainer>
</Router>
</LoadingContainer>
</Provider>
)
App.propTypes = {
store: PropTypes.object.isRequired
}
export default App

72
packages/concordia-app/src/components/AppContext.js

@ -0,0 +1,72 @@
// Modified version of https://github.com/trufflesuite/drizzle/blob/develop/packages/react-plugin/src/DrizzleContext.js
import React from "react";
const Context = React.createContext();
class Provider extends React.Component {
state = {
drizzleState: null,
drizzleInitialized: false,
breezeState: null,
breezeInitialized: false
};
componentDidMount() {
const { drizzle, breeze } = this.props;
// subscribe to changes in the store, keep state up-to-date
this.unsubscribe = drizzle.store.subscribe(() => {
const drizzleState = drizzle.store.getState();
const breezeState = breeze.store.getState();
if (drizzleState.drizzleStatus.initialized) {
this.setState({
drizzleState,
drizzleInitialized: true
});
}
if (breezeState.breezeStatus.initialized) {
this.setState({
breezeState: breezeState,
breezeInitialized: true
});
}
});
this.unsubscribe = breeze.store.subscribe(() => {
const breezeState = breeze.store.getState();
if (breezeState.breezeStatus.initialized) {
this.setState({
breezeState: breezeState,
breezeInitialized: true
});
}
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<Context.Provider
value={{
drizzle: this.props.drizzle,
drizzleState: this.state.drizzleState,
drizzleInitialized: this.state.drizzleInitialized,
breeze: this.props.breeze,
breezeState: this.state.breezeState,
breezeInitialized: this.state.breezeInitialized
}}
>
{this.props.children}
</Context.Provider>
);
}
}
export default {
Context: Context,
Consumer: Context.Consumer,
Provider
};

19
packages/concordia-app/src/components/CoreLayoutContainer.jsx

@ -0,0 +1,19 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import MenuComponent from './MenuComponent';
export default class CoreLayout extends Component {
render() {
return (
<div>
<MenuComponent/>
{this.props.children}
</div>
)
}
}
CoreLayout.propTypes = {
children: PropTypes.element.isRequired
};

9
packages/concordia-app/src/components/HomeContainer.jsx

@ -0,0 +1,9 @@
import React, { Component } from 'react';
class HomeContainer extends Component {
render() {
return(<p>TODO: Home Container</p>);
}
}
export default HomeContainer;

73
packages/concordia-app/src/components/LoadingComponent.jsx

@ -0,0 +1,73 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Container, Progress } from 'semantic-ui-react';
// CSS
import '../assets/css/loading-component.css';
// Images
import ethereum_logo from '../assets/images/ethereum_logo.svg';
import ipfs_logo from '../assets/images/ipfs_logo.svg';
import orbitdb_logo from '../assets/images/orbitdb_logo.png';
import app_logo from '../assets/images/app_logo.png';
class LoadingComponent extends Component {
render(){
const { image_type, message_list, progress_type } = this.props ;
let imageSrc, imageAlt, listItems, indicating, error;
if (image_type === "ethereum"){
imageSrc = ethereum_logo;
imageAlt = "ethereum_logo";
}
else if (image_type === "ipfs"){
imageSrc = ipfs_logo;
imageAlt = "ipfs_logo";
}
else if (image_type === "orbit"){
imageSrc = orbitdb_logo;
imageAlt = "orbitdb_logo";
}
else if (image_type === "app"){
imageSrc = app_logo;
imageAlt = "app_logo";
}
if(progress_type === "indicating")
indicating = true;
else if(progress_type === "error")
error = true;
if(message_list){
listItems = message_list.map((listItem) =>
<li>{listItem}</li>
);
}
const list = message_list ? <ul>{listItems}</ul> : '';
return(
<main className="loading-screen">
<Container>
<img src={imageSrc} alt={imageAlt} className="loading-img" />
<p><strong>{this.props.title}</strong></p>
<p>{this.props.message}</p>
{list}
</Container>
<Progress percent={this.props.progress} size='small' indicating={indicating} error={error}/>
</main>
);
}
}
LoadingComponent.propTypes = {
title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
message_list: PropTypes.arrayOf(PropTypes.string),
image_type: PropTypes.string.isRequired,
progress: PropTypes.number.isRequired,
progress_type: PropTypes.string.isRequired,
};
export default LoadingComponent;

136
packages/concordia-app/src/components/LoadingContainer.jsx

@ -0,0 +1,136 @@
import React, { Children, Component } from 'react';
import { connect } from 'react-redux';
import { breezeConstants } from '@ezerous/breeze'
import LoadingComponent from './LoadingComponent';
// CSS
import '../assets/css/loading-component.css';
class LoadingContainer extends Component {
render() {
if ((this.props.web3.status === 'initializing' || !this.props.web3.networkId)
&& !this.props.web3.networkFailed) {
return <LoadingComponent
title="Connecting to the Ethereum network..."
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account."
image_type="ethereum"
progress={20}
progress_type="indicating"
/>
}
if (this.props.web3.status === 'failed' || this.props.web3.networkFailed) {
return <LoadingComponent
title="No connection to the Ethereum network!"
message="Please make sure that:"
message_list={['MetaMask is unlocked and pointed to the correct, available network',
'The app has been granted the right to connect to your account']}
image_type="ethereum"
progress={20}
progress_type="error"
/>
}
if (this.props.web3.status === 'initialized' && this.props.web3.accountsFailed) {
return <LoadingComponent
title="We can't find any Ethereum accounts!"
message="Please make sure that MetaMask is unlocked."
image_type="ethereum"
progress={20}
progress_type="error"
/>
}
if (this.props.drizzleStatus.initializing
|| (!this.props.drizzleStatus.failed && !this.props.contractInitialized && this.props.contractDeployed )){
return <LoadingComponent
title="Initializing contracts..."
message=""
image_type="ethereum"
progress={40}
progress_type="indicating"
/>
}
if (!this.props.contractDeployed) {
return <LoadingComponent
title="No contracts found on the current network!"
message="Please make sure that you are connected to the correct network and the contracts are deployed."
image_type="ethereum"
progress={40}
progress_type="error"
/>
}
if (this.props.ipfsStatus === breezeConstants.STATUS_INITIALIZING) {
return <LoadingComponent
title="Initializing IPFS..."
message=""
image_type="ipfs"
progress={60}
progress_type="indicating"
/>
}
if (this.props.ipfsStatus === breezeConstants.STATUS_FAILED) {
return <LoadingComponent
title="IPFS initialization failed!"
message=""
image_type="ipfs"
progress={60}
progress_type="error"
/>
}
if (this.props.orbitStatus === breezeConstants.STATUS_INITIALIZING) {
const message = process.env.NODE_ENV === 'development'
? 'If needed, please sign the transaction in MetaMask to create the databases.'
: 'Please sign the transaction in MetaMask to create the databases.';
return <LoadingComponent
title="Preparing OrbitDB..."
message={message}
image_type="orbit"
progress={80}
progress_type="indicating"
/>
}
if (this.props.orbitStatus === breezeConstants.STATUS_FAILED) {
return <LoadingComponent
title="OrbitDB initialization failed!"
message=""
image_type="orbit"
progress={80}
progress_type="error"
/>
}
if (!this.props.userFetched){
return <LoadingComponent
title="Loading dapp..."
message=""
image_type="app"
progress={90}
progress_type="indicating"
/>
}
return Children.only(this.props.children);
}
}
const mapStateToProps = (state) => ({
drizzleStatus: state.drizzleStatus,
breezeStatus: state.breezeStatus,
ipfsStatus: state.ipfs.status,
orbitStatus: state.orbit.status,
web3: state.web3,
accounts: state.accounts,
contractInitialized: state.contracts.Forum.initialized,
contractDeployed: state.contracts.Forum.deployed,
userFetched: state.user.address
});
export default connect(mapStateToProps)(LoadingContainer);

38
packages/concordia-app/src/components/MenuComponent.jsx

@ -0,0 +1,38 @@
import React, { Component } from 'react';
import { withRouter } from "react-router";
import { Menu } from 'semantic-ui-react';
import AppContext from "./AppContext";
import app_logo from '../assets/images/app_logo.png';
import SignUpForm from './SignUpForm';
class MenuComponent extends Component {
render() {
return (
<AppContext.Consumer>
{context => {
return(
<div>
<Menu color='black' inverted>
<Menu.Item
link
name='home'
onClick={() => { this.props.history.push("/"); }}
>
<img src={app_logo} alt="app_logo"/>
</Menu.Item>
<SignUpForm/>
</Menu>
</div>
)
}
}
</AppContext.Consumer>
)
}
}
export default withRouter(MenuComponent);

12
app/src/components/NotFound.js → packages/concordia-app/src/components/NotFound.jsx

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

139
packages/concordia-app/src/components/SignUpForm.jsx

@ -0,0 +1,139 @@
import React, { Component } from 'react';
import { Button, Form, Menu, Message, Modal } from 'semantic-ui-react';
import AppContext from "./AppContext";
import { connect } from 'react-redux';
const contractName = 'Forum';
const checkUsernameTakenMethod = 'isUserNameTaken';
const signUpMethod = 'signUp';
class SignUpForm extends Component {
constructor(props, context) {
super(props, context);
// For quick access
this.contract = this.context.drizzle.contracts[contractName];
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;
}
}
this.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) {
// TODO
// // Makes sure current input username has been checked for availability
// if (this.checkedUsernames.some((e) => e.usernameChecked === usernameInput)) {
// this.completeAction();
// }
this.completeAction();
}
}
componentDidUpdate() {
// TODO
}
completeAction() {
const { usernameInput } = this.state;
const { user, account } = this.props;
if (user.hasSignedUp) {
console.log('Signing up..')
this.contract.methods['signUp'].cacheSend(usernameInput);
} else {
this.setState({
signingUp: true,
});
this.contract.methods[signUpMethod].cacheSend(
...[usernameInput], { from: account },
);
}
this.setState({
usernameInput: '',
});
}
render() {
const {
error, usernameInput, errorHeader, errorMessage, signingUp,
} = this.state;
return(
<Modal as={Form} onSubmit={e => this.handleSubmit(e)} trigger={
<Menu.Item
name='signup'
position='right'
content='Sign Up'
/>
}>
<Modal.Header>Sign Up</Modal.Header>
<Modal.Content>
<Form.Field required>
<label>Username</label>
<Form.Input
placeholder='Username'
name="usernameInput"
value={usernameInput}
onChange={this.handleInputChange}
/>
</Form.Field>
<Message
error
header={errorHeader}
content={errorMessage}
/>
<Button type="submit" color="black" content="Sign Up" />
</Modal.Content>
</Modal>
)
}
}
SignUpForm.contextType = AppContext.Context;
const mapStateToProps = (state) => ({
user: state.user
});
export default connect(mapStateToProps)(SignUpForm);

29
packages/concordia-app/src/index.js

@ -0,0 +1,29 @@
import React from 'react';
import { render } from 'react-dom';
import App from './components/App'
import store from './redux/store';
import { Drizzle } from '@ezerous/drizzle'
import { Breeze } from '@ezerous/breeze'
import AppContext from "./components/AppContext";
import drizzleOptions from './options/drizzleOptions';
import * as serviceWorker from './utils/serviceWorker';
import './assets/css/index.css';
import breezeOptions from './options/breezeOptions';
const drizzle = new Drizzle(drizzleOptions, store);
const breeze = new Breeze(breezeOptions, store);
render(
<AppContext.Provider drizzle={drizzle} breeze={breeze}>
<App store={store} />
</AppContext.Provider>,
document.getElementById('root')
);
serviceWorker.unregister(); // See also: http://bit.ly/CRA-PWA

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

Loading…
Cancel
Save