@ -1,7 +0,0 @@ |
|||||
env/ |
|
||||
node_modules/ |
|
||||
package-lock.json |
|
||||
yarn.lock |
|
||||
app/node_modules/ |
|
||||
app/package-lock.json |
|
||||
app/yarn.lock |
|
@ -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 +0,0 @@ |
|||||
# node_modules in the project root is ignored by default |
|
@ -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"] |
|
@ -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"` |
|
@ -1,3 +0,0 @@ |
|||||
# Apella |
|
||||
|
|
||||
*Note: This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).* |
|
@ -1,3 +0,0 @@ |
|||||
{ |
|
||||
"extends": "plugin:react/recommended" |
|
||||
} |
|
@ -1 +0,0 @@ |
|||||
node_modules/* |
|
@ -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" |
|
||||
] |
|
||||
} |
|
@ -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 }; |
|
@ -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%; |
|
||||
} |
|
@ -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; |
|
||||
} |
|
@ -1,8 +0,0 @@ |
|||||
.loading-screen { |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.loading-img { |
|
||||
margin-bottom: 30px; |
|
||||
height: 100px; |
|
||||
} |
|
@ -1,5 +0,0 @@ |
|||||
/* PROFILE SCREEN */ |
|
||||
|
|
||||
.profile-tab { |
|
||||
width: 100%; |
|
||||
} |
|
@ -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; |
|
||||
} |
|
@ -1,6 +0,0 @@ |
|||||
/* START TOPIC SCREEN */ |
|
||||
|
|
||||
.topic-form { |
|
||||
width: 100%; |
|
||||
margin: 20px 0px; |
|
||||
} |
|
@ -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; |
|
||||
} |
|
@ -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)); |
|
@ -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; |
|
@ -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; |
|
@ -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; |
|
@ -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); |
|
||||
|
|
@ -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; |
|
@ -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); |
|
@ -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); |
|
@ -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); |
|
@ -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); |
|
@ -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)); |
|
@ -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); |
|
@ -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); |
|
@ -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); |
|
@ -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); |
|
@ -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); |
|
@ -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)); |
|
@ -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); |
|
@ -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); |
|
@ -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; |
|
@ -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); |
|
@ -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; |
|
@ -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; |
|
@ -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; |
|
@ -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
|
|
@ -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 } |
|
@ -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 } |
|
@ -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 |
|
||||
}; |
|
@ -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 |
|
||||
}; |
|
||||
} |
|
@ -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 }; |
|
@ -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 |
|
||||
}; |
|
||||
} |
|
@ -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; |
|
@ -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 |
|
||||
}); |
|
@ -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; |
|
@ -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; |
|
@ -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; |
|
@ -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 } |
|
@ -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; |
|
@ -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)), |
|
||||
); |
|
||||
} |
|
@ -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; |
|
@ -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; |
|
@ -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; |
|
@ -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); |
|
@ -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; |
|
@ -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; |
|
@ -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 |
|
||||
}; |
|
@ -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(); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
@ -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); |
|
||||
} |
|
||||
} |
|
@ -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: |
|
@ -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 |
|
@ -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"] |
|
@ -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 |
|
@ -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 |
|
@ -1,5 +0,0 @@ |
|||||
const Migrations = artifacts.require("./Migrations.sol"); |
|
||||
|
|
||||
module.exports = function(deployer) { |
|
||||
deployer.deploy(Migrations); |
|
||||
}; |
|
@ -1,5 +0,0 @@ |
|||||
const Forum = artifacts.require("Forum"); |
|
||||
|
|
||||
module.exports = function(deployer) { |
|
||||
deployer.deploy(Forum); |
|
||||
}; |
|
@ -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" |
|
||||
} |
} |
||||
} |
} |
||||
|
@ -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" |
||||
|
} |
||||
|
} |
@ -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', |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,2 @@ |
|||||
|
# https://www.robotstxt.org/robotstxt.html |
||||
|
User-agent: * |
@ -0,0 +1,3 @@ |
|||||
|
body { |
||||
|
margin: 1em !important; |
||||
|
} |
@ -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; |
||||
|
} |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
@ -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 |
@ -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 |
||||
|
}; |
@ -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 |
||||
|
}; |
@ -0,0 +1,9 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
|
||||
|
class HomeContainer extends Component { |
||||
|
render() { |
||||
|
return(<p>TODO: Home Container</p>); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default HomeContainer; |
@ -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; |
@ -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); |
@ -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); |
@ -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); |
@ -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
|
||||
|
|
||||
|
|
||||
|
|