@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"extends": "airbnb", |
||||
|
"rules": { |
||||
|
"comma-dangle": ["error", "never"], |
||||
|
"no-console": "off", |
||||
|
"no-unused-vars": "warn", |
||||
|
"object-curly-newline": ["error", { |
||||
|
"ObjectExpression": "always", |
||||
|
"ObjectPattern": { "multiline": true }, |
||||
|
"ImportDeclaration": "never", |
||||
|
"ExportDeclaration": "never" |
||||
|
}], |
||||
|
"object-curly-spacing": ["error", "always"] |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
# node_modules in the project root is ignored by default |
@ -1 +1,7 @@ |
|||||
|
# Set the default behavior, in case people don't have core.autocrlf set. |
||||
* text=auto eol=lf |
* text=auto eol=lf |
||||
|
|
||||
|
# Denote all files that are truly binary and should not be modified. |
||||
|
*.png binary |
||||
|
*.jpg binary |
||||
|
*.ico binary |
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"extends": "plugin:react/recommended" |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
node_modules/* |
@ -0,0 +1,45 @@ |
|||||
|
{ |
||||
|
"name": "apella", |
||||
|
"version": "0.1.0", |
||||
|
"private": true, |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "https://gitlab.com/Ezerous/Apella.git" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"connected-react-router": "6.3.2", |
||||
|
"drizzle": "1.3.3", |
||||
|
"history": "4.9.0", |
||||
|
"ipfs": "github:ipfs/js-ipfs#e849dbcab4a313f7ffc1532a389097ee76344067", |
||||
|
"lodash.isequal": "4.5.0", |
||||
|
"orbit-db": "0.19.9", |
||||
|
"orbit-db-keystore": "0.1.0", |
||||
|
"prop-types": "15.7.2", |
||||
|
"react": "16.8.4", |
||||
|
"react-content-loader": "4.2.1", |
||||
|
"react-dom": "16.8.4", |
||||
|
"react-markdown": "4.0.6", |
||||
|
"react-redux": "6.0.1", |
||||
|
"react-router-dom": "5.0.0", |
||||
|
"react-scripts": "2.1.8", |
||||
|
"react-timeago": "4.4.0", |
||||
|
"react-user-avatar": "1.10.0", |
||||
|
"redux": "4.0.1", |
||||
|
"redux-saga": "0.16.2", |
||||
|
"semantic-ui-react": "0.86.0", |
||||
|
"uuid": "3.3.2", |
||||
|
"web3": "1.0.0-beta.50" |
||||
|
}, |
||||
|
"scripts": { |
||||
|
"start": "react-scripts start", |
||||
|
"build": "react-scripts build", |
||||
|
"test": "react-scripts test", |
||||
|
"eject": "react-scripts eject" |
||||
|
}, |
||||
|
"browserslist": [ |
||||
|
">0.2%", |
||||
|
"not dead", |
||||
|
"not ie <= 11", |
||||
|
"not op_mini all" |
||||
|
] |
||||
|
} |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,39 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="utf-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||
|
<meta name="theme-color" content="#000000"> |
||||
|
<!-- |
||||
|
manifest.json provides metadata used when your web app is added to the |
||||
|
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/ |
||||
|
--> |
||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> |
||||
|
<!-- |
||||
|
Notice the use of %PUBLIC_URL% in the tags above. |
||||
|
It will be replaced with the URL of the `public` folder during the build. |
||||
|
Only files inside the `public` folder can be referenced from the HTML. |
||||
|
|
||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
||||
|
work correctly both with client-side routing and a non-root public URL. |
||||
|
Learn how to configure a non-root public URL by running `npm run build`. |
||||
|
--> |
||||
|
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" /> |
||||
|
<title>Apella</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<noscript>You need to enable JavaScript to run this app.</noscript> |
||||
|
<div id="root"></div> |
||||
|
<!-- |
||||
|
This HTML file is a template. |
||||
|
If you open it directly in the browser, you will see an empty page. |
||||
|
|
||||
|
You can add webfonts, meta tags, or analytics to this file. |
||||
|
The build step will place the bundled scripts into the <body> tag. |
||||
|
|
||||
|
To begin the development, run `npm start` or `yarn start`. |
||||
|
To create a production bundle, use `npm run build` or `yarn build`. |
||||
|
--> |
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"short_name": "Apella", |
||||
|
"name": "Apella", |
||||
|
"icons": [ |
||||
|
{ |
||||
|
"src": "favicon.ico", |
||||
|
"sizes": "64x64 32x32 24x24 16x16", |
||||
|
"type": "image/x-icon" |
||||
|
} |
||||
|
], |
||||
|
"start_url": ".", |
||||
|
"display": "standalone", |
||||
|
"theme_color": "#000000", |
||||
|
"background_color": "#ffffff" |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
import PropTypes from 'prop-types'; |
||||
|
|
||||
|
const GetTopicResult = PropTypes.PropTypes.shape({ |
||||
|
0: PropTypes.string, |
||||
|
1: PropTypes.string, |
||||
|
2: PropTypes.string, |
||||
|
3: PropTypes.string, |
||||
|
4: PropTypes.arrayOf(PropTypes.number) |
||||
|
}); |
||||
|
|
||||
|
export { GetTopicResult }; |
@ -0,0 +1,187 @@ |
|||||
|
/* PAGE */ |
||||
|
|
||||
|
html, body { |
||||
|
margin: 0; |
||||
|
display: block; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
strong { |
||||
|
font-weight: bold !important; |
||||
|
} |
||||
|
|
||||
|
#root { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.App { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
margin: 0px; |
||||
|
display: flex; |
||||
|
flex-flow: column nowrap; |
||||
|
align-items: flex-start; |
||||
|
} |
||||
|
|
||||
|
.page-container { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
margin: 71px 0px 0px; |
||||
|
} |
||||
|
|
||||
|
.left-side-panel { |
||||
|
margin-top: 71px; |
||||
|
position: fixed; |
||||
|
width: 20%; |
||||
|
height: calc(100% - 71px); |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
} |
||||
|
|
||||
|
.main-panel { |
||||
|
width: 60%; |
||||
|
height: 100%; |
||||
|
margin: 0px 20%; |
||||
|
} |
||||
|
|
||||
|
.right-side-panel { |
||||
|
margin-top: 71px; |
||||
|
position: fixed; |
||||
|
width: 20%; |
||||
|
height: calc(100% - 71px); |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
} |
||||
|
|
||||
|
.sidebar-message { |
||||
|
margin: 0px 5px 12px 12px; |
||||
|
padding: 0px; |
||||
|
} |
||||
|
|
||||
|
.view-container { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
margin: 0px auto; |
||||
|
} |
||||
|
|
||||
|
/* MISC */ |
||||
|
|
||||
|
.navBarText { |
||||
|
height: 61px; |
||||
|
width: 1192px; |
||||
|
position: absolute; |
||||
|
left: calc(50% - 596px); |
||||
|
text-align: center; |
||||
|
z-index: -1; /* Temporary (?) */ |
||||
|
} |
||||
|
|
||||
|
.navBarText span { |
||||
|
color: #00b5ad; |
||||
|
height: 61px; |
||||
|
line-height: 61px; |
||||
|
vertical-align: middle; |
||||
|
font-size: 1.5em; |
||||
|
} |
||||
|
|
||||
|
.form-textarea-required { |
||||
|
color: rgb(159, 58, 56) !important; |
||||
|
outline-color: rgb(159, 58, 56) !important; |
||||
|
border-color: rgb(224, 180, 180) !important; |
||||
|
background-color: rgb(255, 246, 246) !important; |
||||
|
} |
||||
|
|
||||
|
.card { |
||||
|
width: 100% !important; |
||||
|
} |
||||
|
|
||||
|
.bottom-overlay-pad { |
||||
|
background: rgba(255, 255, 255, 0.85); |
||||
|
z-index: 10; |
||||
|
position: fixed; |
||||
|
bottom: 0px; |
||||
|
height: 62px; |
||||
|
width: 60%; |
||||
|
margin: 0px; |
||||
|
padding: 0px; |
||||
|
} |
||||
|
|
||||
|
.action-button { |
||||
|
z-index: 11; |
||||
|
position: fixed; |
||||
|
bottom: 10px; |
||||
|
left: calc(50% - 24px); |
||||
|
} |
||||
|
|
||||
|
.grey-text { |
||||
|
color: grey; |
||||
|
} |
||||
|
|
||||
|
.inline { |
||||
|
display: inline-block; |
||||
|
} |
||||
|
|
||||
|
.no-margin { |
||||
|
margin: 0px; |
||||
|
} |
||||
|
|
||||
|
hr { |
||||
|
color: #0c1a2b; |
||||
|
margin: 0px; |
||||
|
} |
||||
|
|
||||
|
*:focus { |
||||
|
outline: none !important |
||||
|
} |
||||
|
|
||||
|
a { |
||||
|
color: inherit; |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
|
||||
|
.center-in-parent { |
||||
|
width: 100%; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.vertical-center-in-parent { |
||||
|
vertical-align: middle; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.vertical-center-children { |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
#overlay { |
||||
|
position: fixed; |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background-color: rgba(0, 0, 0, 0.5); |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
#overlay-content { |
||||
|
position: absolute; |
||||
|
text-align: center; |
||||
|
top: 50%; |
||||
|
left: 50%; |
||||
|
color: white; |
||||
|
transform: translate(-50%, -50%); |
||||
|
-ms-transform: translate(-50%, -50%); |
||||
|
} |
||||
|
|
||||
|
.fill { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
@ -0,0 +1,30 @@ |
|||||
|
/* TOPICS LIST SCREEN */ |
||||
|
|
||||
|
.topics-list { |
||||
|
padding: 0px 2px; |
||||
|
margin-bottom: 75px; |
||||
|
} |
||||
|
|
||||
|
.topics-list a { |
||||
|
color: black !important; |
||||
|
text-decoration: none !important; |
||||
|
} |
||||
|
|
||||
|
.topics-list a:hover { |
||||
|
color: black !important; |
||||
|
text-decoration: none !important; |
||||
|
} |
||||
|
|
||||
|
.topic-subject { |
||||
|
margin: 0px 0px 5px; |
||||
|
} |
||||
|
|
||||
|
.topic-meta { |
||||
|
margin: 5px 0px 0px; |
||||
|
} |
||||
|
|
||||
|
.topic-date { |
||||
|
margin-bottom: 0px; |
||||
|
font-size: 0.77vw !important; |
||||
|
text-align: right; |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
body { |
||||
|
margin: 10em; |
||||
|
padding: 0; |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
/* SIGN UP SCREEN */ |
||||
|
|
||||
|
.sign-up-container { |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.sign-up-container > div { |
||||
|
margin: auto; |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
/* START TOPIC SCREEN */ |
||||
|
|
||||
|
.topic-form { |
||||
|
width: 100%; |
||||
|
margin: 20px 0px; |
||||
|
} |
@ -0,0 +1,51 @@ |
|||||
|
/* POSTS LIST SCREEN */ |
||||
|
|
||||
|
.posts-list-spacer { |
||||
|
margin-bottom: 85px; |
||||
|
height: 0px; |
||||
|
} |
||||
|
|
||||
|
.post { |
||||
|
width: 100%; |
||||
|
background-color: #FFFFFF; |
||||
|
margin: 20px 0px; |
||||
|
padding: 0px; |
||||
|
} |
||||
|
|
||||
|
.post-meta { |
||||
|
float: right; |
||||
|
margin-right: 11.25px; |
||||
|
} |
||||
|
|
||||
|
.user-avatar { |
||||
|
width: 52px; |
||||
|
height: 52px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.user-avatar a { |
||||
|
color: inherit !important; |
||||
|
text-decoration: none !important; |
||||
|
} |
||||
|
|
||||
|
.stretch-space-between { |
||||
|
display: flex; |
||||
|
flex-flow: row nowrap; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
|
||||
|
.user-info { |
||||
|
background-color: #FFFFFF; |
||||
|
margin: 12px auto; |
||||
|
padding: 7px; |
||||
|
} |
||||
|
|
||||
|
.post-content a { |
||||
|
margin-top: 10px; |
||||
|
color: #039be5; |
||||
|
} |
||||
|
|
||||
|
.post-form { |
||||
|
width: 100%; |
||||
|
margin: 20px 0px; |
||||
|
} |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
@ -0,0 +1,17 @@ |
|||||
|
import React from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { Button, Icon } from 'semantic-ui-react'; |
||||
|
|
||||
|
const FloatingButton = ({ onClick }) => ( |
||||
|
<div className="action-button" role="button" onClick={onClick} onKeyUp={onClick} tabIndex={0}> |
||||
|
<Button icon color="teal" size="large"> |
||||
|
<Icon name="add" /> |
||||
|
</Button> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
FloatingButton.propTypes = { |
||||
|
onClick: PropTypes.func.isRequired |
||||
|
}; |
||||
|
|
||||
|
export default FloatingButton; |
@ -0,0 +1,23 @@ |
|||||
|
import React from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
|
||||
|
const LoadingSpinner = ({ className, style }) => ( |
||||
|
<div className="vertical-center-children"> |
||||
|
<div |
||||
|
className={`center-in-parent ${ |
||||
|
className ? className : ''}`}
|
||||
|
style={style ? style : []} |
||||
|
> |
||||
|
<p> |
||||
|
<i className="fas fa-spinner fa-3x fa-spin" /> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
LoadingSpinner.propTypes = { |
||||
|
className: PropTypes.string, |
||||
|
style: PropTypes.string |
||||
|
}; |
||||
|
|
||||
|
export default LoadingSpinner; |
@ -0,0 +1,232 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
|
||||
|
import { Button, Divider, Form, Grid, Icon, TextArea } from 'semantic-ui-react'; |
||||
|
|
||||
|
import TimeAgo from 'react-timeago'; |
||||
|
import UserAvatar from 'react-user-avatar'; |
||||
|
import ReactMarkdown from 'react-markdown'; |
||||
|
|
||||
|
import { createPost } from '../redux/actions/transactionsActions'; |
||||
|
|
||||
|
class NewPost extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
const { subject } = props; |
||||
|
|
||||
|
this.handleInputChange = this.handleInputChange.bind(this); |
||||
|
this.handlePreviewToggle = this.handlePreviewToggle.bind(this); |
||||
|
this.validateAndPost = this.validateAndPost.bind(this); |
||||
|
|
||||
|
this.newPostOuterRef = React.createRef(); |
||||
|
|
||||
|
this.state = { |
||||
|
postSubjectInput: subject ? subject : '', |
||||
|
postContentInput: '', |
||||
|
postSubjectInputEmptySubmit: false, |
||||
|
postContentInputEmptySubmit: false, |
||||
|
previewEnabled: false, |
||||
|
previewDate: '' |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.newPostOuterRef.current.scrollIntoView(true); |
||||
|
} |
||||
|
|
||||
|
getDate() { |
||||
|
const currentdate = new Date(); |
||||
|
return (`${currentdate.getMonth() + 1} ${ |
||||
|
currentdate.getDate()}, ${ |
||||
|
currentdate.getFullYear()}, ${ |
||||
|
currentdate.getHours()}:${ |
||||
|
currentdate.getMinutes()}:${ |
||||
|
currentdate.getSeconds()}`);
|
||||
|
} |
||||
|
|
||||
|
async validateAndPost() { |
||||
|
const { postSubjectInput, postContentInput } = this.state; |
||||
|
const { topicID, onPostCreated, dispatch } = this.props; |
||||
|
|
||||
|
if (postSubjectInput === '' || postContentInput |
||||
|
=== '') { |
||||
|
this.setState({ |
||||
|
postSubjectInputEmptySubmit: postSubjectInput === '', |
||||
|
postContentInputEmptySubmit: postContentInput === '' |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
dispatch( |
||||
|
createPost(topicID, |
||||
|
{ |
||||
|
postSubject: postSubjectInput, |
||||
|
postMessage: postContentInput |
||||
|
}), |
||||
|
); |
||||
|
onPostCreated(); |
||||
|
} |
||||
|
|
||||
|
handleInputChange(event) { |
||||
|
this.setState({ |
||||
|
[event.target.name]: event.target.value |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
handlePreviewToggle() { |
||||
|
this.setState(prevState => ({ |
||||
|
previewEnabled: !prevState.previewEnabled, |
||||
|
previewDate: this.getDate() |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
previewDate, postSubjectInputEmptySubmit, postSubjectInput, postContentInputEmptySubmit, |
||||
|
postContentInput, previewEnabled |
||||
|
} = this.state; |
||||
|
const { postIndex, avatarUrl, user, onCancelClick } = this.props; |
||||
|
|
||||
|
return ( |
||||
|
<div className="post" ref={this.newPostOuterRef}> |
||||
|
<Divider horizontal> |
||||
|
<span className="grey-text"> |
||||
|
# |
||||
|
{postIndex} |
||||
|
</span> |
||||
|
</Divider> |
||||
|
<Grid> |
||||
|
<Grid.Row columns={16} stretched> |
||||
|
<Grid.Column width={1} className="user-avatar"> |
||||
|
<UserAvatar |
||||
|
size="52" |
||||
|
className="inline user-avatar" |
||||
|
src={avatarUrl} |
||||
|
name={user.username} |
||||
|
/> |
||||
|
</Grid.Column> |
||||
|
<Grid.Column width={15}> |
||||
|
<div className=""> |
||||
|
<div className="stretch-space-between"> |
||||
|
<span><strong>{user.username}</strong></span> |
||||
|
<span className="grey-text"> |
||||
|
{previewEnabled |
||||
|
&& <TimeAgo date={previewDate} /> |
||||
|
} |
||||
|
</span> |
||||
|
</div> |
||||
|
<div className="stretch-space-between"> |
||||
|
<span> |
||||
|
<strong> |
||||
|
{previewEnabled |
||||
|
&& (`Subject: ${ |
||||
|
postSubjectInput}`)
|
||||
|
} |
||||
|
</strong> |
||||
|
</span> |
||||
|
</div> |
||||
|
<div className="post-content"> |
||||
|
<div style={{ |
||||
|
display: previewEnabled |
||||
|
? 'block' |
||||
|
: 'none' |
||||
|
}} |
||||
|
> |
||||
|
<ReactMarkdown |
||||
|
source={postContentInput} |
||||
|
className="markdown-preview" |
||||
|
/> |
||||
|
</div> |
||||
|
<Form className="topic-form"> |
||||
|
<Form.Input |
||||
|
key="postSubjectInput" |
||||
|
style={{ |
||||
|
display: previewEnabled |
||||
|
? 'none' |
||||
|
: '' |
||||
|
}} |
||||
|
name="postSubjectInput" |
||||
|
error={postSubjectInputEmptySubmit} |
||||
|
type="text" |
||||
|
value={postSubjectInput} |
||||
|
placeholder="Subject" |
||||
|
id="postSubjectInput" |
||||
|
onChange={this.handleInputChange} |
||||
|
/> |
||||
|
<TextArea |
||||
|
key="postContentInput" |
||||
|
style={{ |
||||
|
display: previewEnabled |
||||
|
? 'none' |
||||
|
: '' |
||||
|
}} |
||||
|
name="postContentInput" |
||||
|
className={postContentInputEmptySubmit |
||||
|
? 'form-textarea-required' |
||||
|
: ''} |
||||
|
value={postContentInput} |
||||
|
placeholder="Post" |
||||
|
id="postContentInput" |
||||
|
onChange={this.handleInputChange} |
||||
|
rows={4} |
||||
|
autoHeight |
||||
|
/> |
||||
|
<br /> |
||||
|
<br /> |
||||
|
<Button.Group> |
||||
|
<Button |
||||
|
key="submit" |
||||
|
type="button" |
||||
|
onClick={this.validateAndPost} |
||||
|
color="teal" |
||||
|
animated |
||||
|
> |
||||
|
<Button.Content visible>Post</Button.Content> |
||||
|
<Button.Content hidden> |
||||
|
<Icon name="reply" /> |
||||
|
</Button.Content> |
||||
|
</Button> |
||||
|
<Button |
||||
|
type="button" |
||||
|
onClick={this.handlePreviewToggle} |
||||
|
color="yellow" |
||||
|
> |
||||
|
{previewEnabled ? 'Edit' : 'Preview'} |
||||
|
</Button> |
||||
|
<Button |
||||
|
type="button" |
||||
|
onClick={onCancelClick} |
||||
|
color="red" |
||||
|
> |
||||
|
Cancel |
||||
|
</Button> |
||||
|
</Button.Group> |
||||
|
</Form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Grid.Column> |
||||
|
</Grid.Row> |
||||
|
</Grid> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
NewPost.propTypes = { |
||||
|
subject: PropTypes.string, |
||||
|
topicID: PropTypes.number.isRequired, |
||||
|
postIndex: PropTypes.number.isRequired, |
||||
|
avatarUrl: PropTypes.string, |
||||
|
user: PropTypes.object.isRequired, |
||||
|
onCancelClick: PropTypes.func.isRequired, |
||||
|
dispatch: PropTypes.func.isRequired, |
||||
|
onPostCreated: PropTypes.func.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
orbitDB: state.orbitDB, |
||||
|
user: state.user |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(NewPost); |
@ -0,0 +1,68 @@ |
|||||
|
import React from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
|
||||
|
import { Divider, Grid } from 'semantic-ui-react'; |
||||
|
|
||||
|
import TimeAgo from 'react-timeago'; |
||||
|
import UserAvatar from 'react-user-avatar'; |
||||
|
import ReactMarkdown from 'react-markdown'; |
||||
|
|
||||
|
const Post = ({ user, date, subject, content }) => ( |
||||
|
<div className="post"> |
||||
|
<Divider horizontal> |
||||
|
<span className="grey-text">#0</span> |
||||
|
</Divider> |
||||
|
<Grid> |
||||
|
<Grid.Row columns={16} stretched> |
||||
|
<Grid.Column width={1} className="user-avatar"> |
||||
|
<UserAvatar |
||||
|
size="52" |
||||
|
className="inline" |
||||
|
src={user.avatarUrl} |
||||
|
name={user.username} |
||||
|
/> |
||||
|
</Grid.Column> |
||||
|
<Grid.Column width={15}> |
||||
|
<div className=""> |
||||
|
<div className="stretch-space-between"> |
||||
|
<span> |
||||
|
<strong> |
||||
|
{user.username} |
||||
|
</strong> |
||||
|
</span> |
||||
|
<span className="grey-text"> |
||||
|
<TimeAgo date={date} /> |
||||
|
</span> |
||||
|
</div> |
||||
|
<div className="stretch-space-between"> |
||||
|
<span> |
||||
|
<strong> |
||||
|
Subject: |
||||
|
{' '} |
||||
|
{subject} |
||||
|
</strong> |
||||
|
</span> |
||||
|
</div> |
||||
|
<div className="post-content"> |
||||
|
<ReactMarkdown source={content} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Grid.Column> |
||||
|
</Grid.Row> |
||||
|
</Grid> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
Post.propTypes = { |
||||
|
subject: PropTypes.string, |
||||
|
date: PropTypes.string, |
||||
|
content: PropTypes.string, |
||||
|
user: PropTypes.object.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
user: state.user |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(Post); |
@ -0,0 +1,13 @@ |
|||||
|
import React from 'react'; |
||||
|
import pageNotFound from '../assets/images/PageNotFound.jpg'; |
||||
|
|
||||
|
const NotFound = () => ( |
||||
|
<div style={{ |
||||
|
textAlign: 'center' |
||||
|
}} |
||||
|
> |
||||
|
<img src={pageNotFound} alt="Page not found!" /> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
export default NotFound; |
@ -0,0 +1,310 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { bindActionCreators } from 'redux'; |
||||
|
import { push } from 'connected-react-router'; |
||||
|
import { Link, withRouter } from 'react-router-dom'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
|
||||
|
import ContentLoader from 'react-content-loader'; |
||||
|
import { Button, Divider, Grid, Icon, Label, Transition } from 'semantic-ui-react'; |
||||
|
|
||||
|
import TimeAgo from 'react-timeago'; |
||||
|
import UserAvatar from 'react-user-avatar'; |
||||
|
import ReactMarkdown from 'react-markdown'; |
||||
|
import epochTimeConverter from '../helpers/EpochTimeConverter'; |
||||
|
|
||||
|
class Post extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
const { getFocus } = props; |
||||
|
|
||||
|
this.getBlockchainData = this.getBlockchainData.bind(this); |
||||
|
this.fetchPost = this.fetchPost.bind(this); |
||||
|
if (getFocus) { |
||||
|
this.postRef = React.createRef(); |
||||
|
} |
||||
|
|
||||
|
this.state = { |
||||
|
fetchPostDataStatus: 'pending', |
||||
|
postContent: '', |
||||
|
postSubject: '', |
||||
|
readyForAnimation: false, |
||||
|
animateOnToggle: true |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
this.getBlockchainData(); |
||||
|
const { readyForAnimation } = this.state; |
||||
|
if (readyForAnimation) { |
||||
|
if (this.postRef) { |
||||
|
setTimeout(() => { |
||||
|
this.postRef.current.scrollIntoView( |
||||
|
{ |
||||
|
block: 'start', behavior: 'smooth' |
||||
|
}, |
||||
|
); |
||||
|
setTimeout(() => { |
||||
|
this.setState({ |
||||
|
animateOnToggle: false |
||||
|
}); |
||||
|
}, 300); |
||||
|
}, 100); |
||||
|
this.setState({ |
||||
|
readyForAnimation: false |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getBlockchainData() { |
||||
|
const { fetchPostDataStatus } = this.state; |
||||
|
const { postData, orbitDB, postID } = this.props; |
||||
|
|
||||
|
if (postData && orbitDB.orbitdb && fetchPostDataStatus === 'pending') { |
||||
|
this.setState({ |
||||
|
fetchPostDataStatus: 'fetching' |
||||
|
}); |
||||
|
this.fetchPost(postID); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async fetchPost(postID) { |
||||
|
const { user, postData, orbitDB } = this.props; |
||||
|
let orbitPostData; |
||||
|
|
||||
|
if (postData.value[1] === user.address) { |
||||
|
orbitPostData = orbitDB.postsDB.get(postID); |
||||
|
} else { |
||||
|
const fullAddress = `/orbitdb/${postData.value[0]}/posts`; |
||||
|
const store = await orbitDB.orbitdb.keyvalue(fullAddress); |
||||
|
await store.load(); |
||||
|
|
||||
|
const localOrbitData = store.get(postID); |
||||
|
if (localOrbitData) { |
||||
|
orbitPostData = localOrbitData; |
||||
|
} else { |
||||
|
// Wait until we have received something from the network
|
||||
|
store.events.on('replicated', () => { |
||||
|
orbitPostData = store.get(postID); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.setState({ |
||||
|
postContent: orbitPostData.content, |
||||
|
postSubject: orbitPostData.subject, |
||||
|
fetchPostDataStatus: 'fetched', |
||||
|
readyForAnimation: true |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { animateOnToggle, postSubject, postContent } = this.state; |
||||
|
const { avatarUrl, postIndex, navigateTo, postData, postID } = this.props; |
||||
|
|
||||
|
const avatarView = (postData |
||||
|
? ( |
||||
|
<UserAvatar |
||||
|
size="52" |
||||
|
className="inline" |
||||
|
src={avatarUrl} |
||||
|
name={postData.value[2]} |
||||
|
/> |
||||
|
) |
||||
|
: ( |
||||
|
<div className="user-avatar"> |
||||
|
<ContentLoader |
||||
|
height={52} |
||||
|
width={52} |
||||
|
speed={2} |
||||
|
primaryColor="#b2e8e6" |
||||
|
secondaryColor="#00b5ad" |
||||
|
> |
||||
|
<circle cx="26" cy="26" r="26" /> |
||||
|
</ContentLoader> |
||||
|
</div> |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<Transition |
||||
|
animation="tada" |
||||
|
duration={500} |
||||
|
visible={animateOnToggle} |
||||
|
> |
||||
|
<div className="post" ref={this.postRef ? this.postRef : null}> |
||||
|
<Divider horizontal> |
||||
|
<span className="grey-text"> |
||||
|
# |
||||
|
{postIndex} |
||||
|
</span> |
||||
|
</Divider> |
||||
|
<Grid> |
||||
|
<Grid.Row columns={16} stretched> |
||||
|
<Grid.Column width={1} className="user-avatar"> |
||||
|
{postData !== null |
||||
|
? ( |
||||
|
<Link |
||||
|
to={`/profile/${postData.value[1] |
||||
|
}/${postData.value[2]}`}
|
||||
|
onClick={(event) => { event.stopPropagation(); }} |
||||
|
> |
||||
|
{avatarView} |
||||
|
</Link> |
||||
|
) |
||||
|
: avatarView |
||||
|
} |
||||
|
</Grid.Column> |
||||
|
<Grid.Column width={15}> |
||||
|
<div className=""> |
||||
|
<div className="stretch-space-between"> |
||||
|
<span className={postData |
||||
|
!== null ? '' : 'grey-text'} |
||||
|
> |
||||
|
<strong> |
||||
|
{postData !== null |
||||
|
? postData.value[2] |
||||
|
: 'Username' |
||||
|
} |
||||
|
</strong> |
||||
|
</span> |
||||
|
<span className="grey-text"> |
||||
|
{postData !== null |
||||
|
&& ( |
||||
|
<TimeAgo date={epochTimeConverter( |
||||
|
postData.value[3], |
||||
|
)} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
</span> |
||||
|
</div> |
||||
|
<div className="stretch-space-between"> |
||||
|
<span |
||||
|
className={postSubject |
||||
|
=== '' ? '' : 'grey-text'} |
||||
|
> |
||||
|
<strong> |
||||
|
{postSubject === '' |
||||
|
? ( |
||||
|
<ContentLoader |
||||
|
height={5.8} |
||||
|
width={300} |
||||
|
speed={2} |
||||
|
primaryColor="#b2e8e6" |
||||
|
secondaryColor="#00b5ad" |
||||
|
> |
||||
|
<rect |
||||
|
x="0" |
||||
|
y="0" |
||||
|
rx="3" |
||||
|
ry="3" |
||||
|
width="75" |
||||
|
height="5.5" |
||||
|
/> |
||||
|
</ContentLoader> |
||||
|
) |
||||
|
: `Subject: ${ |
||||
|
postSubject}` |
||||
|
} |
||||
|
</strong> |
||||
|
</span> |
||||
|
</div> |
||||
|
<div className="post-content"> |
||||
|
{postContent !== '' |
||||
|
? <ReactMarkdown source={postContent} /> |
||||
|
: ( |
||||
|
<ContentLoader |
||||
|
height={11.2} |
||||
|
width={300} |
||||
|
speed={2} |
||||
|
primaryColor="#b2e8e6" |
||||
|
secondaryColor="#00b5ad" |
||||
|
> |
||||
|
<rect |
||||
|
x="0" |
||||
|
y="0" |
||||
|
rx="3" |
||||
|
ry="3" |
||||
|
width="180" |
||||
|
height="4.0" |
||||
|
/> |
||||
|
<rect |
||||
|
x="0" |
||||
|
y="6.5" |
||||
|
rx="3" |
||||
|
ry="3" |
||||
|
width="140" |
||||
|
height="4.0" |
||||
|
/> |
||||
|
</ContentLoader> |
||||
|
) |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
</Grid.Column> |
||||
|
</Grid.Row> |
||||
|
<Grid.Row> |
||||
|
<Grid.Column floated="right" textAlign="right"> |
||||
|
<Button |
||||
|
icon |
||||
|
size="mini" |
||||
|
style={{ |
||||
|
marginRight: '0px' |
||||
|
}} |
||||
|
> |
||||
|
<Icon name="chevron up" /> |
||||
|
</Button> |
||||
|
<Label color="teal">8000</Label> |
||||
|
<Button icon size="mini"> |
||||
|
<Icon name="chevron down" /> |
||||
|
</Button> |
||||
|
<Button |
||||
|
icon |
||||
|
size="mini" |
||||
|
onClick={postData |
||||
|
? () => { |
||||
|
navigateTo(`/topic/${ |
||||
|
postData.value[4]}/${ |
||||
|
postID}`);
|
||||
|
} |
||||
|
: () => {}} |
||||
|
> |
||||
|
<Icon name="linkify" /> |
||||
|
</Button> |
||||
|
</Grid.Column> |
||||
|
</Grid.Row> |
||||
|
</Grid> |
||||
|
</div> |
||||
|
</Transition> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Post.propTypes = { |
||||
|
getFocus: PropTypes.bool.isRequired, |
||||
|
user: PropTypes.object.isRequired, |
||||
|
orbitDB: PropTypes.object.isRequired, |
||||
|
avatarUrl: PropTypes.string, |
||||
|
postIndex: PropTypes.number.isRequired, |
||||
|
navigateTo: PropTypes.func.isRequired, |
||||
|
postData: PropTypes.object, |
||||
|
postID: PropTypes.string.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapDispatchToProps = dispatch => bindActionCreators({ |
||||
|
navigateTo: location => push(location) |
||||
|
}, dispatch); |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
user: state.user, |
||||
|
orbitDB: state.orbit |
||||
|
}); |
||||
|
|
||||
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Post)); |
@ -0,0 +1,97 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { drizzle } from '../index'; |
||||
|
|
||||
|
import Post from './Post'; |
||||
|
|
||||
|
const contract = 'Forum'; |
||||
|
const getPostMethod = 'getPost'; |
||||
|
|
||||
|
class PostList extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.getBlockchainData = this.getBlockchainData.bind(this); |
||||
|
|
||||
|
this.state = { |
||||
|
dataKeys: [] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
getBlockchainData() { |
||||
|
const { dataKeys } = this.state; |
||||
|
const { drizzleStatus, postIDs } = this.props; |
||||
|
|
||||
|
if (drizzleStatus.initialized) { |
||||
|
const dataKeysShallowCopy = dataKeys.slice(); |
||||
|
let fetchingNewData = false; |
||||
|
|
||||
|
postIDs.forEach((postID) => { |
||||
|
if (!dataKeys[postID]) { |
||||
|
dataKeysShallowCopy[postID] = drizzle.contracts[contract].methods[getPostMethod].cacheCall( |
||||
|
postID, |
||||
|
); |
||||
|
fetchingNewData = true; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (fetchingNewData) { |
||||
|
this.setState({ |
||||
|
dataKeys: dataKeysShallowCopy |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { dataKeys } = this.state; |
||||
|
const { postIDs, contracts, focusOnPost, recentToTheTop } = this.props; |
||||
|
|
||||
|
const posts = postIDs.map((postID, index) => ( |
||||
|
<Post |
||||
|
postData={(dataKeys[postID] |
||||
|
&& contracts[contract][getPostMethod][dataKeys[postID]]) |
||||
|
? contracts[contract][getPostMethod][dataKeys[postID]] |
||||
|
: null} |
||||
|
avatarUrl="" |
||||
|
postIndex={index} |
||||
|
postID={postID} |
||||
|
getFocus={focusOnPost === postID} |
||||
|
key={postID} |
||||
|
/> |
||||
|
)); |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
{recentToTheTop |
||||
|
? posts.slice(0).reverse() |
||||
|
: posts |
||||
|
} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
PostList.propTypes = { |
||||
|
drizzleStatus: PropTypes.object.isRequired, |
||||
|
postIDs: PropTypes.array.isRequired, |
||||
|
contracts: PropTypes.array.isRequired, |
||||
|
focusOnPost: PropTypes.number, |
||||
|
recentToTheTop: PropTypes.bool |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
contracts: state.contracts, |
||||
|
drizzleStatus: state.drizzleStatus |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(PostList); |
@ -0,0 +1,162 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import UserAvatar from 'react-user-avatar'; |
||||
|
import { drizzle } from '../index'; |
||||
|
|
||||
|
import epochTimeConverter from '../helpers/EpochTimeConverter'; |
||||
|
|
||||
|
import UsernameFormContainer from '../containers/UsernameFormContainer'; |
||||
|
|
||||
|
const callsInfo = [ |
||||
|
{ |
||||
|
contract: 'Forum', |
||||
|
method: 'getUserDateOfRegister' |
||||
|
}, { |
||||
|
contract: 'Forum', |
||||
|
method: 'getOrbitDBId' |
||||
|
}]; |
||||
|
|
||||
|
class ProfileInformation extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.getBlockchainData = this.getBlockchainData.bind(this); |
||||
|
this.dataKey = []; |
||||
|
|
||||
|
this.state = { |
||||
|
pageStatus: 'initialized', |
||||
|
dateOfRegister: '', |
||||
|
orbitDBId: '' |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
getBlockchainData() { |
||||
|
const { pageStatus, dateOfRegister, orbitDBId } = this.state; |
||||
|
const { drizzleStatus, address, contracts } = this.props; |
||||
|
|
||||
|
if (pageStatus === 'initialized' |
||||
|
&& drizzleStatus.initialized) { |
||||
|
callsInfo.forEach((call, index) => { |
||||
|
this.dataKey[index] = drizzle.contracts[call.contract].methods[call.method].cacheCall( |
||||
|
address, |
||||
|
); |
||||
|
}); |
||||
|
this.setState({ |
||||
|
pageStatus: 'loading' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (pageStatus === 'loading') { |
||||
|
let pageStatusUpdate = 'loaded'; |
||||
|
callsInfo.forEach((call, index) => { |
||||
|
if (!contracts[call.contract][call.method][this.dataKey[index]]) { |
||||
|
pageStatusUpdate = 'loading'; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (pageStatusUpdate === 'loaded') { |
||||
|
this.setState({ |
||||
|
pageStatus: pageStatusUpdate |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (pageStatus === 'loaded') { |
||||
|
if (dateOfRegister === '') { |
||||
|
const transaction = contracts[callsInfo[0].contract][callsInfo[0].method][this.dataKey[0]]; |
||||
|
if (transaction) { |
||||
|
this.setState({ |
||||
|
dateOfRegister: transaction.value |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
if (orbitDBId === '') { |
||||
|
const transaction = contracts[callsInfo[1].contract][callsInfo[1].method][this.dataKey[1]]; |
||||
|
if (transaction) { |
||||
|
this.setState({ |
||||
|
orbitDBId: transaction.value |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { orbitDBId, dateOfRegister } = this.state; |
||||
|
const { avatarUrl, username, address, numberOfTopics, numberOfPosts, self } = this.props; |
||||
|
|
||||
|
return ( |
||||
|
<div className="user-info"> |
||||
|
{avatarUrl && ( |
||||
|
<UserAvatar |
||||
|
size="40" |
||||
|
className="inline user-avatar" |
||||
|
src={avatarUrl} |
||||
|
name={username} |
||||
|
/> |
||||
|
)} |
||||
|
<table className="highlight centered responsive-table"> |
||||
|
<tbody> |
||||
|
<tr> |
||||
|
<td><strong>Username:</strong></td> |
||||
|
<td>{username}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><strong>Account address:</strong></td> |
||||
|
<td>{address}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><strong>OrbitDB:</strong></td> |
||||
|
<td>{orbitDBId}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><strong>Number of topics created:</strong></td> |
||||
|
<td>{numberOfTopics}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td><strong>Number of posts:</strong></td> |
||||
|
<td>{numberOfPosts}</td> |
||||
|
</tr> |
||||
|
{dateOfRegister |
||||
|
&& ( |
||||
|
<tr> |
||||
|
<td><strong>Member since:</strong></td> |
||||
|
<td>{epochTimeConverter(dateOfRegister)}</td> |
||||
|
</tr> |
||||
|
) |
||||
|
} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
{self && <UsernameFormContainer />} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ProfileInformation.propTypes = { |
||||
|
drizzleStatus: PropTypes.object.isRequired, |
||||
|
contracts: PropTypes.array.isRequired, |
||||
|
avatarUrl: PropTypes.string, |
||||
|
username: PropTypes.string.isRequired, |
||||
|
address: PropTypes.string.isRequired, |
||||
|
numberOfTopics: PropTypes.number.isRequired, |
||||
|
numberOfPosts: PropTypes.number.isRequired, |
||||
|
self: PropTypes.bool |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
drizzleStatus: state.drizzleStatus, |
||||
|
contracts: state.contracts, |
||||
|
user: state.user |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(ProfileInformation); |
@ -0,0 +1,139 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { GetTopicResult } from '../CustomPropTypes' |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { withRouter } from 'react-router-dom'; |
||||
|
|
||||
|
import ContentLoader from 'react-content-loader'; |
||||
|
import { Card } from 'semantic-ui-react'; |
||||
|
|
||||
|
import TimeAgo from 'react-timeago'; |
||||
|
import epochTimeConverter from '../helpers/EpochTimeConverter'; |
||||
|
import { addPeerDatabase } from '../redux/actions/orbitActions'; |
||||
|
|
||||
|
class Topic extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { |
||||
|
askedForReplication: false, |
||||
|
fetchedSubject: false |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
const { dispatch, topicData, topicSubject, orbitDB } = this.props; |
||||
|
const { askedForReplication } = this.state; |
||||
|
if(!askedForReplication && orbitDB.ipfsInitialized && orbitDB.orbitdb && dispatch && !topicSubject && topicData) { |
||||
|
this.setState({ askedForReplication: true }); |
||||
|
dispatch(addPeerDatabase(`/orbitdb/${topicData.value[0]}/topics`)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { history, topicID, topicData, topicSubject } = this.props; |
||||
|
|
||||
|
return ( |
||||
|
<Card |
||||
|
link |
||||
|
className="card" |
||||
|
onClick={() => { |
||||
|
history.push(`/topic/${topicID}`); |
||||
|
}} |
||||
|
> |
||||
|
<Card.Content> |
||||
|
<div className={`topic-subject${ |
||||
|
topicSubject ? '' : ' grey-text'}`}
|
||||
|
> |
||||
|
<p> |
||||
|
<strong> |
||||
|
{(topicSubject) ? topicSubject |
||||
|
: ( |
||||
|
<ContentLoader |
||||
|
height={5.8} |
||||
|
width={300} |
||||
|
speed={2} |
||||
|
primaryColor="#b2e8e6" |
||||
|
secondaryColor="#00b5ad" |
||||
|
> |
||||
|
<rect x="0" y="0" rx="3" ry="3" width="150" height="5.5" /> |
||||
|
</ContentLoader> |
||||
|
)} |
||||
|
</strong> |
||||
|
</p> |
||||
|
</div> |
||||
|
<hr /> |
||||
|
<div className="topic-meta"> |
||||
|
<p className={`no-margin${ |
||||
|
topicData !== null ? '' : ' grey-text'}`}
|
||||
|
> |
||||
|
{topicData !== null |
||||
|
? topicData.value[2] |
||||
|
: 'Username' |
||||
|
} |
||||
|
</p> |
||||
|
<p className={`no-margin${ |
||||
|
topicData !== null ? '' : ' grey-text'}`}
|
||||
|
> |
||||
|
{`Number of replies: ${topicData !== null |
||||
|
? topicData.value[4].length |
||||
|
: ''}` |
||||
|
} |
||||
|
</p> |
||||
|
<p className="topic-date grey-text"> |
||||
|
{topicData !== null |
||||
|
&& ( |
||||
|
<TimeAgo |
||||
|
date={epochTimeConverter(topicData.value[3])} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
</p> |
||||
|
</div> |
||||
|
</Card.Content> |
||||
|
</Card> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Topic.propTypes = { |
||||
|
user: PropTypes.object.isRequired, |
||||
|
history: PropTypes.object.isRequired, |
||||
|
topicData: GetTopicResult.isRequired, |
||||
|
orbitDB: PropTypes.object.isRequired, |
||||
|
topicID: PropTypes.number.isRequired |
||||
|
}; |
||||
|
|
||||
|
function getTopicSubject(state, props){ |
||||
|
const { user, orbit } = state; |
||||
|
if (orbit.ipfsInitialized && orbit.orbitdb) { |
||||
|
const { topicData, topicID } = props; |
||||
|
if (topicData){ |
||||
|
if(user && topicData.value[1] === user.address) { |
||||
|
const orbitData = orbit.topicsDB.get(topicID); |
||||
|
if(orbitData && orbitData.subject) |
||||
|
return orbitData.subject; |
||||
|
} |
||||
|
const db = orbit.replicatedDatabases.find(db => db.fullAddress === `/orbitdb/${topicData.value[0]}/topics`); |
||||
|
if(db && db.ready && db.store){ |
||||
|
const localOrbitData = db.store.get(topicID); |
||||
|
if (localOrbitData){ |
||||
|
return localOrbitData.subject; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
function mapStateToProps(state, ownProps) { |
||||
|
return { |
||||
|
user: state.user, |
||||
|
orbitDB: state.orbit, |
||||
|
topicSubject: getTopicSubject(state, ownProps) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export default withRouter(connect(mapStateToProps)(Topic)); |
@ -0,0 +1,88 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { drizzle } from '../index'; |
||||
|
|
||||
|
import Topic from './Topic'; |
||||
|
|
||||
|
const contract = 'Forum'; |
||||
|
const getTopicMethod = 'getTopic'; |
||||
|
|
||||
|
class TopicList extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.getBlockchainData = this.getBlockchainData.bind(this); |
||||
|
|
||||
|
this.state = { |
||||
|
dataKeys: [] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
getBlockchainData() { |
||||
|
const { dataKeys } = this.state; |
||||
|
const { drizzleStatus, topicIDs } = this.props; |
||||
|
|
||||
|
if (drizzleStatus.initialized) { |
||||
|
const dataKeysShallowCopy = dataKeys.slice(); |
||||
|
let fetchingNewData = false; |
||||
|
|
||||
|
topicIDs.forEach((topicID) => { |
||||
|
if (!dataKeys[topicID]) { |
||||
|
dataKeysShallowCopy[topicID] = drizzle.contracts[contract].methods[getTopicMethod] |
||||
|
.cacheCall(topicID); |
||||
|
fetchingNewData = true; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (fetchingNewData) { |
||||
|
this.setState({ |
||||
|
dataKeys: dataKeysShallowCopy |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { dataKeys } = this.state; |
||||
|
const { topicIDs, contracts } = this.props; |
||||
|
|
||||
|
const topics = topicIDs.map(topicID => ( |
||||
|
<Topic |
||||
|
topicData={(dataKeys[topicID] |
||||
|
&& contracts[contract][getTopicMethod][dataKeys[topicID]]) |
||||
|
? contracts[contract][getTopicMethod][dataKeys[topicID]] |
||||
|
: null} |
||||
|
topicID={topicID} |
||||
|
key={topicID} |
||||
|
/> |
||||
|
)); |
||||
|
|
||||
|
return ( |
||||
|
<div className="topics-list"> |
||||
|
{topics.slice(0).reverse()} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
TopicList.propTypes = { |
||||
|
topicIDs: PropTypes.arrayOf(PropTypes.string).isRequired, |
||||
|
contracts: PropTypes.PropTypes.objectOf(PropTypes.object).isRequired, |
||||
|
drizzleStatus: PropTypes.object.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
contracts: state.contracts, |
||||
|
drizzleStatus: state.drizzleStatus |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(TopicList); |
@ -0,0 +1,20 @@ |
|||||
|
import Forum from '../contracts/Forum.json'; |
||||
|
|
||||
|
const drizzleOptions = { |
||||
|
web3: { |
||||
|
fallback: { |
||||
|
type: 'ws', |
||||
|
url: 'ws://127.0.0.1:9545' |
||||
|
} |
||||
|
}, |
||||
|
contracts: [Forum], |
||||
|
events: { |
||||
|
Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated'] |
||||
|
}, |
||||
|
polls: { |
||||
|
accounts: 2000, |
||||
|
blocks: 2000 |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default drizzleOptions; |
@ -0,0 +1,22 @@ |
|||||
|
// OrbitDB uses Pubsub which is an experimental feature
|
||||
|
// and need to be turned on manually.
|
||||
|
const ipfsOptions = { |
||||
|
EXPERIMENTAL: { |
||||
|
pubsub: true |
||||
|
}, |
||||
|
config: { |
||||
|
Addresses: { |
||||
|
Swarm: [ |
||||
|
'/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star', |
||||
|
// Use local signal server (https://github.com/libp2p/js-libp2p-websocket-star-rendezvous)
|
||||
|
// (e.g. rendezvous --port=9090 --host=127.0.0.1)
|
||||
|
'/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star' |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
preload: { |
||||
|
enabled: false |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default ipfsOptions; |
@ -0,0 +1,139 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { withRouter } from 'react-router-dom'; |
||||
|
|
||||
|
import { Header } from 'semantic-ui-react'; |
||||
|
import { drizzle } from '../index'; |
||||
|
|
||||
|
import TopicList from '../components/TopicList'; |
||||
|
import FloatingButton from '../components/FloatingButton'; |
||||
|
|
||||
|
/* import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions'; */ |
||||
|
|
||||
|
const contract = 'Forum'; |
||||
|
const getNumberOfTopicsMethod = 'getNumberOfTopics'; |
||||
|
|
||||
|
class BoardContainer extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
/* this.props.store.dispatch(showProgressBar()); */ |
||||
|
|
||||
|
this.getBlockchainData = this.getBlockchainData.bind(this); |
||||
|
this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this); |
||||
|
|
||||
|
this.state = { |
||||
|
pageStatus: 'initialized' |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
getBlockchainData() { |
||||
|
const { pageStatus } = this.state; |
||||
|
const { drizzleStatus, contracts } = this.props; |
||||
|
|
||||
|
if (pageStatus === 'initialized' |
||||
|
&& drizzleStatus.initialized) { |
||||
|
this.dataKey = drizzle.contracts[contract].methods[getNumberOfTopicsMethod].cacheCall(); |
||||
|
this.setState({ |
||||
|
pageStatus: 'loading' |
||||
|
}); |
||||
|
} |
||||
|
if (pageStatus === 'loading' |
||||
|
&& contracts[contract][getNumberOfTopicsMethod][this.dataKey]) { |
||||
|
this.setState({ |
||||
|
pageStatus: 'loaded' |
||||
|
}); |
||||
|
/* this.props.store.dispatch(hideProgressBar()); */ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
handleCreateTopicClick() { |
||||
|
const { history } = this.props; |
||||
|
history.push('/startTopic'); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { pageStatus } = this.state; |
||||
|
const { contracts, hasSignedUp } = this.props; |
||||
|
|
||||
|
let boardContents; |
||||
|
if (pageStatus === 'loaded') { |
||||
|
const numberOfTopics = contracts[contract][getNumberOfTopicsMethod][this.dataKey].value; |
||||
|
|
||||
|
if (numberOfTopics !== '0') { |
||||
|
this.topicIDs = []; |
||||
|
for (let i = 0; i < numberOfTopics; i++) { |
||||
|
this.topicIDs.push(i); |
||||
|
} |
||||
|
boardContents = ([ |
||||
|
<TopicList topicIDs={this.topicIDs} key="topicList" />, |
||||
|
<div className="bottom-overlay-pad" key="pad" />, |
||||
|
hasSignedUp |
||||
|
&& ( |
||||
|
<FloatingButton |
||||
|
onClick={this.handleCreateTopicClick} |
||||
|
key="createTopicButton" |
||||
|
/> |
||||
|
) |
||||
|
]); |
||||
|
} else if (!hasSignedUp) { |
||||
|
boardContents = ( |
||||
|
<div className="vertical-center-in-parent"> |
||||
|
<Header color="teal" textAlign="center" as="h2"> |
||||
|
There are no topics yet! |
||||
|
</Header> |
||||
|
<Header color="teal" textAlign="center" as="h4"> |
||||
|
Sign up to be the first to post. |
||||
|
</Header> |
||||
|
</div> |
||||
|
); |
||||
|
} else { |
||||
|
boardContents = ( |
||||
|
<div className="vertical-center-in-parent"> |
||||
|
<Header color="teal" textAlign="center" as="h2"> |
||||
|
There are no topics yet! |
||||
|
</Header> |
||||
|
<Header color="teal" textAlign="center" as="h4"> |
||||
|
Click the add button at the bottom of the page to be the first |
||||
|
to post. |
||||
|
</Header> |
||||
|
<FloatingButton |
||||
|
onClick={this.handleCreateTopicClick} |
||||
|
key="createTopicButton" |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className="fill"> |
||||
|
{boardContents} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
BoardContainer.propTypes = { |
||||
|
drizzleStatus: PropTypes.object.isRequired, |
||||
|
history: PropTypes.object.isRequired, |
||||
|
contracts: PropTypes.objectOf(PropTypes.object).isRequired, |
||||
|
hasSignedUp: PropTypes.bool.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
contracts: state.contracts, |
||||
|
drizzleStatus: state.drizzleStatus, |
||||
|
hasSignedUp: state.user.hasSignedUp |
||||
|
}); |
||||
|
|
||||
|
export default withRouter(connect(mapStateToProps)(BoardContainer)); |
@ -0,0 +1,45 @@ |
|||||
|
import React from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
|
||||
|
import NavBarContainer from './NavBarContainer'; |
||||
|
import RightSideBarContainer from './TransactionsMonitorContainer'; |
||||
|
// Styles
|
||||
|
import '../assets/fonts/fontawesome-free-5.7.2/all.js'; // TODO: check https://fontawesome.com/how-to-use/on-the-web/setup/using-package-managers
|
||||
|
import '../assets/css/App.css'; |
||||
|
import '../assets/css/sign-up-container.css'; |
||||
|
|
||||
|
import '../assets/css/board-container.css'; |
||||
|
import '../assets/css/start-topic-container.css'; |
||||
|
import '../assets/css/topic-container.css'; |
||||
|
import '../assets/css/profile-container.css'; |
||||
|
|
||||
|
/* import TransactionsMonitorContainer from '../../containers/TransactionsMonitorContainer'; */ |
||||
|
|
||||
|
const CoreLayout = ({ children }) => ( |
||||
|
<div className="App"> |
||||
|
<NavBarContainer /> |
||||
|
{/* <div className="progress-bar-container" |
||||
|
style={{display: this.props.isProgressBarVisible ? "block" : "none"}}> |
||||
|
<div className="progress"> |
||||
|
<div className="indeterminate"></div> |
||||
|
</div> |
||||
|
</div> */} |
||||
|
<div className="page-container"> |
||||
|
<aside className="left-side-panel" /> |
||||
|
<div className="main-panel"> |
||||
|
<div className="view-container"> |
||||
|
{children} |
||||
|
</div> |
||||
|
</div> |
||||
|
<aside className="right-side-panel"> |
||||
|
<RightSideBarContainer /> |
||||
|
</aside> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
CoreLayout.propTypes = { |
||||
|
children: PropTypes.objectOf(PropTypes.object) |
||||
|
}; |
||||
|
|
||||
|
export default CoreLayout; |
@ -0,0 +1,26 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
|
||||
|
import BoardContainer from './BoardContainer'; |
||||
|
|
||||
|
class HomeContainer extends Component { |
||||
|
render() { |
||||
|
// We can add a modal to tell the user to sign up
|
||||
|
|
||||
|
/* var modal = this.props.user.hasSignedUp && ( |
||||
|
<Modal dimmer='blurring' open={this.state.open}> |
||||
|
<Modal.Header>Select a Photo</Modal.Header> |
||||
|
<Modal.Content image> |
||||
|
<Image wrapped size='medium' src='/assets/images/avatar/large/rachel.png' /> |
||||
|
<Modal.Description> |
||||
|
<Header>Default Profile Image</Header> |
||||
|
<p>We've found the following gravatar image associated with your e-mail address.</p> |
||||
|
<p>Is it okay to use this photo?</p> |
||||
|
</Modal.Description> |
||||
|
</Modal.Content> |
||||
|
</Modal>); */ |
||||
|
|
||||
|
return (<BoardContainer />); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default HomeContainer; |
@ -0,0 +1,107 @@ |
|||||
|
import React, { Children, Component } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import ipfs_logo from '../assets/images/ipfs_logo.png'; |
||||
|
|
||||
|
|
||||
|
//TODO: Add OrbitDB Loading thingy
|
||||
|
class LoadingContainer extends Component { |
||||
|
render() { |
||||
|
if (this.props.web3.status === 'failed' || !this.props.web3.networkId) { |
||||
|
|
||||
|
//TODO: wtf is this
|
||||
|
if (this.props.errorComp) |
||||
|
return this.props.errorComp; |
||||
|
|
||||
|
return ( |
||||
|
<main className="container loading-screen"> |
||||
|
<div className="pure-g"> |
||||
|
<div className="pure-u-1-1"> |
||||
|
<h1><span role="img" aria-label="Fox Face">🦊</span></h1> |
||||
|
<p><strong>This browser has no connection to the Ethereum network.</strong></p> |
||||
|
<p> |
||||
|
Please make sure that: |
||||
|
<ul> |
||||
|
<li>You use MetaMask or a dedicated Ethereum browser (e.g. Mist or Parity)</li> |
||||
|
<li>They are pointed to the correct network</li> |
||||
|
<li>Your account is unlocked and the app has the rights to access it</li> |
||||
|
</ul> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (this.props.web3.status === 'initialized' && Object.keys(this.props.accounts).length === 0) { |
||||
|
return( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<div> |
||||
|
<h1><span role="img" aria-label="Fox Face">🦊</span></h1> |
||||
|
<p><strong>We can't find any Ethereum accounts!</strong>.</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (!this.props.contractInitialized) { |
||||
|
return( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<div> |
||||
|
<h1><span role="img" aria-label="Gear">⚙</span></h1> |
||||
|
<p><strong>Initializing contracts...</strong></p> |
||||
|
<p>If this takes too long please make sure they are deployed to the network.</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (!this.props.ipfsInitialized) { |
||||
|
return( |
||||
|
<main className="loading-screen"> |
||||
|
<div> |
||||
|
<div> |
||||
|
<img src={ipfs_logo} alt="ipfs_logo" height="50"/> |
||||
|
<p><strong>Initializing IPFS...</strong></p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
//TODO: wtf is this
|
||||
|
if (this.props.drizzleStatus.initialized) |
||||
|
return Children.only(this.props.children); |
||||
|
|
||||
|
//TODO: wtf is this
|
||||
|
if (this.props.loadingComp) |
||||
|
return this.props.loadingComp; |
||||
|
|
||||
|
return( |
||||
|
<main className="container loading-screen"> |
||||
|
<div> |
||||
|
<div> |
||||
|
<h1><span role="img" aria-label="Gear">⚙</span></h1> |
||||
|
<p>Loading dapp...</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</main> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const mapStateToProps = state => { |
||||
|
return { |
||||
|
accounts: state.accounts, |
||||
|
drizzleStatus: state.drizzleStatus, |
||||
|
web3: state.web3, |
||||
|
ipfsInitialized: state.orbit.ipfsInitialized, |
||||
|
contractInitialized: state.contracts.Forum.initialized |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default connect(mapStateToProps)(LoadingContainer); |
||||
|
|
@ -0,0 +1,66 @@ |
|||||
|
import React from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { bindActionCreators } from 'redux'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { push } from 'connected-react-router'; |
||||
|
import { Image, Menu } from 'semantic-ui-react'; |
||||
|
|
||||
|
import logo from '../assets/images/logo.png'; |
||||
|
|
||||
|
const NavBarContainer = ({ hasSignedUp, navigateTo, navBarTitle }) => ( |
||||
|
<Menu fixed="top" inverted> |
||||
|
<Menu.Item header onClick={() => { navigateTo('/'); }}> |
||||
|
<Image |
||||
|
size="mini" |
||||
|
src={logo} |
||||
|
style={{ |
||||
|
marginRight: '1.5em' |
||||
|
}} |
||||
|
/> |
||||
|
Apella |
||||
|
</Menu.Item> |
||||
|
<Menu.Item onClick={() => { navigateTo('/home'); }}> |
||||
|
Home |
||||
|
</Menu.Item> |
||||
|
{hasSignedUp |
||||
|
? ( |
||||
|
<Menu.Item onClick={() => { navigateTo('/profile'); }}> |
||||
|
Profile |
||||
|
</Menu.Item> |
||||
|
) |
||||
|
: ( |
||||
|
<Menu.Menu |
||||
|
position="right" |
||||
|
style={{ |
||||
|
backgroundColor: '#00b5ad' |
||||
|
}} |
||||
|
> |
||||
|
<Menu.Item onClick={() => { navigateTo('/signup'); }}> |
||||
|
SignUp |
||||
|
</Menu.Item> |
||||
|
</Menu.Menu> |
||||
|
) |
||||
|
} |
||||
|
<div className="navBarText"> |
||||
|
{navBarTitle !== '' |
||||
|
&& <span>{navBarTitle}</span>} |
||||
|
</div> |
||||
|
</Menu> |
||||
|
); |
||||
|
|
||||
|
NavBarContainer.propTypes = { |
||||
|
hasSignedUp: PropTypes.bool.isRequired, |
||||
|
navigateTo: PropTypes.func.isRequired, |
||||
|
navBarTitle: PropTypes.string.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapDispatchToProps = dispatch => bindActionCreators({ |
||||
|
navigateTo: location => push(location) |
||||
|
}, dispatch); |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
hasSignedUp: state.user.hasSignedUp, |
||||
|
navBarTitle: state.interface.navBarTitle |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps, mapDispatchToProps)(NavBarContainer); |
@ -0,0 +1,221 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { bindActionCreators } from 'redux'; |
||||
|
import { push } from 'connected-react-router'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Tab } from 'semantic-ui-react'; |
||||
|
import { drizzle } from '../index'; |
||||
|
|
||||
|
import ProfileInformation from '../components/ProfileInformation'; |
||||
|
import TopicList from '../components/TopicList'; |
||||
|
import PostList from '../components/PostList'; |
||||
|
import LoadingSpinner from '../components/LoadingSpinner'; |
||||
|
import { setNavBarTitle } from '../redux/actions/userInterfaceActions'; |
||||
|
|
||||
|
const callsInfo = [ |
||||
|
{ |
||||
|
contract: 'Forum', |
||||
|
method: 'getUsername' |
||||
|
}, { |
||||
|
contract: 'Forum', |
||||
|
method: 'getUserTopics' |
||||
|
}, { |
||||
|
contract: 'Forum', |
||||
|
method: 'getUserPosts' |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
class ProfileContainer extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
const { match, user } = this.props; |
||||
|
|
||||
|
this.getBlockchainData = this.getBlockchainData.bind(this); |
||||
|
|
||||
|
this.dataKey = []; |
||||
|
const address = match.params.address |
||||
|
? match.params.address |
||||
|
: user.address; |
||||
|
|
||||
|
this.state = { |
||||
|
pageStatus: 'initialized', |
||||
|
userAddress: address, |
||||
|
username: '', |
||||
|
topicIDs: null, |
||||
|
postIDs: null |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
const { setNavBarTitle } = this.props; |
||||
|
setNavBarTitle(''); |
||||
|
} |
||||
|
|
||||
|
getBlockchainData() { |
||||
|
const { userAddress, pageStatus, username, topicIDs, postIDs } = this.state; |
||||
|
const { drizzleStatus, setNavBarTitle, contracts } = this.props; |
||||
|
|
||||
|
if (pageStatus === 'initialized' |
||||
|
&& drizzleStatus.initialized) { |
||||
|
callsInfo.forEach((call, index) => { |
||||
|
this.dataKey[index] = drizzle.contracts[call.contract].methods[call.method].cacheCall( |
||||
|
userAddress, |
||||
|
); |
||||
|
}); |
||||
|
this.setState({ |
||||
|
pageStatus: 'loading' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (pageStatus === 'loading') { |
||||
|
let pageStatusUpdate = 'loaded'; |
||||
|
callsInfo.forEach((call, index) => { |
||||
|
if (!contracts[call.contract][call.method][this.dataKey[index]]) { |
||||
|
pageStatusUpdate = 'loading'; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (pageStatusUpdate === 'loaded') { |
||||
|
this.setState({ |
||||
|
pageStatus: pageStatusUpdate |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (pageStatus === 'loaded') { |
||||
|
if (username === '') { |
||||
|
const transaction = contracts[callsInfo[0].contract][callsInfo[0].method][this.dataKey[0]]; |
||||
|
if (transaction) { |
||||
|
const username = transaction.value; |
||||
|
setNavBarTitle(username); |
||||
|
this.setState({ |
||||
|
username |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
if (topicIDs === null) { |
||||
|
const transaction = contracts[callsInfo[1].contract][callsInfo[1].method][this.dataKey[1]]; |
||||
|
if (transaction) { |
||||
|
this.setState({ |
||||
|
topicIDs: transaction.value |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
if (postIDs === null) { |
||||
|
const transaction = contracts[callsInfo[2].contract][callsInfo[2].method][this.dataKey[2]]; |
||||
|
if (transaction) { |
||||
|
this.setState({ |
||||
|
postIDs: transaction.value |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* this.props.store.dispatch(hideProgressBar()); */ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { userAddress, username, topicIDs, postIDs } = this.state; |
||||
|
const { navigateTo, user } = this.props; |
||||
|
|
||||
|
if (!user.hasSignedUp) { |
||||
|
navigateTo('/signup'); |
||||
|
return (null); |
||||
|
} |
||||
|
|
||||
|
const infoTab = ( |
||||
|
<ProfileInformation |
||||
|
address={userAddress} |
||||
|
username={username} |
||||
|
numberOfTopics={topicIDs && topicIDs.length} |
||||
|
numberOfPosts={postIDs && postIDs.length} |
||||
|
self={userAddress === user.address} |
||||
|
key="profileInfo" |
||||
|
/> |
||||
|
); |
||||
|
const topicsTab = ( |
||||
|
<div className="profile-tab"> |
||||
|
{topicIDs |
||||
|
? <TopicList topicIDs={topicIDs} /> |
||||
|
: <LoadingSpinner /> |
||||
|
} |
||||
|
</div> |
||||
|
); |
||||
|
const postsTab = ( |
||||
|
<div className="profile-tab"> |
||||
|
{postIDs |
||||
|
? <PostList postIDs={postIDs} recentToTheTop /> |
||||
|
: <LoadingSpinner /> |
||||
|
} |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
const profilePanes = [ |
||||
|
{ |
||||
|
menuItem: 'INFORMATION', |
||||
|
pane: { |
||||
|
key: 'INFORMATION', |
||||
|
content: (infoTab) |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
menuItem: 'TOPICS', |
||||
|
pane: { |
||||
|
key: 'TOPICS', |
||||
|
content: (topicsTab) |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
menuItem: 'POSTS', |
||||
|
pane: { |
||||
|
key: 'POSTS', |
||||
|
content: (postsTab) |
||||
|
} |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<Tab |
||||
|
menu={{ |
||||
|
secondary: true, pointing: true |
||||
|
}} |
||||
|
panes={profilePanes} |
||||
|
renderActiveOnly={false} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ProfileContainer.propTypes = { |
||||
|
match: PropTypes.object.isRequired, |
||||
|
drizzleStatus: PropTypes.object.isRequired, |
||||
|
contracts: PropTypes.array.isRequired, |
||||
|
navigateTo: PropTypes.func.isRequired, |
||||
|
user: PropTypes.object.isRequired, |
||||
|
setNavBarTitle: PropTypes.func.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapDispatchToProps = dispatch => bindActionCreators({ |
||||
|
navigateTo: location => push(location), |
||||
|
setNavBarTitle: navBarTitle => setNavBarTitle(navBarTitle) |
||||
|
}, dispatch); |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
user: state.user, |
||||
|
drizzleStatus: state.drizzleStatus, |
||||
|
contracts: state.contracts, |
||||
|
orbitDB: state.orbitDB |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ProfileContainer); |
@ -0,0 +1,55 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
|
||||
|
import { Header } from 'semantic-ui-react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import UsernameFormContainer from './UsernameFormContainer'; |
||||
|
|
||||
|
class SignUpContainer extends Component { |
||||
|
componentDidUpdate(prevProps) { |
||||
|
const { user, history } = this.props; |
||||
|
if (user.hasSignedUp && !prevProps.user.hasSignedUp) history.push('/'); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { user } = this.props; |
||||
|
|
||||
|
return ( |
||||
|
user.hasSignedUp |
||||
|
? ( |
||||
|
<div className="vertical-center-in-parent"> |
||||
|
<Header color="teal" textAlign="center" as="h2"> |
||||
|
There is already an account for this addresss. |
||||
|
</Header> |
||||
|
<Header color="teal" textAlign="center" as="h4"> |
||||
|
If you want to create another account please change your address. |
||||
|
</Header> |
||||
|
</div> |
||||
|
) |
||||
|
: ( |
||||
|
<div className="sign-up-container"> |
||||
|
<div> |
||||
|
<h1>Sign Up</h1> |
||||
|
<p className="no-margin"> |
||||
|
<strong>Account address:</strong> |
||||
|
{' '} |
||||
|
{user.address} |
||||
|
</p> |
||||
|
<UsernameFormContainer /> |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
SignUpContainer.propTypes = { |
||||
|
user: PropTypes.object.isRequired, |
||||
|
history: PropTypes.object.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
user: state.user |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(SignUpContainer); |
@ -0,0 +1,167 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
|
||||
|
import { Button, Form, Icon, TextArea } from 'semantic-ui-react'; |
||||
|
import NewTopicPreview from '../components/NewTopicPreview'; |
||||
|
|
||||
|
import { createTopic } from '../redux/actions/transactionsActions'; |
||||
|
|
||||
|
class StartTopicContainer extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.handleInputChange = this.handleInputChange.bind(this); |
||||
|
this.handlePreviewToggle = this.handlePreviewToggle.bind(this); |
||||
|
this.validateAndPost = this.validateAndPost.bind(this); |
||||
|
|
||||
|
this.state = { |
||||
|
topicSubjectInput: '', |
||||
|
topicMessageInput: '', |
||||
|
topicSubjectInputEmptySubmit: false, |
||||
|
topicMessageInputEmptySubmit: false, |
||||
|
previewEnabled: false, |
||||
|
previewDate: '' |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
async validateAndPost() { |
||||
|
const { topicSubjectInput, topicMessageInput } = this.state; |
||||
|
const { dispatch, history } = this.props; |
||||
|
|
||||
|
if (topicSubjectInput === '' || topicMessageInput |
||||
|
=== '') { |
||||
|
this.setState({ |
||||
|
topicSubjectInputEmptySubmit: topicSubjectInput === '', |
||||
|
topicMessageInputEmptySubmit: topicMessageInput === '' |
||||
|
}); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
dispatch( |
||||
|
createTopic( |
||||
|
{ |
||||
|
topicSubject: topicSubjectInput, |
||||
|
topicMessage: topicMessageInput |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
history.push('/home'); |
||||
|
} |
||||
|
|
||||
|
handleInputChange(event) { |
||||
|
this.setState({ |
||||
|
[event.target.name]: event.target.value |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
handlePreviewToggle() { |
||||
|
this.setState((prevState) => ({ |
||||
|
previewEnabled: !prevState.previewEnabled, |
||||
|
previewDate: this.getDate() |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
getDate() { |
||||
|
const currentdate = new Date(); |
||||
|
return (`${currentdate.getMonth() + 1} ${ |
||||
|
currentdate.getDate()}, ${ |
||||
|
currentdate.getFullYear()}, ${ |
||||
|
currentdate.getHours()}:${ |
||||
|
currentdate.getMinutes()}:${ |
||||
|
currentdate.getSeconds()}`);
|
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
previewDate, previewEnabled, topicSubjectInputEmptySubmit, topicSubjectInput, |
||||
|
topicMessageInputEmptySubmit, topicMessageInput |
||||
|
} = this.state; |
||||
|
const { user, history } = this.props; |
||||
|
|
||||
|
if (!user.hasSignedUp) { |
||||
|
history.push('/signup'); |
||||
|
return (null); |
||||
|
} |
||||
|
|
||||
|
const previewEditText = previewEnabled ? 'Edit' : 'Preview'; |
||||
|
return ( |
||||
|
<div> |
||||
|
{previewEnabled |
||||
|
&& ( |
||||
|
<NewTopicPreview |
||||
|
date={previewDate} |
||||
|
subject={topicSubjectInput} |
||||
|
content={topicMessageInput} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
<Form> |
||||
|
{!previewEnabled |
||||
|
&& [ |
||||
|
<Form.Field key="topicSubjectInput"> |
||||
|
<Form.Input |
||||
|
name="topicSubjectInput" |
||||
|
error={topicSubjectInputEmptySubmit} |
||||
|
type="text" |
||||
|
value={topicSubjectInput} |
||||
|
placeholder="Subject" |
||||
|
id="topicSubjectInput" |
||||
|
onChange={this.handleInputChange} |
||||
|
/> |
||||
|
</Form.Field>, |
||||
|
<TextArea |
||||
|
key="topicMessageInput" |
||||
|
name="topicMessageInput" |
||||
|
className={topicMessageInputEmptySubmit |
||||
|
? 'form-textarea-required' |
||||
|
: ''} |
||||
|
value={topicMessageInput} |
||||
|
placeholder="Post" |
||||
|
id="topicMessageInput" |
||||
|
rows={5} |
||||
|
autoHeight |
||||
|
onChange={this.handleInputChange} |
||||
|
/>] |
||||
|
} |
||||
|
<br /> |
||||
|
<br /> |
||||
|
<Button.Group> |
||||
|
<Button |
||||
|
animated |
||||
|
key="submit" |
||||
|
type="button" |
||||
|
color="teal" |
||||
|
onClick={this.validateAndPost} |
||||
|
> |
||||
|
<Button.Content visible>Post</Button.Content> |
||||
|
<Button.Content hidden> |
||||
|
<Icon name="send" /> |
||||
|
</Button.Content> |
||||
|
</Button> |
||||
|
<Button |
||||
|
type="button" |
||||
|
color="yellow" |
||||
|
onClick={this.handlePreviewToggle} |
||||
|
> |
||||
|
{previewEditText} |
||||
|
</Button> |
||||
|
</Button.Group> |
||||
|
</Form> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
StartTopicContainer.propTypes = { |
||||
|
dispatch: PropTypes.func.isRequired, |
||||
|
history: PropTypes.object.isRequired, |
||||
|
user: PropTypes.object.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
orbitDB: state.orbitDB, |
||||
|
user: state.user |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(StartTopicContainer); |
@ -0,0 +1,212 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { bindActionCreators } from 'redux'; |
||||
|
import { push } from 'connected-react-router'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { drizzle } from '../index'; |
||||
|
|
||||
|
import PostList from '../components/PostList'; |
||||
|
import NewPost from '../components/NewPost'; |
||||
|
import FloatingButton from '../components/FloatingButton'; |
||||
|
|
||||
|
import { setNavBarTitle } from '../redux/actions/userInterfaceActions.js'; |
||||
|
|
||||
|
const contract = 'Forum'; |
||||
|
const getTopicMethod = 'getTopic'; |
||||
|
|
||||
|
class TopicContainer extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
const { match, navigateTo } = props; |
||||
|
|
||||
|
// Topic ID should be a positive integer
|
||||
|
if (!/^[0-9]+$/.test(match.params.topicId)) { |
||||
|
navigateTo('/404'); |
||||
|
} |
||||
|
|
||||
|
this.getBlockchainData = this.getBlockchainData.bind(this); |
||||
|
this.fetchTopicSubject = this.fetchTopicSubject.bind(this); |
||||
|
this.togglePostingState = this.togglePostingState.bind(this); |
||||
|
this.postCreated = this.postCreated.bind(this); |
||||
|
|
||||
|
this.state = { |
||||
|
pageStatus: 'initialized', |
||||
|
topicID: parseInt(match.params.topicId), |
||||
|
topicSubject: null, |
||||
|
postFocus: match.params.postId |
||||
|
&& /^[0-9]+$/.test(match.params.postId) |
||||
|
? match.params.postId |
||||
|
: null, |
||||
|
fetchTopicSubjectStatus: 'pending', |
||||
|
posting: false |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
this.getBlockchainData(); |
||||
|
} |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
const { setNavBarTitle } = this.props; |
||||
|
setNavBarTitle(''); |
||||
|
} |
||||
|
|
||||
|
getBlockchainData() { |
||||
|
const { pageStatus, topicID, fetchTopicSubjectStatus } = this.state; |
||||
|
const { drizzleStatus, orbitDB, contracts } = this.props; |
||||
|
|
||||
|
if (pageStatus === 'initialized' |
||||
|
&& drizzleStatus.initialized) { |
||||
|
this.dataKey = drizzle.contracts[contract].methods[getTopicMethod].cacheCall( |
||||
|
topicID, |
||||
|
); |
||||
|
this.setState({ |
||||
|
pageStatus: 'loading' |
||||
|
}); |
||||
|
} |
||||
|
if (pageStatus === 'loading' |
||||
|
&& contracts[contract][getTopicMethod][this.dataKey]) { |
||||
|
this.setState({ |
||||
|
pageStatus: 'loaded' |
||||
|
}); |
||||
|
if (orbitDB.orbitdb !== null) { |
||||
|
this.fetchTopicSubject( |
||||
|
contracts[contract][getTopicMethod][this.dataKey].value[0], |
||||
|
); |
||||
|
this.setState({ |
||||
|
fetchTopicSubjectStatus: 'fetching' |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
if (pageStatus === 'loaded' |
||||
|
&& fetchTopicSubjectStatus === 'pending' |
||||
|
&& orbitDB.orbitdb !== null) { |
||||
|
this.fetchTopicSubject( |
||||
|
contracts[contract][getTopicMethod][this.dataKey].value[0], |
||||
|
); |
||||
|
this.setState({ |
||||
|
fetchTopicSubjectStatus: 'fetching' |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async fetchTopicSubject(orbitDBAddress) { |
||||
|
const { topicID } = this.state; |
||||
|
const { contracts, user, orbitDB, setNavBarTitle } = this.props; |
||||
|
|
||||
|
let orbitData; |
||||
|
if (contracts[contract][getTopicMethod][this.dataKey].value[1] |
||||
|
=== user.address) { |
||||
|
orbitData = orbitDB.topicsDB.get(topicID); |
||||
|
} else { |
||||
|
const fullAddress = `/orbitdb/${orbitDBAddress}/topics`; |
||||
|
const store = await orbitDB.orbitdb.keyvalue(fullAddress); |
||||
|
await store.load(); |
||||
|
|
||||
|
const localOrbitData = store.get(topicID); |
||||
|
if (localOrbitData) { |
||||
|
orbitData = localOrbitData; |
||||
|
} else { |
||||
|
// Wait until we have received something from the network
|
||||
|
store.events.on('replicated', () => { |
||||
|
orbitData = store.get(topicID); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setNavBarTitle(orbitData.subject); |
||||
|
this.setState({ |
||||
|
topicSubject: orbitData.subject, |
||||
|
fetchTopicSubjectStatus: 'fetched' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
togglePostingState(event) { |
||||
|
if (event) { |
||||
|
event.preventDefault(); |
||||
|
} |
||||
|
this.setState(prevState => ({ |
||||
|
posting: !prevState.posting |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
postCreated() { |
||||
|
this.setState(prevState => ({ |
||||
|
posting: false |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { pageStatus, postFocus, topicID, topicSubject, posting } = this.state; |
||||
|
const { contracts, user } = this.props; |
||||
|
|
||||
|
let topicContents; |
||||
|
if (pageStatus === 'loaded') { |
||||
|
topicContents = ( |
||||
|
( |
||||
|
<div> |
||||
|
<PostList |
||||
|
postIDs={contracts[contract][getTopicMethod][this.dataKey].value[4]} |
||||
|
focusOnPost={postFocus |
||||
|
? postFocus |
||||
|
: null} |
||||
|
/> |
||||
|
{posting |
||||
|
&& ( |
||||
|
<NewPost |
||||
|
topicID={topicID} |
||||
|
subject={topicSubject} |
||||
|
postIndex={contracts[contract][getTopicMethod][this.dataKey].value[4].length} |
||||
|
onCancelClick={() => { this.togglePostingState(); }} |
||||
|
onPostCreated={() => { this.postCreated(); }} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
<div className="posts-list-spacer" /> |
||||
|
{user.hasSignedUp && !posting |
||||
|
&& <FloatingButton onClick={this.togglePostingState} /> |
||||
|
} |
||||
|
</div> |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className="fill"> |
||||
|
{topicContents} |
||||
|
{!posting |
||||
|
&& <div className="bottom-overlay-pad" /> |
||||
|
} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
TopicContainer.propTypes = { |
||||
|
drizzleStatus: PropTypes.object.isRequired, |
||||
|
orbitDB: PropTypes.object.isRequired, |
||||
|
setNavBarTitle: PropTypes.func.isRequired, |
||||
|
contracts: PropTypes.array.isRequired, |
||||
|
user: PropTypes.object.isRequired, |
||||
|
match: PropTypes.object.isRequired, |
||||
|
navigateTo: PropTypes.func.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapDispatchToProps = dispatch => bindActionCreators({ |
||||
|
navigateTo: location => push(location), |
||||
|
setNavBarTitle: navBarTitle => setNavBarTitle(navBarTitle) |
||||
|
}, dispatch); |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
user: state.user, |
||||
|
contracts: state.contracts, |
||||
|
drizzleStatus: state.drizzleStatus, |
||||
|
orbitDB: state.orbit |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps, mapDispatchToProps)(TopicContainer); |
@ -0,0 +1,153 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { withRouter } from 'react-router-dom'; |
||||
|
|
||||
|
import { Message } from 'semantic-ui-react'; |
||||
|
|
||||
|
class RightSideBar extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.handleMessageClick = this.handleMessageClick.bind(this); |
||||
|
this.handleMessageDismiss = this.handleMessageDismiss.bind(this); |
||||
|
|
||||
|
this.state = { |
||||
|
isTransactionMessageDismissed: [] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
handleMessageClick(index) { |
||||
|
const { transactionStack, history, transactions } = this.props; |
||||
|
|
||||
|
const transactionHash = transactionStack[index]; |
||||
|
if (transactions[transactionHash]) { |
||||
|
if (transactions[transactionHash].status === 'error') { |
||||
|
this.handleMessageDismiss(null, index); |
||||
|
} else if (transactions[transactionHash].receipt |
||||
|
&& transactions[transactionHash].receipt.events) { |
||||
|
switch (Object.keys( |
||||
|
transactions[transactionHash].receipt.events, |
||||
|
)[0]) { |
||||
|
case 'UserSignedUp': |
||||
|
history.push('/profile'); |
||||
|
this.handleMessageDismiss(null, index); |
||||
|
break; |
||||
|
case 'UsernameUpdated': |
||||
|
history.push('/profile'); |
||||
|
this.handleMessageDismiss(null, index); |
||||
|
break; |
||||
|
case 'TopicCreated': |
||||
|
history.push(`/topic/${ |
||||
|
transactions[transactionHash].receipt.events.TopicCreated.returnValues.topicID}`);
|
||||
|
this.handleMessageDismiss(null, index); |
||||
|
break; |
||||
|
case 'PostCreated': |
||||
|
history.push(`/topic/${ |
||||
|
transactions[transactionHash].receipt.events.PostCreated.returnValues.topicID |
||||
|
}/${ |
||||
|
transactions[transactionHash].receipt.events.PostCreated.returnValues.postID}`);
|
||||
|
this.handleMessageDismiss(null, index); |
||||
|
break; |
||||
|
default: |
||||
|
this.handleMessageDismiss(null, index); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
handleMessageDismiss(event, messageIndex) { |
||||
|
if (event !== null) { |
||||
|
event.stopPropagation(); |
||||
|
} |
||||
|
|
||||
|
const { isTransactionMessageDismissed } = this.state; |
||||
|
|
||||
|
const isTransactionMessageDismissedShallowCopy = isTransactionMessageDismissed.slice(); |
||||
|
isTransactionMessageDismissedShallowCopy[messageIndex] = true; |
||||
|
this.setState({ |
||||
|
isTransactionMessageDismissed: isTransactionMessageDismissedShallowCopy |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { isTransactionMessageDismissed } = this.state; |
||||
|
const { transactionStack, transactions } = this.props; |
||||
|
|
||||
|
if (transactionStack.length === 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const transactionMessages = transactionStack.map( |
||||
|
(transaction, index) => { |
||||
|
if (isTransactionMessageDismissed[index]) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
let color = 'black'; |
||||
|
const message = []; |
||||
|
message.push( |
||||
|
'New transaction has been queued and is waiting your confirmation.', |
||||
|
); |
||||
|
if (transactions[transaction]) { |
||||
|
message.push(<br key="confirmed" />); |
||||
|
message.push('- transaction confirmed'); |
||||
|
} |
||||
|
if (transactions[transaction] |
||||
|
&& transactions[transaction].status === 'success') { |
||||
|
/* Transaction completed successfully */ |
||||
|
message.push(<br key="mined" />); |
||||
|
message.push('- transaction mined'); |
||||
|
color = 'green'; |
||||
|
message.push(<br key="success" />); |
||||
|
message.push('- transaction completed successfully'); |
||||
|
} else if (transactions[transaction] |
||||
|
&& transactions[transaction].status === 'error') { |
||||
|
/* Transaction failed to complete */ |
||||
|
message.push(<br key="mined" />); |
||||
|
message.push('- transaction mined'); |
||||
|
color = 'red'; |
||||
|
message.push(<br key="fail" />); |
||||
|
message.push('Transaction failed to complete!'); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
className="sidebar-message" |
||||
|
key={index} |
||||
|
onClick={() => { this.handleMessageClick(index); }} |
||||
|
> |
||||
|
<Message |
||||
|
color={color} |
||||
|
onDismiss={(e) => { |
||||
|
this.handleMessageDismiss(e, index); |
||||
|
}} |
||||
|
> |
||||
|
{message} |
||||
|
</Message> |
||||
|
</div> |
||||
|
); |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
return (transactionMessages); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
RightSideBar.propTypes = { |
||||
|
transactionStack: PropTypes.array.isRequired, |
||||
|
history: PropTypes.object.isRequired, |
||||
|
transactions: PropTypes.PropTypes.objectOf(PropTypes.object).isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
transactions: state.transactions, |
||||
|
transactionStack: state.transactionStack |
||||
|
}); |
||||
|
|
||||
|
const RightSideBarContainer = withRouter( |
||||
|
connect(mapStateToProps)(RightSideBar), |
||||
|
); |
||||
|
|
||||
|
export default RightSideBarContainer; |
@ -0,0 +1,216 @@ |
|||||
|
import React, { Component } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
|
||||
|
import { Button, Dimmer, Form, Header, Loader, Message } from 'semantic-ui-react'; |
||||
|
|
||||
|
import { drizzle } from '../index'; |
||||
|
import { createDatabases } from '../utils/orbitUtils'; |
||||
|
import { updateUsername } from '../redux/actions/transactionsActions'; |
||||
|
|
||||
|
const contract = 'Forum'; |
||||
|
const checkUsernameTakenMethod = 'isUserNameTaken'; |
||||
|
const signUpMethod = 'signUp'; |
||||
|
|
||||
|
class UsernameFormContainer extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.handleInputChange = this.handleInputChange.bind(this); |
||||
|
this.handleSubmit = this.handleSubmit.bind(this); |
||||
|
this.completeAction = this.completeAction.bind(this); |
||||
|
this.checkedUsernames = []; |
||||
|
|
||||
|
this.state = { |
||||
|
usernameInput: '', |
||||
|
error: false, |
||||
|
errorHeader: '', |
||||
|
errorMessage: '', |
||||
|
signingUp: false |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
handleInputChange(e, { name, value }) { |
||||
|
this.setState({ |
||||
|
[name]: value, |
||||
|
error: false |
||||
|
}); |
||||
|
if (value !== '') { |
||||
|
if (this.checkedUsernames.length > 0) { |
||||
|
if (this.checkedUsernames.some(e => e.usernameChecked === value)) { |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
drizzle.contracts[contract].methods[checkUsernameTakenMethod].cacheCall( |
||||
|
value, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
handleSubmit() { |
||||
|
const { usernameInput, error } = this.state; |
||||
|
|
||||
|
if (usernameInput === '') { |
||||
|
this.setState({ |
||||
|
error: true, |
||||
|
errorHeader: 'Data Incomplete', |
||||
|
errorMessage: 'You need to provide a username' |
||||
|
}); |
||||
|
} else if (!error) { |
||||
|
// Makes sure current input username has been checked for availability
|
||||
|
if (this.checkedUsernames.some( |
||||
|
e => e.usernameChecked === usernameInput, |
||||
|
)) { |
||||
|
this.completeAction(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async completeAction() { |
||||
|
const { usernameInput } = this.state; |
||||
|
const { user, dispatch, account } = this.props; |
||||
|
|
||||
|
if (user.hasSignedUp) { |
||||
|
dispatch(updateUsername(...[usernameInput], null)); |
||||
|
} else { |
||||
|
this.setState({ |
||||
|
signingUp: true |
||||
|
}); |
||||
|
const orbitdbInfo = await createDatabases(); |
||||
|
this.stackId = drizzle.contracts[contract].methods[signUpMethod].cacheSend( |
||||
|
...[ |
||||
|
usernameInput, |
||||
|
orbitdbInfo.identityId, |
||||
|
orbitdbInfo.identityPublicKey, |
||||
|
orbitdbInfo.identityPrivateKey, |
||||
|
orbitdbInfo.orbitId, |
||||
|
orbitdbInfo.orbitPublicKey, |
||||
|
orbitdbInfo.orbitPrivateKey, |
||||
|
orbitdbInfo.topicsDB, |
||||
|
orbitdbInfo.postsDB |
||||
|
], { |
||||
|
from: account |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
this.setState({ |
||||
|
usernameInput: '' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate() { |
||||
|
const { signingUp, usernameInput, error } = this.state; |
||||
|
const { transactionStack, transactions, contracts } = this.props; |
||||
|
|
||||
|
if (signingUp) { |
||||
|
const txHash = transactionStack[this.stackId]; |
||||
|
if (txHash |
||||
|
&& transactions[txHash] |
||||
|
&& transactions[txHash].status === 'error') { |
||||
|
this.setState({ |
||||
|
signingUp: false |
||||
|
}); |
||||
|
} |
||||
|
} else { |
||||
|
const temp = Object.values( |
||||
|
contracts[contract][checkUsernameTakenMethod], |
||||
|
); |
||||
|
this.checkedUsernames = temp.map(checked => ({ |
||||
|
usernameChecked: checked.args[0], |
||||
|
isTaken: checked.value |
||||
|
})); |
||||
|
|
||||
|
if (this.checkedUsernames.length > 0) { |
||||
|
this.checkedUsernames.forEach((checked) => { |
||||
|
if (checked.usernameChecked === usernameInput |
||||
|
&& checked.isTaken && !error) { |
||||
|
this.setState({ |
||||
|
error: true, |
||||
|
errorHeader: 'Data disapproved', |
||||
|
errorMessage: 'This username is already taken' |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { error, usernameInput, errorHeader, errorMessage, signingUp } = this.state; |
||||
|
const { user } = this.props; |
||||
|
|
||||
|
if (user.hasSignedUp !== null) { |
||||
|
const buttonText = user.hasSignedUp ? 'Update' : 'Sign Up'; |
||||
|
const placeholderText = user.hasSignedUp |
||||
|
? user.username |
||||
|
: 'Username'; |
||||
|
const withError = error && { |
||||
|
error: true |
||||
|
}; |
||||
|
|
||||
|
/* var disableSubmit = true; |
||||
|
if (this.checkedUsernames.length > 0) { |
||||
|
if (this.checkedUsernames.some(e => e.usernameChecked === this.state.usernameInput)){ |
||||
|
disableSubmit = false; |
||||
|
} |
||||
|
} else { |
||||
|
disableSubmit = false; |
||||
|
} |
||||
|
|
||||
|
disableSubmit = (disableSubmit || this.state.error) && {loading: true}; */ |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<Form onSubmit={this.handleSubmit} {...withError}> |
||||
|
<Form.Field required> |
||||
|
<label>Username</label> |
||||
|
<Form.Input |
||||
|
placeholder={placeholderText} |
||||
|
name="usernameInput" |
||||
|
value={usernameInput} |
||||
|
onChange={this.handleInputChange} |
||||
|
/> |
||||
|
</Form.Field> |
||||
|
<Message |
||||
|
error |
||||
|
header={errorHeader} |
||||
|
content={errorMessage} |
||||
|
/> |
||||
|
<Button type="submit">{buttonText}</Button> |
||||
|
</Form> |
||||
|
<Dimmer active={signingUp} page> |
||||
|
<Header as="h2" inverted> |
||||
|
<Loader size="large"> |
||||
|
Magic elves are processing your noble |
||||
|
request. |
||||
|
</Loader> |
||||
|
</Header> |
||||
|
</Dimmer> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return (null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
UsernameFormContainer.propTypes = { |
||||
|
dispatch: PropTypes.func.isRequired, |
||||
|
account: PropTypes.string.isRequired, |
||||
|
transactionStack: PropTypes.array.isRequired, |
||||
|
transactions: PropTypes.array.isRequired, |
||||
|
contracts: PropTypes.array.isRequired, |
||||
|
hasSignedUp: PropTypes.object.isRequired, |
||||
|
user: PropTypes.object.isRequired |
||||
|
}; |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
account: state.accounts[0], |
||||
|
contracts: state.contracts, |
||||
|
transactions: state.transactions, |
||||
|
transactionStack: state.transactionStack, |
||||
|
user: state.user |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(UsernameFormContainer); |
@ -0,0 +1,12 @@ |
|||||
|
const epochTimeConverter = (timestamp) => { |
||||
|
const timestampDate = new Date(0); |
||||
|
timestampDate.setUTCSeconds(timestamp); |
||||
|
return (`${timestampDate.getMonth() + 1} ${ |
||||
|
timestampDate.getDate()}, ${ |
||||
|
timestampDate.getFullYear()}, ${ |
||||
|
timestampDate.getHours()}:${ |
||||
|
timestampDate.getMinutes()}:${ |
||||
|
timestampDate.getSeconds()}`);
|
||||
|
}; |
||||
|
|
||||
|
export default epochTimeConverter; |
@ -0,0 +1,33 @@ |
|||||
|
import React from 'react'; |
||||
|
import { render } from 'react-dom'; |
||||
|
import { Provider } from 'react-redux'; |
||||
|
import { ConnectedRouter } from 'connected-react-router'; |
||||
|
import { Drizzle } from 'drizzle'; |
||||
|
|
||||
|
import store, { history } from './redux/store'; |
||||
|
import routes from './router/routes'; |
||||
|
import { initIPFS } from './utils/orbitUtils'; |
||||
|
import * as serviceWorker from './utils/serviceWorker'; |
||||
|
|
||||
|
import './assets/css/index.css'; |
||||
|
import drizzleOptions from './config/drizzleOptions'; |
||||
|
import LoadingContainer from './containers/LoadingContainer'; |
||||
|
|
||||
|
initIPFS(); |
||||
|
|
||||
|
const drizzle = new Drizzle(drizzleOptions, store); |
||||
|
|
||||
|
export { drizzle }; |
||||
|
|
||||
|
render( |
||||
|
<Provider store={store}> |
||||
|
<LoadingContainer> |
||||
|
<ConnectedRouter history={history}> |
||||
|
{routes} |
||||
|
</ConnectedRouter> |
||||
|
</LoadingContainer> |
||||
|
</Provider>, |
||||
|
document.getElementById('root'), |
||||
|
); |
||||
|
|
||||
|
serviceWorker.unregister(); // See also: http://bit.ly/CRA-PWA
|
@ -0,0 +1 @@ |
|||||
|
export const DRIZZLE_UTILS_SAGA_INITIALIZED = 'DRIZZLE_UTILS_SAGA_INITIALIZED'; |
@ -0,0 +1,37 @@ |
|||||
|
const IPFS_INITIALIZED = 'IPFS_INITIALIZED'; |
||||
|
const DATABASES_CREATED = 'DATABASES_CREATED'; |
||||
|
const DATABASES_LOADED = 'DATABASES_LOADED'; |
||||
|
const DATABASES_NOT_READY = 'DATABASES_NOT_READY'; |
||||
|
const ADD_PEER_DATABASE = 'ADD_PEER_DATABASE'; |
||||
|
const OPENING_PEER_DATABASE = 'OPENING_PEER_DATABASE'; |
||||
|
const PEER_DATABASE_LOADED = 'PEER_DATABASE_LOADED'; |
||||
|
const UPDATE_PEERS = 'UPDATE_PEERS'; |
||||
|
|
||||
|
function updateDatabases(type, orbitdb, topicsDB, postsDB) { |
||||
|
return { |
||||
|
type, |
||||
|
orbitdb, |
||||
|
topicsDB, |
||||
|
postsDB, |
||||
|
id: orbitdb.id |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function addPeerDatabase(fullAddress) { |
||||
|
return { |
||||
|
type: ADD_PEER_DATABASE, |
||||
|
fullAddress |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export { DATABASES_CREATED, |
||||
|
DATABASES_LOADED, |
||||
|
DATABASES_NOT_READY, |
||||
|
IPFS_INITIALIZED, |
||||
|
UPDATE_PEERS, |
||||
|
ADD_PEER_DATABASE, |
||||
|
OPENING_PEER_DATABASE, |
||||
|
PEER_DATABASE_LOADED, |
||||
|
addPeerDatabase, |
||||
|
updateDatabases |
||||
|
}; |
@ -0,0 +1,53 @@ |
|||||
|
// Action creators
|
||||
|
|
||||
|
export const INIT_TRANSACTION = 'INIT_TRANSACTION'; |
||||
|
export const UPDATE_TRANSACTION = 'UPDATE_TRANSACTION'; |
||||
|
|
||||
|
export function updateUsername(newUsername, callback) { |
||||
|
return { |
||||
|
type: INIT_TRANSACTION, |
||||
|
transactionDescriptor: |
||||
|
{ |
||||
|
contract: 'Forum', |
||||
|
method: 'updateUsername', |
||||
|
params: [newUsername], |
||||
|
event: 'UsernameUpdated' |
||||
|
}, |
||||
|
callback |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function createTopic(userInputs) { |
||||
|
return { |
||||
|
type: INIT_TRANSACTION, |
||||
|
transactionDescriptor: |
||||
|
{ |
||||
|
contract: 'Forum', |
||||
|
method: 'createTopic', |
||||
|
params: [], |
||||
|
event: 'TopicCreated' |
||||
|
}, |
||||
|
userInputs |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function createPost(topicID, userInputs) { |
||||
|
return { |
||||
|
type: INIT_TRANSACTION, |
||||
|
transactionDescriptor: |
||||
|
{ |
||||
|
contract: 'Forum', |
||||
|
method: 'createPost', |
||||
|
params: [topicID], |
||||
|
event: 'PostCreated' |
||||
|
}, |
||||
|
userInputs |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function updateTransaction(transactionIndex, updateDescriptor) { |
||||
|
return { |
||||
|
type: UPDATE_TRANSACTION, |
||||
|
transactionUpdates: updateDescriptor |
||||
|
}; |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
// Action creators
|
||||
|
|
||||
|
export const SET_NAVBAR_TITLE = 'SET_NAVBAR_TITLE'; |
||||
|
|
||||
|
export function setNavBarTitle(newTitle) { |
||||
|
return { |
||||
|
type: SET_NAVBAR_TITLE, |
||||
|
title: newTitle |
||||
|
}; |
||||
|
} |
@ -0,0 +1,82 @@ |
|||||
|
import { |
||||
|
DATABASES_CREATED, |
||||
|
DATABASES_LOADED, |
||||
|
DATABASES_NOT_READY, OPENING_PEER_DATABASE, |
||||
|
IPFS_INITIALIZED, UPDATE_PEERS, PEER_DATABASE_LOADED |
||||
|
} from '../actions/orbitActions'; |
||||
|
|
||||
|
const initialState = { |
||||
|
ipfs: null, |
||||
|
ipfsInitialized: false, |
||||
|
ready: false, |
||||
|
orbitdb: null, |
||||
|
topicsDB: null, |
||||
|
postsDB: null, |
||||
|
pubsubPeers: {topicsDBPeers:[], postsDBPeers:[]}, |
||||
|
replicatedDatabases: [], |
||||
|
id: null |
||||
|
}; |
||||
|
|
||||
|
const orbitReducer = (state = initialState, action) => { |
||||
|
switch (action.type) { |
||||
|
case IPFS_INITIALIZED: |
||||
|
return { |
||||
|
...state, |
||||
|
ipfs: action.ipfs, |
||||
|
ipfsInitialized: true |
||||
|
}; |
||||
|
case DATABASES_CREATED: |
||||
|
return { |
||||
|
...state, |
||||
|
ready: true, |
||||
|
orbitdb: action.orbitdb, |
||||
|
topicsDB: action.topicsDB, |
||||
|
postsDB: action.postsDB, |
||||
|
id: action.id |
||||
|
}; |
||||
|
case DATABASES_LOADED: |
||||
|
return { |
||||
|
...state, |
||||
|
ready: true, |
||||
|
orbitdb: action.orbitdb, |
||||
|
topicsDB: action.topicsDB, |
||||
|
postsDB: action.postsDB, |
||||
|
id: action.id |
||||
|
}; |
||||
|
case DATABASES_NOT_READY: |
||||
|
return { |
||||
|
...state, |
||||
|
ready: false, |
||||
|
orbitdb: null, |
||||
|
topicsDB: null, |
||||
|
postsDB: null, |
||||
|
id: null |
||||
|
}; |
||||
|
case OPENING_PEER_DATABASE: |
||||
|
if(state.replicatedDatabases.find(db => db.fullAddress === action.fullAddress)) |
||||
|
return state; |
||||
|
return { |
||||
|
...state, |
||||
|
replicatedDatabases:[...state.replicatedDatabases, |
||||
|
{fullAddress: action.fullAddress, ready: false, store: null}] |
||||
|
}; |
||||
|
case PEER_DATABASE_LOADED: |
||||
|
return { |
||||
|
...state, |
||||
|
replicatedDatabases: [...state.replicatedDatabases.map((db) => { |
||||
|
if (db.fullAddress !== action.fullAddress) |
||||
|
return db; // This isn't the item we care about - keep it as-is
|
||||
|
return { ...db, ready: true, store: action.store} // Otherwise return an updated value
|
||||
|
})] |
||||
|
}; |
||||
|
case UPDATE_PEERS: |
||||
|
return { |
||||
|
...state, |
||||
|
pubsubPeers: {topicsDBPeers:action.topicsDBPeers, postsDBPeers:action.postsDBPeers} |
||||
|
}; |
||||
|
default: |
||||
|
return state; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default orbitReducer; |
@ -0,0 +1,14 @@ |
|||||
|
import { combineReducers } from 'redux'; |
||||
|
import { drizzleReducers } from 'drizzle'; |
||||
|
import { connectRouter } from 'connected-react-router'; |
||||
|
import userReducer from './userReducer'; |
||||
|
import orbitReducer from './orbitReducer'; |
||||
|
import userInterfaceReducer from './userInterfaceReducer'; |
||||
|
|
||||
|
export default history => combineReducers({ |
||||
|
router: connectRouter(history), |
||||
|
user: userReducer, |
||||
|
orbit: orbitReducer, |
||||
|
interface: userInterfaceReducer, |
||||
|
...drizzleReducers |
||||
|
}); |
@ -0,0 +1,18 @@ |
|||||
|
import { SET_NAVBAR_TITLE } from '../actions/userInterfaceActions'; |
||||
|
|
||||
|
const initialState = { |
||||
|
navBarTitle: '' |
||||
|
}; |
||||
|
|
||||
|
const userInterfaceReducer = (state = initialState, action) => { |
||||
|
switch (action.type) { |
||||
|
case SET_NAVBAR_TITLE: |
||||
|
return { |
||||
|
navBarTitle: action.title |
||||
|
}; |
||||
|
default: |
||||
|
return state; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default userInterfaceReducer; |
@ -0,0 +1,27 @@ |
|||||
|
const initialState = { |
||||
|
username: '', |
||||
|
address: '0x0', |
||||
|
avatarUrl: '', |
||||
|
hasSignedUp: false |
||||
|
}; |
||||
|
|
||||
|
const userReducer = (state = initialState, action) => { |
||||
|
switch (action.type) { |
||||
|
case 'USER_DATA_UPDATED_(AUTHENTICATED)': |
||||
|
return { |
||||
|
username: action.username, |
||||
|
address: action.address, |
||||
|
hasSignedUp: true |
||||
|
}; |
||||
|
case 'USER_DATA_UPDATED_(GUEST)': |
||||
|
return { |
||||
|
username: '', |
||||
|
address: action.address, |
||||
|
hasSignedUp: false |
||||
|
}; |
||||
|
default: |
||||
|
return state; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default userReducer; |
@ -0,0 +1,37 @@ |
|||||
|
import { call, put, select, takeLatest } from 'redux-saga/effects'; |
||||
|
import { getContractInstance, getWeb3 } from '../../utils/drizzleUtils'; |
||||
|
|
||||
|
import Forum from '../../contracts/Forum'; |
||||
|
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions'; |
||||
|
|
||||
|
const accounts = state => state.accounts; |
||||
|
let initFlag, web3, forumContract; |
||||
|
|
||||
|
function* init() { |
||||
|
if (!initFlag) { |
||||
|
web3 = yield call(getWeb3); |
||||
|
forumContract = yield call(getContractInstance, { |
||||
|
web3, artifact: Forum |
||||
|
}); |
||||
|
initFlag = true; |
||||
|
yield put({ |
||||
|
type: DRIZZLE_UTILS_SAGA_INITIALIZED, ...[] |
||||
|
}); |
||||
|
} |
||||
|
else console.warn('Attempted to reinitialize drizzleUtilsSaga!'); |
||||
|
} |
||||
|
|
||||
|
// If the method below proves to be problematic/ineffective (i.e. getting current account
|
||||
|
// from state), consider getting it from @drizzle-utils/get-accounts instead
|
||||
|
// with (yield call(getAccounts, {web3}))[0];
|
||||
|
function* getCurrentAccount() { |
||||
|
return (yield select(accounts))[0]; |
||||
|
} |
||||
|
|
||||
|
function* drizzleUtilsSaga() { |
||||
|
yield takeLatest('DRIZZLE_INITIALIZED', init); |
||||
|
} |
||||
|
|
||||
|
export { web3, forumContract, getCurrentAccount }; |
||||
|
|
||||
|
export default drizzleUtilsSaga; |
@ -0,0 +1,22 @@ |
|||||
|
import { put, takeEvery } from 'redux-saga/effects'; |
||||
|
|
||||
|
const EVENT_FIRED = 'EVENT_FIRED'; // This is fired internally by drizzle
|
||||
|
const CONTRACT_EVENT_FIRED = 'CONTRACT_EVENT_FIRED'; |
||||
|
|
||||
|
let eventSet = new Set(); |
||||
|
|
||||
|
// Entire purpose of this saga is to bypass a strange bug where EVENT_FIRED is called
|
||||
|
// multiple times for the same event
|
||||
|
function* sanitizeEvent(action) { |
||||
|
const size = eventSet.size; |
||||
|
eventSet.add(action.event.transactionHash + action.name); |
||||
|
if(eventSet.size>size) |
||||
|
yield put({ ...action, type: CONTRACT_EVENT_FIRED }); |
||||
|
} |
||||
|
|
||||
|
function* eventSaga() { |
||||
|
yield takeEvery(EVENT_FIRED, sanitizeEvent); |
||||
|
} |
||||
|
|
||||
|
export default eventSaga; |
||||
|
export { CONTRACT_EVENT_FIRED } |
@ -0,0 +1,106 @@ |
|||||
|
import { all, call, put, select, take, takeEvery, takeLatest } from 'redux-saga/effects'; |
||||
|
import isEqual from 'lodash.isequal'; |
||||
|
import { forumContract, getCurrentAccount } from './drizzleUtilsSaga'; |
||||
|
import { loadDatabases, orbitSagaOpen } from '../../utils/orbitUtils'; |
||||
|
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions'; |
||||
|
import { |
||||
|
ADD_PEER_DATABASE, PEER_DATABASE_LOADED, |
||||
|
DATABASES_NOT_READY, |
||||
|
IPFS_INITIALIZED, OPENING_PEER_DATABASE, |
||||
|
UPDATE_PEERS |
||||
|
} from '../actions/orbitActions'; |
||||
|
|
||||
|
let latestAccount; |
||||
|
|
||||
|
function* getOrbitDBInfo() { |
||||
|
yield put({ |
||||
|
type: 'ORRBIT_GETTING_INFO', ...[] |
||||
|
}); |
||||
|
const account = yield call(getCurrentAccount); |
||||
|
if (account !== latestAccount) { |
||||
|
const txObj1 = yield call(forumContract.methods.hasUserSignedUp, |
||||
|
...[account]); |
||||
|
try { |
||||
|
const callResult = yield call(txObj1.call, { |
||||
|
address: account |
||||
|
}); |
||||
|
if (callResult) { |
||||
|
const txObj2 = yield call(forumContract.methods.getOrbitIdentityInfo, |
||||
|
...[account]); |
||||
|
const orbitIdentityInfo = yield call(txObj2.call, { |
||||
|
address: account |
||||
|
}); |
||||
|
const txObj3 = yield call(forumContract.methods.getOrbitDBInfo, |
||||
|
...[account]); |
||||
|
const orbitDBInfo = yield call(txObj3.call, { |
||||
|
address: account |
||||
|
}); |
||||
|
yield call(loadDatabases, orbitIdentityInfo[0], orbitIdentityInfo[1], |
||||
|
orbitIdentityInfo[2], |
||||
|
orbitDBInfo[0], orbitDBInfo[1], orbitDBInfo[2], orbitDBInfo[3], |
||||
|
orbitDBInfo[4]); |
||||
|
} else { |
||||
|
yield put({ |
||||
|
type: DATABASES_NOT_READY, ...[] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
latestAccount = account; |
||||
|
} catch (error) { |
||||
|
console.error(error); |
||||
|
yield put({ |
||||
|
type: 'ORBIT_SAGA_ERROR', ...[] |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let peerOrbitAddresses = new Set(); |
||||
|
|
||||
|
function* addPeerDatabase(action) { |
||||
|
const fullAddress = action.fullAddress; |
||||
|
const size = peerOrbitAddresses.size; |
||||
|
peerOrbitAddresses.add(fullAddress); |
||||
|
|
||||
|
if(peerOrbitAddresses.size>size){ |
||||
|
const { orbitdb } = yield select(state => state.orbit); |
||||
|
if(orbitdb){ |
||||
|
yield put.resolve({ |
||||
|
type: OPENING_PEER_DATABASE, fullAddress |
||||
|
}); |
||||
|
const store = yield call(orbitSagaOpen, orbitdb, fullAddress); |
||||
|
yield put({ |
||||
|
type: PEER_DATABASE_LOADED, fullAddress, store: store |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//Keeps track of connected pubsub peers in Redux store
|
||||
|
function* updatePeersState() { |
||||
|
const orbit = yield select(state => state.orbit); |
||||
|
if(orbit.ready){ |
||||
|
const topicsDBAddress = orbit.topicsDB.address.toString(); |
||||
|
const postsDBAddress = orbit.postsDB.address.toString(); |
||||
|
const topicsDBPeers = yield call(orbit.ipfs.pubsub.peers, topicsDBAddress); |
||||
|
const postsDBPeers = yield call(orbit.ipfs.pubsub.peers, postsDBAddress); |
||||
|
if(!isEqual(topicsDBPeers.sort(), orbit.pubsubPeers.topicsDBPeers.sort()) || |
||||
|
!isEqual(postsDBPeers.sort(), orbit.pubsubPeers.postsDBPeers.sort())){ |
||||
|
yield put({ |
||||
|
type: UPDATE_PEERS, topicsDBPeers, postsDBPeers |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function* orbitSaga() { |
||||
|
yield all([ |
||||
|
take(DRIZZLE_UTILS_SAGA_INITIALIZED), |
||||
|
take(IPFS_INITIALIZED) |
||||
|
]); |
||||
|
yield takeLatest('ACCOUNT_CHANGED', getOrbitDBInfo); |
||||
|
yield takeEvery(ADD_PEER_DATABASE, addPeerDatabase); |
||||
|
yield takeEvery('ACCOUNTS_FETCHED', updatePeersState); |
||||
|
} |
||||
|
|
||||
|
export default orbitSaga; |
@ -0,0 +1,20 @@ |
|||||
|
import { all, fork } from 'redux-saga/effects'; |
||||
|
import { drizzleSagas } from 'drizzle'; |
||||
|
import drizzleUtilsSaga from './drizzleUtilsSaga'; |
||||
|
import userSaga from './userSaga'; |
||||
|
import orbitSaga from './orbitSaga'; |
||||
|
import transactionsSaga from './transactionsSaga'; |
||||
|
import eventSaga from './eventSaga'; |
||||
|
|
||||
|
export default function* root() { |
||||
|
const sagas = [ |
||||
|
...drizzleSagas, |
||||
|
drizzleUtilsSaga, |
||||
|
orbitSaga, |
||||
|
userSaga, |
||||
|
eventSaga, |
||||
|
transactionsSaga]; |
||||
|
yield all( |
||||
|
sagas.map(saga => fork(saga)), |
||||
|
); |
||||
|
} |
@ -0,0 +1,82 @@ |
|||||
|
import { call, select, take, takeEvery } from 'redux-saga/effects'; |
||||
|
|
||||
|
import { drizzle } from '../../index'; |
||||
|
import { orbitSagaPut } from '../../utils/orbitUtils'; |
||||
|
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions'; |
||||
|
import { CONTRACT_EVENT_FIRED } from './eventSaga'; |
||||
|
|
||||
|
const transactionsHistory = Object.create(null); |
||||
|
|
||||
|
function* initTransaction(action) { |
||||
|
const dataKey = drizzle.contracts[action.transactionDescriptor.contract] |
||||
|
.methods[action.transactionDescriptor.method].cacheSend( |
||||
|
...(action.transactionDescriptor.params), |
||||
|
); |
||||
|
|
||||
|
transactionsHistory[dataKey] = action; |
||||
|
transactionsHistory[dataKey].state = 'initialized'; |
||||
|
} |
||||
|
|
||||
|
function* handleEvent(action) { |
||||
|
const transactionStack = yield select(state => state.transactionStack); |
||||
|
const dataKey = transactionStack.indexOf(action.event.transactionHash); |
||||
|
switch (action.event.event) { |
||||
|
case 'TopicCreated': |
||||
|
if (dataKey !== -1 |
||||
|
&& transactionsHistory[dataKey] |
||||
|
&& transactionsHistory[dataKey].state === 'initialized') { |
||||
|
transactionsHistory[dataKey].state = 'success'; |
||||
|
// Gets orbit
|
||||
|
const orbit = yield select(state => state.orbit); |
||||
|
// And saves the topic
|
||||
|
yield call(orbitSagaPut, orbit.topicsDB, |
||||
|
action.event.returnValues.topicID, |
||||
|
{ |
||||
|
subject: transactionsHistory[dataKey].userInputs.topicSubject |
||||
|
}); |
||||
|
yield call(orbitSagaPut, orbit.postsDB, |
||||
|
action.event.returnValues.postID, |
||||
|
{ |
||||
|
subject: transactionsHistory[dataKey].userInputs.topicSubject, |
||||
|
content: transactionsHistory[dataKey].userInputs.topicMessage |
||||
|
}); |
||||
|
} |
||||
|
break; |
||||
|
case 'PostCreated': |
||||
|
if (dataKey !== -1 |
||||
|
&& transactionsHistory[dataKey] |
||||
|
&& transactionsHistory[dataKey].state === 'initialized') { |
||||
|
transactionsHistory[dataKey].state = 'success'; |
||||
|
// Gets orbit
|
||||
|
const orbit = yield select(state => state.orbit); |
||||
|
// And saves the topic
|
||||
|
yield call(orbitSagaPut, orbit.postsDB, |
||||
|
action.event.returnValues.postID, |
||||
|
{ |
||||
|
subject: transactionsHistory[dataKey].userInputs.postSubject, |
||||
|
content: transactionsHistory[dataKey].userInputs.postMessage |
||||
|
}); |
||||
|
} |
||||
|
break; |
||||
|
default: |
||||
|
// Nothing to do here
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function* handleError() { |
||||
|
const transactionStack = yield select(state => state.transactionStack); |
||||
|
transactionStack.forEach((transaction, index) => { |
||||
|
if (transaction.startsWith('TEMP_')) { |
||||
|
transactionsHistory[index].state = 'error'; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function* transactionsSaga() { |
||||
|
yield take(DRIZZLE_UTILS_SAGA_INITIALIZED); |
||||
|
yield takeEvery('INIT_TRANSACTION', initTransaction); |
||||
|
yield takeEvery(CONTRACT_EVENT_FIRED, handleEvent); |
||||
|
yield takeEvery('TX_ERROR', handleError); |
||||
|
} |
||||
|
|
||||
|
export default transactionsSaga; |
@ -0,0 +1,61 @@ |
|||||
|
import { call, put, select, take, takeEvery } from 'redux-saga/effects'; |
||||
|
|
||||
|
import { forumContract, getCurrentAccount } from './drizzleUtilsSaga'; |
||||
|
import { DRIZZLE_UTILS_SAGA_INITIALIZED } from '../actions/drizzleUtilsActions'; |
||||
|
|
||||
|
let account; |
||||
|
|
||||
|
function* updateUserData() { |
||||
|
const currentAccount = yield call(getCurrentAccount); |
||||
|
if (currentAccount !== account) { |
||||
|
account = currentAccount; |
||||
|
yield put({ |
||||
|
type: 'ACCOUNT_CHANGED', ...[] |
||||
|
}); |
||||
|
} |
||||
|
const txObj1 = yield call(forumContract.methods.hasUserSignedUp, ...[account]); |
||||
|
try { |
||||
|
const userState = yield call(getUserState); |
||||
|
const callResult = yield call(txObj1.call, { |
||||
|
address: account |
||||
|
}); |
||||
|
if (callResult) { |
||||
|
const txObj2 = yield call(forumContract.methods.getUsername, ...[account]); |
||||
|
const username = yield call(txObj2.call, { |
||||
|
address: account |
||||
|
}); |
||||
|
if (account !== userState.address || username !== userState.username) { |
||||
|
const dispatchArgs = { |
||||
|
address: account, |
||||
|
username |
||||
|
}; |
||||
|
yield put({ |
||||
|
type: 'USER_DATA_UPDATED_(AUTHENTICATED)', ...dispatchArgs |
||||
|
}); |
||||
|
} |
||||
|
} else if (account !== userState.address) { |
||||
|
const dispatchArgs = { |
||||
|
address: account |
||||
|
}; |
||||
|
yield put({ |
||||
|
type: 'USER_DATA_UPDATED_(GUEST)', ...dispatchArgs |
||||
|
}); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error(error); |
||||
|
yield put({ |
||||
|
type: 'USER_FETCHING_ERROR', ...[] |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function* getUserState() { |
||||
|
return yield select(state => state.user); |
||||
|
} |
||||
|
|
||||
|
function* userSaga() { |
||||
|
yield take(DRIZZLE_UTILS_SAGA_INITIALIZED); |
||||
|
yield takeEvery('ACCOUNTS_FETCHED', updateUserData); |
||||
|
} |
||||
|
|
||||
|
export default userSaga; |
@ -0,0 +1,35 @@ |
|||||
|
import { applyMiddleware, compose, createStore } from 'redux'; |
||||
|
import { createBrowserHistory } from 'history'; |
||||
|
import createSagaMiddleware from 'redux-saga'; |
||||
|
import { generateContractsInitialState } from 'drizzle'; |
||||
|
import { routerMiddleware } from 'connected-react-router'; |
||||
|
|
||||
|
import rootSaga from './sagas/rootSaga'; |
||||
|
import drizzleOptions from '../config/drizzleOptions'; |
||||
|
import createRootReducer from './reducers/rootReducer'; |
||||
|
|
||||
|
export const history = createBrowserHistory(); |
||||
|
|
||||
|
const rootReducer = createRootReducer(history); |
||||
|
|
||||
|
const initialState = { |
||||
|
contracts: generateContractsInitialState(drizzleOptions) |
||||
|
}; |
||||
|
|
||||
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; |
||||
|
|
||||
|
const sagaMiddleware = createSagaMiddleware(); |
||||
|
const routingMiddleware = routerMiddleware(history); |
||||
|
const composedEnhancers = composeEnhancers( |
||||
|
applyMiddleware(sagaMiddleware, routingMiddleware), |
||||
|
); |
||||
|
|
||||
|
const store = createStore( |
||||
|
rootReducer, |
||||
|
initialState, |
||||
|
composedEnhancers, |
||||
|
); |
||||
|
|
||||
|
sagaMiddleware.run(rootSaga); |
||||
|
|
||||
|
export default store; |
@ -0,0 +1,27 @@ |
|||||
|
import React from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Redirect, Route } from 'react-router-dom'; |
||||
|
|
||||
|
const PrivateRoute = ({ component: Component, ...rest }) => ( |
||||
|
<Route |
||||
|
{...rest} |
||||
|
render={props => (props.hasSignedUp ? ( |
||||
|
<Component {...props} /> |
||||
|
) : ( |
||||
|
<Redirect to={{ |
||||
|
pathname: '/signup', |
||||
|
state: { |
||||
|
from: props.location |
||||
|
} |
||||
|
}} |
||||
|
/> |
||||
|
)) |
||||
|
} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
const mapStateToProps = state => ({ |
||||
|
hasSignedUp: state.user.hasSignedUp |
||||
|
}); |
||||
|
|
||||
|
export default connect(mapStateToProps)(PrivateRoute); |
@ -0,0 +1,31 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Redirect, Route, Switch } from 'react-router-dom'; |
||||
|
import CoreLayoutContainer from '../containers/CoreLayoutContainer'; |
||||
|
import HomeContainer from '../containers/HomeContainer'; |
||||
|
import SignUpContainer from '../containers/SignUpContainer'; |
||||
|
import StartTopicContainer from '../containers/StartTopicContainer'; |
||||
|
import TopicContainer from '../containers/TopicContainer'; |
||||
|
import ProfileContainer from '../containers/ProfileContainer'; |
||||
|
import NotFound from '../components/NotFound'; |
||||
|
|
||||
|
const routes = ( |
||||
|
<div> |
||||
|
<CoreLayoutContainer> |
||||
|
<Switch> |
||||
|
<Route exact path="/" component={HomeContainer} /> |
||||
|
<Redirect from="/home" to="/" /> |
||||
|
<Route path="/signup" component={SignUpContainer} /> |
||||
|
<Route path="/startTopic" component={StartTopicContainer} /> |
||||
|
<Route path="/topic/:topicId/:postId?" component={TopicContainer} /> |
||||
|
<Route |
||||
|
path="/profile/:address?/:username?" |
||||
|
component={ProfileContainer} |
||||
|
/> |
||||
|
<Route path="/404" component={NotFound} /> |
||||
|
<Route component={NotFound} /> |
||||
|
</Switch> |
||||
|
</CoreLayoutContainer> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
export default routes; |
@ -0,0 +1,86 @@ |
|||||
|
// See also: https://github.com/trufflesuite/drizzle-utils
|
||||
|
const Web3 = require('web3'); |
||||
|
|
||||
|
const resolveWeb3 = (resolve, options, isBrowser) => { |
||||
|
let provider; |
||||
|
|
||||
|
if (options.customProvider) { |
||||
|
// use custom provider from options object
|
||||
|
provider = options.customProvider; |
||||
|
} else if (isBrowser && window.ethereum) { |
||||
|
// use `ethereum` object injected by MetaMask
|
||||
|
provider = window.ethereum; |
||||
|
} else if (isBrowser && typeof window.web3 !== 'undefined') { |
||||
|
// use injected web3 object by legacy dapp browsers
|
||||
|
provider = window.web3.currentProvider; |
||||
|
} else if (options.fallbackProvider) { |
||||
|
// use fallback provider from options object
|
||||
|
provider = options.fallbackProvider; |
||||
|
} else { |
||||
|
// connect to development blockchain from `truffle develop`
|
||||
|
provider = new Web3.providers.HttpProvider('http://127.0.0.1:9545'); |
||||
|
} |
||||
|
|
||||
|
const web3 = new Web3(provider); |
||||
|
resolve(web3); |
||||
|
}; |
||||
|
|
||||
|
const getWeb3 = (options = { |
||||
|
}) => new Promise((resolve) => { |
||||
|
// handle server-side and React Native environments
|
||||
|
const isReactNative = typeof navigator !== 'undefined' && navigator.product |
||||
|
=== 'ReactNative'; |
||||
|
const isNode = typeof window === 'undefined'; |
||||
|
if (isNode || isReactNative) { |
||||
|
return resolveWeb3(resolve, options, false); |
||||
|
} |
||||
|
|
||||
|
// if page is ready, resolve for web3 immediately
|
||||
|
if (document.readyState === 'complete') { |
||||
|
return resolveWeb3(resolve, options, true); |
||||
|
} |
||||
|
|
||||
|
// otherwise, resolve for web3 when page is done loading
|
||||
|
return window.addEventListener('load', () => resolveWeb3(resolve, options, true)); |
||||
|
}); |
||||
|
|
||||
|
const getContractInstance = (options = { |
||||
|
}) => new Promise(async (resolve, reject) => { |
||||
|
if (!options.web3) { |
||||
|
return reject(new Error('The options object with web3 is required.')); |
||||
|
} |
||||
|
|
||||
|
const { web3 } = options; |
||||
|
|
||||
|
let instance; |
||||
|
try { |
||||
|
if (options.artifact) { |
||||
|
// if artifact exists, attempt to get network ID and the deployed address
|
||||
|
const { artifact } = options; |
||||
|
const networkId = await web3.eth.net.getId(); // web3 v1.0.0-beta.47 breaks here
|
||||
|
const deployedNetwork = artifact.networks[networkId]; |
||||
|
|
||||
|
// if no deployed address is found, instantiate without the address
|
||||
|
const address = deployedNetwork && deployedNetwork.address; |
||||
|
|
||||
|
instance = new web3.eth.Contract(artifact.abi, address); |
||||
|
} else if (options.abi) { |
||||
|
// otherwise, use passed-in ABI and deployed address (optional)
|
||||
|
const { abi, address } = options; |
||||
|
|
||||
|
instance = new web3.eth.Contract(abi, address); |
||||
|
} else { |
||||
|
return reject( |
||||
|
new Error( |
||||
|
'You must pass in a contract artifact or the ABI of a deployed contract.', |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return resolve(instance); |
||||
|
} catch (err) { |
||||
|
return reject(err); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export { getWeb3, getContractInstance }; |
@ -0,0 +1,109 @@ |
|||||
|
import OrbitDB from 'orbit-db'; |
||||
|
import Keystore from 'orbit-db-keystore'; |
||||
|
import path from 'path'; |
||||
|
import IPFS from 'ipfs'; |
||||
|
import store from '../redux/store'; |
||||
|
import { DATABASES_CREATED, DATABASES_LOADED, IPFS_INITIALIZED, updateDatabases } from '../redux/actions/orbitActions'; |
||||
|
import ipfsOptions from '../config/ipfsOptions'; |
||||
|
|
||||
|
function initIPFS() { |
||||
|
const ipfs = new IPFS(ipfsOptions); |
||||
|
ipfs.on('error', (error) => console.error(`IPFS error: ${error}`)); |
||||
|
ipfs.on('ready', async () => { |
||||
|
store.dispatch({ |
||||
|
type: IPFS_INITIALIZED, ipfs |
||||
|
}); |
||||
|
console.debug('IPFS initialized.'); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async function createDatabases() { |
||||
|
console.debug("Deleting local storage..."); // Else we are in danger of reusing an existing orbit
|
||||
|
localStorage.clear(); // Perhaps not needed at all when orbit ids are used in Orbit 0.20.x+
|
||||
|
console.debug('Creating databases...'); |
||||
|
const ipfs = getIPFS(); |
||||
|
const orbitdb = await new OrbitDB(ipfs); |
||||
|
const topicsDB = await orbitdb.keyvalue('topics'); |
||||
|
const postsDB = await orbitdb.keyvalue('posts'); |
||||
|
store.dispatch( |
||||
|
updateDatabases(DATABASES_CREATED, orbitdb, topicsDB, postsDB), |
||||
|
); |
||||
|
|
||||
|
const orbitKey = orbitdb.keystore.getKey(orbitdb.id); |
||||
|
|
||||
|
return { |
||||
|
identityId: 'Tempus', |
||||
|
identityPublicKey: 'edax', |
||||
|
identityPrivateKey: 'rerum', |
||||
|
orbitId: orbitdb.id, |
||||
|
orbitPublicKey: orbitKey.getPublic('hex'), |
||||
|
orbitPrivateKey: orbitKey.getPrivate('hex'), |
||||
|
topicsDB: topicsDB.address.root, |
||||
|
postsDB: postsDB.address.root |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
async function loadDatabases(identityId, identityPublicKey, identityPrivateKey, |
||||
|
orbitId, orbitPublicKey, orbitPrivateKey, |
||||
|
topicsDBId, postsDBId) { |
||||
|
const directory = './orbitdb'; |
||||
|
const keystore = Keystore.create(path.join(directory, orbitId, '/keystore')); |
||||
|
|
||||
|
keystore._storage.setItem(orbitId, JSON.stringify({ |
||||
|
publicKey: orbitPublicKey, |
||||
|
privateKey: orbitPrivateKey |
||||
|
})); |
||||
|
|
||||
|
const ipfs = getIPFS(); |
||||
|
const orbitdb = await new OrbitDB(ipfs, directory, |
||||
|
{ |
||||
|
peerId: orbitId, keystore |
||||
|
}); |
||||
|
const topicsDB = await orbitdb.keyvalue(`/orbitdb/${topicsDBId}/topics`) |
||||
|
.catch((error) => console.error(`TopicsDB init error: ${error}`)); |
||||
|
const postsDB = await orbitdb.keyvalue(`/orbitdb/${postsDBId}/posts`) |
||||
|
.catch((error) => console.error(`PostsDB init error: ${error}`)); |
||||
|
|
||||
|
await topicsDB.load().catch((error) => console.error(`TopicsDB loading error: ${error}`)); |
||||
|
await postsDB.load().catch((error) => console.error(`PostsDB loading error: ${error}`)); |
||||
|
|
||||
|
//It is possible that we lack our own data and need to replicate them from somewhere else
|
||||
|
topicsDB.events.on('replicate', (address) => { |
||||
|
console.log(`TopicsDB Replicating (${address}).`); |
||||
|
}); |
||||
|
topicsDB.events.on('replicated', (address) => { |
||||
|
console.log(`TopicsDB replicated (${address}).`); |
||||
|
}); |
||||
|
postsDB.events.on('replicate', (address) => { |
||||
|
console.log(`PostsDB replicating (${address}).`); |
||||
|
}); |
||||
|
postsDB.events.on('replicated', (address) => { |
||||
|
console.log(`PostsDB replicated (${address}).`); |
||||
|
}); |
||||
|
|
||||
|
console.debug('Orbit databases loaded successfully.'); |
||||
|
store.dispatch(updateDatabases(DATABASES_LOADED, orbitdb, topicsDB, postsDB)); |
||||
|
} |
||||
|
|
||||
|
function getIPFS() { |
||||
|
return store.getState().orbit.ipfs; |
||||
|
} |
||||
|
|
||||
|
async function orbitSagaPut(db, key, value) { |
||||
|
await db.put(key, value).catch((error) => console.error(`Orbit put error: ${error}`)); |
||||
|
} |
||||
|
|
||||
|
async function orbitSagaOpen(orbitdb, address) { |
||||
|
const store = await orbitdb.keyvalue(address) |
||||
|
.catch((error) => console.error(`Error opening a peer's db: ${error}`)); |
||||
|
await store.load().catch((error) => console.log(error)); |
||||
|
store.events.on('replicate', (address) => { |
||||
|
console.log(`A peer's DB is being replicated (${address}).`); |
||||
|
}); |
||||
|
store.events.on('replicated', (address) => { |
||||
|
console.log(`A peer's DB was replicated (${address}).`); |
||||
|
}); |
||||
|
return store; |
||||
|
} |
||||
|
|
||||
|
export { initIPFS, createDatabases, loadDatabases, orbitSagaPut, orbitSagaOpen }; |
@ -0,0 +1,130 @@ |
|||||
|
// This optional code is used to register a service worker.
|
||||
|
// register() is not called by default.
|
||||
|
|
||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||
|
// resources are updated in the background.
|
||||
|
|
||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||
|
// opt-in, read http://bit.ly/CRA-PWA
|
||||
|
|
||||
|
const isLocalhost = Boolean( |
||||
|
window.location.hostname === 'localhost' |
||||
|
// [::1] is the IPv6 localhost address.
|
||||
|
|| window.location.hostname === '[::1]' |
||||
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
|
|| window.location.hostname.match( |
||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
export function register(config) { |
||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { |
||||
|
// The URL constructor is available in all browsers that support SW.
|
||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); |
||||
|
if (publicUrl.origin !== window.location.origin) { |
||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
window.addEventListener('load', () => { |
||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; |
||||
|
|
||||
|
if (isLocalhost) { |
||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
|
checkValidServiceWorker(swUrl, config); |
||||
|
|
||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||
|
// service worker/PWA documentation.
|
||||
|
navigator.serviceWorker.ready.then(() => { |
||||
|
console.log( |
||||
|
'This web app is being served cache-first by a service ' |
||||
|
+ 'worker. To learn more, visit http://bit.ly/CRA-PWA', |
||||
|
); |
||||
|
}); |
||||
|
} else { |
||||
|
// Is not localhost. Just register service worker
|
||||
|
registerValidSW(swUrl, config); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function registerValidSW(swUrl, config) { |
||||
|
navigator.serviceWorker.register(swUrl).then((registration) => { |
||||
|
registration.onupdatefound = () => { |
||||
|
const installingWorker = registration.installing; |
||||
|
if (installingWorker == null) { |
||||
|
return; |
||||
|
} |
||||
|
installingWorker.onstatechange = () => { |
||||
|
if (installingWorker.state === 'installed') { |
||||
|
if (navigator.serviceWorker.controller) { |
||||
|
// At this point, the updated precached content has been fetched,
|
||||
|
// but the previous service worker will still serve the older
|
||||
|
// content until all client tabs are closed.
|
||||
|
console.log( |
||||
|
'New content is available and will be used when all ' |
||||
|
+ 'tabs for this page are closed. See http://bit.ly/CRA-PWA.', |
||||
|
); |
||||
|
|
||||
|
// Execute callback
|
||||
|
if (config && config.onUpdate) { |
||||
|
config.onUpdate(registration); |
||||
|
} |
||||
|
} else { |
||||
|
// At this point, everything has been precached.
|
||||
|
// It's the perfect time to display a
|
||||
|
// "Content is cached for offline use." message.
|
||||
|
console.log('Content is cached for offline use.'); |
||||
|
|
||||
|
// Execute callback
|
||||
|
if (config && config.onSuccess) { |
||||
|
config.onSuccess(registration); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}; |
||||
|
}).catch((error) => { |
||||
|
console.error('Error during service worker registration:', error); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function checkValidServiceWorker(swUrl, config) { |
||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||
|
fetch(swUrl).then((response) => { |
||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
|
const contentType = response.headers.get('content-type'); |
||||
|
if ( |
||||
|
response.status === 404 |
||||
|
|| (contentType != null && contentType.indexOf('javascript') === -1) |
||||
|
) { |
||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||
|
navigator.serviceWorker.ready.then((registration) => { |
||||
|
registration.unregister().then(() => { |
||||
|
window.location.reload(); |
||||
|
}); |
||||
|
}); |
||||
|
} else { |
||||
|
// Service worker found. Proceed as normal.
|
||||
|
registerValidSW(swUrl, config); |
||||
|
} |
||||
|
}).catch(() => { |
||||
|
console.log( |
||||
|
'No internet connection found. App is running in offline mode.', |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function unregister() { |
||||
|
if ('serviceWorker' in navigator) { |
||||
|
navigator.serviceWorker.ready.then((registration) => { |
||||
|
registration.unregister(); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,81 @@ |
|||||
|
pragma solidity >=0.5.6 <0.6.0; |
||||
|
|
||||
|
contract Posting { |
||||
|
address forumContractAddress; |
||||
|
|
||||
|
function setForumContractAddress() public{ |
||||
|
require(forumContractAddress==address(0)); |
||||
|
forumContractAddress = msg.sender; |
||||
|
} |
||||
|
|
||||
|
struct Topic { |
||||
|
uint topicID; |
||||
|
address author; |
||||
|
uint timestamp; |
||||
|
uint[] postIDs; |
||||
|
} |
||||
|
|
||||
|
struct Post { |
||||
|
uint postID; |
||||
|
address author; |
||||
|
uint timestamp; |
||||
|
uint topicID; |
||||
|
} |
||||
|
|
||||
|
uint numTopics; // Total number of topics |
||||
|
uint numPosts; // Total number of posts |
||||
|
|
||||
|
mapping (uint => Topic) topics; |
||||
|
mapping (uint => Post) posts; |
||||
|
|
||||
|
function createTopic(address author) public returns (uint, uint) { |
||||
|
require(msg.sender==forumContractAddress); |
||||
|
//Creates topic |
||||
|
uint topicID = numTopics++; |
||||
|
topics[topicID] = Topic(topicID, author, block.timestamp, new uint[](0)); |
||||
|
|
||||
|
//Adds first post to topic |
||||
|
uint postID = numPosts++; |
||||
|
posts[postID] = Post(postID, author, block.timestamp, topicID); |
||||
|
topics[topicID].postIDs.push(postID); |
||||
|
|
||||
|
return (topicID, postID); |
||||
|
} |
||||
|
|
||||
|
function createPost(uint topicID, address author) public returns (uint) { |
||||
|
require(msg.sender==forumContractAddress); |
||||
|
require(topicID<numTopics); // Only allow posting to a topic that exists |
||||
|
uint postID = numPosts++; |
||||
|
posts[postID] = Post(postID, author, block.timestamp, topicID); |
||||
|
topics[topicID].postIDs.push(postID); |
||||
|
|
||||
|
return postID; |
||||
|
} |
||||
|
|
||||
|
function getNumberOfTopics() public view returns (uint) { |
||||
|
return numTopics; |
||||
|
} |
||||
|
|
||||
|
function getTopicInfo(uint topicID) public view returns (address, uint, uint[] memory) { |
||||
|
require(topicID<numTopics); |
||||
|
return (topics[topicID].author, |
||||
|
topics[topicID].timestamp, |
||||
|
topics[topicID].postIDs |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function getTopicPosts(uint topicID) public view returns (uint[] memory) { |
||||
|
require(topicID<numTopics); // Topic should exist |
||||
|
return topics[topicID].postIDs; |
||||
|
} |
||||
|
|
||||
|
function getPostInfo(uint postID) public view returns (address, uint, uint) { |
||||
|
require(postID<numPosts); |
||||
|
return ( |
||||
|
posts[postID].author, |
||||
|
posts[postID].timestamp, |
||||
|
posts[postID].topicID |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
@ -1,40 +1,27 @@ |
|||||
{ |
{ |
||||
"name": "apella", |
"name": "apella-box", |
||||
"version": "0.1.0", |
"version": "0.1.0", |
||||
|
"description": "", |
||||
"private": true, |
"private": true, |
||||
"repository": { |
"repository": { |
||||
"type": "git", |
"type": "git", |
||||
"url": "https://gitlab.com/Ezerous/Apella.git" |
"url": "https://gitlab.com/Ezerous/Apella.git" |
||||
}, |
}, |
||||
|
"main": "truffle-config.js", |
||||
|
"directories": { |
||||
|
"test": "test" |
||||
|
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"drizzle": "^1.1.5", |
"openzeppelin-solidity": "^2.2.0" |
||||
"drizzle-react": "^1.1.1", |
}, |
||||
"drizzle-react-components": "^1.1.0", |
"devDependencies": { |
||||
"eth-block-tracker-es5": "^2.3.2", |
"eslint": "5.12.0", |
||||
"ipfs": "^0.30.0", |
"eslint-config-airbnb": "17.1.0", |
||||
"orbit-db": "^0.19.9", |
"eslint-plugin-import": "2.16.0", |
||||
"orbit-db-keystore": "^0.1.0", |
"eslint-plugin-jsx-a11y": "6.2.1", |
||||
"prop-types": "^15.6.1", |
"eslint-plugin-react": "7.12.4" |
||||
"react": "^16.3.2", |
|
||||
"react-dom": "^16.3.2", |
|
||||
"react-markdown": "^3.3.2", |
|
||||
"react-redux": "^5.0.7", |
|
||||
"react-router": "^3.2.1", |
|
||||
"react-router-dom": "^4.2.2", |
|
||||
"react-router-redux": "^4.0.8", |
|
||||
"react-scripts": "^1.1.4", |
|
||||
"react-timeago": "^4.1.9", |
|
||||
"react-user-avatar": "^1.10.0", |
|
||||
"redux": "^3.7.2", |
|
||||
"redux-saga": "0.16.0", |
|
||||
"semantic-ui-react": "^0.81.1", |
|
||||
"uuid": "^3.2.1", |
|
||||
"web3": "^1.0.0-beta.34" |
|
||||
}, |
}, |
||||
"scripts": { |
"scripts": { |
||||
"start": "react-scripts start", |
"lint": "eslint app/src --format table" |
||||
"build": "react-scripts build", |
|
||||
"test": "react-scripts test --env=jsdom", |
|
||||
"eject": "react-scripts eject" |
|
||||
} |
} |
||||
} |
} |
||||
|
@ -1,44 +0,0 @@ |
|||||
<!DOCTYPE html> |
|
||||
<html lang="en"> |
|
||||
<head> |
|
||||
<meta charset="utf-8"> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
|
||||
<meta name="theme-color" content="#000000"> |
|
||||
<!-- |
|
||||
manifest.json provides metadata used when your web app is added to the |
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ |
|
||||
--> |
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"> |
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> |
|
||||
<!-- |
|
||||
Notice the use of %PUBLIC_URL% in the tags above. |
|
||||
It will be replaced with the URL of the `public` folder during the build. |
|
||||
Only files inside the `public` folder can be referenced from the HTML. |
|
||||
|
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
|
||||
work correctly both with client-side routing and a non-root public URL. |
|
||||
Learn how to configure a non-root public URL by running `npm run build`. |
|
||||
--> |
|
||||
<!-- Import Google Icon Font --> |
|
||||
<!-- <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> --> |
|
||||
|
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css"></link> |
|
||||
<title>Apella</title> |
|
||||
</head> |
|
||||
<body> |
|
||||
<noscript> |
|
||||
You need to enable JavaScript to run this app. |
|
||||
</noscript> |
|
||||
<div id="root"></div> |
|
||||
<!-- |
|
||||
This HTML file is a template. |
|
||||
If you open it directly in the browser, you will see an empty page. |
|
||||
|
|
||||
You can add webfonts, meta tags, or analytics to this file. |
|
||||
The build step will place the bundled scripts into the <body> tag. |
|
||||
|
|
||||
To begin the development, run `npm start` or `yarn start`. |
|
||||
To create a production bundle, use `npm run build` or `yarn build`. |
|
||||
--> |
|
||||
</body> |
|
||||
</html> |
|
@ -1,186 +0,0 @@ |
|||||
/* PAGE */ |
|
||||
|
|
||||
html, body { |
|
||||
margin: 0; |
|
||||
display: block; |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
strong { |
|
||||
font-weight: bold !important; |
|
||||
} |
|
||||
|
|
||||
#root { |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
.App { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
margin: 0px; |
|
||||
display: flex; |
|
||||
flex-flow: column nowrap; |
|
||||
align-items: flex-start; |
|
||||
} |
|
||||
|
|
||||
.page-container { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
margin: 71px 0px 0px; |
|
||||
} |
|
||||
|
|
||||
.left-side-panel { |
|
||||
margin-top: 71px; |
|
||||
position: fixed; |
|
||||
width: 20%; |
|
||||
height: calc(100% - 71px); |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
} |
|
||||
|
|
||||
.main-panel { |
|
||||
width: 60%; |
|
||||
height: 100%; |
|
||||
margin: 0px 20%; |
|
||||
} |
|
||||
|
|
||||
.right-side-panel { |
|
||||
margin-top: 71px; |
|
||||
position: fixed; |
|
||||
width: 20%; |
|
||||
height: calc(100% - 71px); |
|
||||
top: 0; |
|
||||
right: 0; |
|
||||
} |
|
||||
|
|
||||
.sidebar-message { |
|
||||
margin: 0px 5px 12px 12px; |
|
||||
padding: 0px; |
|
||||
} |
|
||||
|
|
||||
.view-container { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
margin: 0px auto; |
|
||||
} |
|
||||
|
|
||||
/* MISC */ |
|
||||
|
|
||||
.navBarText { |
|
||||
height: 61px; |
|
||||
width: 1192px; |
|
||||
position: absolute; |
|
||||
left: calc(50% - 596px); |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.navBarText span { |
|
||||
color: #00b5ad; |
|
||||
height: 61px; |
|
||||
line-height: 61px; |
|
||||
vertical-align: middle; |
|
||||
font-size: 1.5em; |
|
||||
} |
|
||||
|
|
||||
.form-textarea-required { |
|
||||
color: rgb(159, 58, 56) !important; |
|
||||
outline-color: rgb(159, 58, 56) !important; |
|
||||
border-color: rgb(224, 180, 180) !important; |
|
||||
background-color: rgb(255, 246, 246) !important; |
|
||||
} |
|
||||
|
|
||||
.card { |
|
||||
width: 100% !important; |
|
||||
} |
|
||||
|
|
||||
.bottom-overlay-pad { |
|
||||
background: rgba(255, 255, 255, 0.85); |
|
||||
z-index: 10; |
|
||||
position: fixed; |
|
||||
bottom: 0px; |
|
||||
height: 62px; |
|
||||
width: 60%; |
|
||||
margin: 0px; |
|
||||
padding: 0px; |
|
||||
} |
|
||||
|
|
||||
.action-button { |
|
||||
z-index: 11; |
|
||||
position: fixed; |
|
||||
bottom: 10px; |
|
||||
left: calc(50% - 24px); |
|
||||
} |
|
||||
|
|
||||
.grey-text { |
|
||||
color: grey; |
|
||||
} |
|
||||
|
|
||||
.inline { |
|
||||
display: inline-block; |
|
||||
} |
|
||||
|
|
||||
.no-margin { |
|
||||
margin: 0px; |
|
||||
} |
|
||||
|
|
||||
hr { |
|
||||
color: #0c1a2b; |
|
||||
margin: 0px; |
|
||||
} |
|
||||
|
|
||||
*:focus { |
|
||||
outline:none !important |
|
||||
} |
|
||||
|
|
||||
a { |
|
||||
color:inherit; |
|
||||
text-decoration: none; |
|
||||
} |
|
||||
|
|
||||
.center-in-parent { |
|
||||
width: 100%; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.vertical-center-in-parent { |
|
||||
vertical-align: middle; |
|
||||
height: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: center; |
|
||||
} |
|
||||
|
|
||||
.vertical-center-children { |
|
||||
height: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: center; |
|
||||
} |
|
||||
|
|
||||
#overlay { |
|
||||
position: fixed; |
|
||||
display: block; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
background-color: rgba(0,0,0,0.5); |
|
||||
z-index: 2; |
|
||||
} |
|
||||
|
|
||||
#overlay-content{ |
|
||||
position: absolute; |
|
||||
text-align: center; |
|
||||
top: 50%; |
|
||||
left: 50%; |
|
||||
color: white; |
|
||||
transform: translate(-50%,-50%); |
|
||||
-ms-transform: translate(-50%,-50%); |
|
||||
} |
|
||||
|
|
||||
.fill { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
} |
|
@ -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,4 +0,0 @@ |
|||||
body { |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
} |
|
@ -1,17 +0,0 @@ |
|||||
/* LOADING SCREEN */ |
|
||||
|
|
||||
.loading-screen { |
|
||||
height: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: center; |
|
||||
text-align: center; |
|
||||
opacity: 1; |
|
||||
visibility: visible; |
|
||||
transition: all .25s ease-in-out; |
|
||||
} |
|
||||
|
|
||||
.loading-screen.loaded { |
|
||||
opacity: 0; |
|
||||
visibility: hidden; |
|
||||
} |
|
@ -1,108 +0,0 @@ |
|||||
/* Progress Bar */ |
|
||||
|
|
||||
.progress-bar-container { |
|
||||
position: absolute; |
|
||||
top: 54px; |
|
||||
left: 0px; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.progress { |
|
||||
position: relative; |
|
||||
height: 4px; |
|
||||
display: block; |
|
||||
width: 100%; |
|
||||
background-color: #acece6; |
|
||||
border-radius: 2px; |
|
||||
background-clip: padding-box; |
|
||||
margin: 0.5rem 0 1rem 0; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
|
|
||||
.progress .indeterminate { |
|
||||
background-color: #00b5ad; |
|
||||
} |
|
||||
|
|
||||
.progress .indeterminate:before { |
|
||||
content: ''; |
|
||||
position: absolute; |
|
||||
background-color: inherit; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
bottom: 0; |
|
||||
will-change: left, right; |
|
||||
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; |
|
||||
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; |
|
||||
} |
|
||||
|
|
||||
.progress .indeterminate:after { |
|
||||
content: ''; |
|
||||
position: absolute; |
|
||||
background-color: inherit; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
bottom: 0; |
|
||||
will-change: left, right; |
|
||||
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; |
|
||||
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; |
|
||||
-webkit-animation-delay: 1.15s; |
|
||||
animation-delay: 1.15s; |
|
||||
} |
|
||||
|
|
||||
@-webkit-keyframes indeterminate { |
|
||||
0% { |
|
||||
left: -35%; |
|
||||
right: 100%; |
|
||||
} |
|
||||
60% { |
|
||||
left: 100%; |
|
||||
right: -90%; |
|
||||
} |
|
||||
100% { |
|
||||
left: 100%; |
|
||||
right: -90%; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@keyframes indeterminate { |
|
||||
0% { |
|
||||
left: -35%; |
|
||||
right: 100%; |
|
||||
} |
|
||||
60% { |
|
||||
left: 100%; |
|
||||
right: -90%; |
|
||||
} |
|
||||
100% { |
|
||||
left: 100%; |
|
||||
right: -90%; |
|
||||
} |
|
||||
} |
|
||||
@-webkit-keyframes indeterminate-short { |
|
||||
0% { |
|
||||
left: -200%; |
|
||||
right: 100%; |
|
||||
} |
|
||||
60% { |
|
||||
left: 107%; |
|
||||
right: -8%; |
|
||||
} |
|
||||
100% { |
|
||||
left: 107%; |
|
||||
right: -8%; |
|
||||
} |
|
||||
} |
|
||||
@keyframes indeterminate-short { |
|
||||
0% { |
|
||||
left: -200%; |
|
||||
right: 100%; |
|
||||
} |
|
||||
60% { |
|
||||
left: 107%; |
|
||||
right: -8%; |
|
||||
} |
|
||||
100% { |
|
||||
left: 107%; |
|
||||
right: -8%; |
|
||||
} |
|
||||
} |
|
@ -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,14 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import { Button, Icon } from 'semantic-ui-react' |
|
||||
|
|
||||
const FloatingButton = (props) => { |
|
||||
return ( |
|
||||
<div className="action-button" onClick={props.onClick}> |
|
||||
<Button icon color='teal' size='large'> |
|
||||
<Icon name='add'/> |
|
||||
</Button> |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default FloatingButton; |
|
@ -1,16 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
const LoadingSpinner = (props) => { |
|
||||
return( |
|
||||
<div className="vertical-center-children"> |
|
||||
<div className={"center-in-parent " + (props.className ? props.className : "")} |
|
||||
style={props.style ? props.style : []}> |
|
||||
<p> |
|
||||
<i className="fas fa-spinner fa-3x fa-spin"></i> |
|
||||
</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export default LoadingSpinner; |
|
@ -1,63 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import { Image, Menu } from 'semantic-ui-react' |
|
||||
|
|
||||
class NavBar extends Component { |
|
||||
constructor(props){ |
|
||||
super(props); |
|
||||
|
|
||||
this.handleItemClick = this.handleItemClick.bind(this); |
|
||||
|
|
||||
this.navRef = React.createRef(); |
|
||||
} |
|
||||
|
|
||||
handleItemClick(to) { |
|
||||
this.context.router.push(to); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return ( |
|
||||
<Menu fixed='top' inverted> |
|
||||
<Menu.Item header onClick={() => {this.handleItemClick("/")}}> |
|
||||
<Image |
|
||||
size='mini' |
|
||||
src={require('../resources/logo.png')} |
|
||||
style={{ marginRight: '1.5em' }} |
|
||||
/> |
|
||||
Apella |
|
||||
</Menu.Item> |
|
||||
<Menu.Item onClick={() => {this.handleItemClick("/")}}> |
|
||||
Home |
|
||||
</Menu.Item> |
|
||||
{this.props.hasSignedUp |
|
||||
? <Menu.Item onClick={() => {this.handleItemClick("/profile")}}> |
|
||||
Profile |
|
||||
</Menu.Item> |
|
||||
:<Menu.Menu position='right' style={{backgroundColor: '#00b5ad'}}> |
|
||||
<Menu.Item onClick={() => {this.handleItemClick("/signup")}}> |
|
||||
Sign Up |
|
||||
</Menu.Item> |
|
||||
</Menu.Menu> |
|
||||
} |
|
||||
<div className="navBarText"> |
|
||||
{this.props.navBarTitle !== '' && <span>{this.props.navBarTitle}</span>} |
|
||||
</div> |
|
||||
</Menu> |
|
||||
); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
NavBar.contextTypes = { |
|
||||
router: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
hasSignedUp: state.user.hasSignedUp, |
|
||||
navBarTitle: state.interface.navBarTitle |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default drizzleConnect(NavBar, mapStateToProps); |
|
@ -1,175 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
|
|
||||
import { Grid, Form, TextArea, Button, Icon, Divider } from 'semantic-ui-react' |
|
||||
|
|
||||
import TimeAgo from 'react-timeago'; |
|
||||
import UserAvatar from 'react-user-avatar'; |
|
||||
import ReactMarkdown from 'react-markdown'; |
|
||||
|
|
||||
import { createPost } from '../redux/actions/transactionsMonitorActions'; |
|
||||
|
|
||||
class NewPost extends Component { |
|
||||
constructor(props, context) { |
|
||||
super(props); |
|
||||
|
|
||||
this.handleInputChange = this.handleInputChange.bind(this); |
|
||||
this.handlePreviewToggle = this.handlePreviewToggle.bind(this); |
|
||||
this.validateAndPost = this.validateAndPost.bind(this); |
|
||||
|
|
||||
this.newPostOuterRef = React.createRef(); |
|
||||
|
|
||||
this.state = { |
|
||||
postSubjectInput: this.props.subject ? this.props.subject : "", |
|
||||
postContentInput: '', |
|
||||
postSubjectInputEmptySubmit: false, |
|
||||
postContentInputEmptySubmit: false, |
|
||||
previewEnabled: false, |
|
||||
previewDate: "" |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
async validateAndPost() { |
|
||||
if (this.state.postSubjectInput === '' || this.state.postContentInput === ''){ |
|
||||
this.setState({ |
|
||||
postSubjectInputEmptySubmit: this.state.postSubjectInput === '', |
|
||||
postContentInputEmptySubmit: this.state.postContentInput === '' |
|
||||
}); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
this.props.store.dispatch( |
|
||||
createPost(this.props.topicID, |
|
||||
{ |
|
||||
postSubject: this.state.postSubjectInput, |
|
||||
postMessage: this.state.postContentInput |
|
||||
} |
|
||||
) |
|
||||
); |
|
||||
this.props.onPostCreated(); |
|
||||
} |
|
||||
|
|
||||
handleInputChange(event) { |
|
||||
this.setState({[event.target.name]: event.target.value}); |
|
||||
} |
|
||||
|
|
||||
handlePreviewToggle() { |
|
||||
this.setState((prevState, props) => ({ |
|
||||
previewEnabled: !prevState.previewEnabled, |
|
||||
previewDate: this.getDate() |
|
||||
})); |
|
||||
} |
|
||||
|
|
||||
getDate() { |
|
||||
const currentdate = new Date(); |
|
||||
return ((currentdate.getMonth() + 1) + " " |
|
||||
+ currentdate.getDate() + ", " |
|
||||
+ currentdate.getFullYear() + ", " |
|
||||
+ currentdate.getHours() + ":" |
|
||||
+ currentdate.getMinutes() + ":" |
|
||||
+ currentdate.getSeconds()); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return ( |
|
||||
<div className="post" ref={this.newPostOuterRef}> |
|
||||
<Divider horizontal> |
|
||||
<span className="grey-text">#{this.props.postIndex}</span> |
|
||||
</Divider> |
|
||||
<Grid> |
|
||||
<Grid.Row columns={16} stretched> |
|
||||
<Grid.Column width={1} className="user-avatar"> |
|
||||
<UserAvatar |
|
||||
size="52" |
|
||||
className="inline user-avatar" |
|
||||
src={this.props.avatarUrl} |
|
||||
name={this.props.user.username} |
|
||||
/> |
|
||||
</Grid.Column> |
|
||||
<Grid.Column width={15}> |
|
||||
<div className=""> |
|
||||
<div className="stretch-space-between"> |
|
||||
<span><strong>{this.props.user.username}</strong></span> |
|
||||
<span className="grey-text"> |
|
||||
{this.state.previewEnabled && |
|
||||
<TimeAgo date={this.state.previewDate}/> |
|
||||
} |
|
||||
</span> |
|
||||
</div> |
|
||||
<div className="stretch-space-between"> |
|
||||
<span><strong> |
|
||||
{this.state.previewEnabled && |
|
||||
("Subject: " + this.state.postSubjectInput) |
|
||||
} |
|
||||
</strong></span> |
|
||||
</div> |
|
||||
<div className="post-content"> |
|
||||
<div style={{display: this.state.previewEnabled ? "block" : "none"}}> |
|
||||
<ReactMarkdown source={this.state.postContentInput} |
|
||||
className="markdown-preview" /> |
|
||||
</div> |
|
||||
<Form className="topic-form"> |
|
||||
<Form.Input key={"postSubjectInput"} |
|
||||
style={{display: this.state.previewEnabled ? "none" : ""}} |
|
||||
name={"postSubjectInput"} |
|
||||
error={this.state.postSubjectInputEmptySubmit} |
|
||||
type="text" |
|
||||
value={this.state.postSubjectInput} |
|
||||
placeholder="Subject" |
|
||||
id="postSubjectInput" |
|
||||
onChange={this.handleInputChange} /> |
|
||||
<TextArea key={"postContentInput"} |
|
||||
style={{display: this.state.previewEnabled ? "none" : ""}} |
|
||||
name={"postContentInput"} |
|
||||
className={this.state.postContentInputEmptySubmit ? "form-textarea-required" : ""} |
|
||||
value={this.state.postContentInput} |
|
||||
placeholder="Post" |
|
||||
id="postContentInput" |
|
||||
onChange={this.handleInputChange} |
|
||||
rows={4} autoHeight /> |
|
||||
<br/><br/> |
|
||||
<Button.Group> |
|
||||
<Button key="submit" |
|
||||
type="button" |
|
||||
onClick={this.validateAndPost} |
|
||||
color='teal' |
|
||||
animated> |
|
||||
<Button.Content visible>Post</Button.Content> |
|
||||
<Button.Content hidden> |
|
||||
<Icon name='reply' /> |
|
||||
</Button.Content> |
|
||||
</Button> |
|
||||
<Button type="button" |
|
||||
onClick={this.handlePreviewToggle} |
|
||||
color='yellow'> |
|
||||
{this.state.previewEnabled ? "Edit" : "Preview"} |
|
||||
</Button> |
|
||||
<Button type="button" |
|
||||
onClick={this.props.onCancelClick} |
|
||||
color='red'> |
|
||||
Cancel |
|
||||
</Button> |
|
||||
</Button.Group> |
|
||||
</Form> |
|
||||
</div> |
|
||||
</div> |
|
||||
</Grid.Column> |
|
||||
</Grid.Row> |
|
||||
</Grid> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
componentDidMount(){ |
|
||||
this.newPostOuterRef.current.scrollIntoView(true); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
orbitDB: state.orbitDB, |
|
||||
user: state.user |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default drizzleConnect(NewPost, mapStateToProps); |
|
@ -1,65 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
|
|
||||
import { Grid, Divider } from 'semantic-ui-react' |
|
||||
|
|
||||
import TimeAgo from 'react-timeago'; |
|
||||
import UserAvatar from 'react-user-avatar'; |
|
||||
import ReactMarkdown from 'react-markdown'; |
|
||||
|
|
||||
class Post extends Component { |
|
||||
constructor(props, context) { |
|
||||
super(props); |
|
||||
} |
|
||||
|
|
||||
render(){ |
|
||||
return ( |
|
||||
<div className="post"> |
|
||||
<Divider horizontal> |
|
||||
<span className="grey-text">#0</span> |
|
||||
</Divider> |
|
||||
<Grid> |
|
||||
<Grid.Row columns={16} stretched> |
|
||||
<Grid.Column width={1} className="user-avatar"> |
|
||||
<UserAvatar |
|
||||
size="52" |
|
||||
className="inline" |
|
||||
src={this.props.user.avatarUrl} |
|
||||
name={this.props.user.username}/> |
|
||||
</Grid.Column> |
|
||||
<Grid.Column width={15}> |
|
||||
<div className=""> |
|
||||
<div className="stretch-space-between"> |
|
||||
<span> |
|
||||
<strong> |
|
||||
{this.props.user.username} |
|
||||
</strong> |
|
||||
</span> |
|
||||
<span className="grey-text"> |
|
||||
<TimeAgo date={this.props.date}/> |
|
||||
</span> |
|
||||
</div> |
|
||||
<div className="stretch-space-between"> |
|
||||
<span><strong> |
|
||||
Subject: {this.props.subject} |
|
||||
</strong></span> |
|
||||
</div> |
|
||||
<div className="post-content"> |
|
||||
<ReactMarkdown source={this.props.content} /> |
|
||||
</div> |
|
||||
</div> |
|
||||
</Grid.Column> |
|
||||
</Grid.Row> |
|
||||
</Grid> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default drizzleConnect(Post, mapStateToProps); |
|
@ -1,11 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
const NotFoundView = (props) => { |
|
||||
return ( |
|
||||
<div style={{textAlign: "center"}}> |
|
||||
<img src={require('../resources/PageNotFound.jpg')} alt="Page not found!"/> |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default NotFoundView; |
|
@ -1,173 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { Link, withRouter } from 'react-router'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import { Transition } from 'semantic-ui-react' |
|
||||
import { Grid, Divider, Button, Icon, Label } from 'semantic-ui-react' |
|
||||
|
|
||||
import TimeAgo from 'react-timeago'; |
|
||||
import epochTimeConverter from '../helpers/EpochTimeConverter'; |
|
||||
import UserAvatar from 'react-user-avatar'; |
|
||||
import ReactMarkdown from 'react-markdown'; |
|
||||
|
|
||||
class Post extends Component { |
|
||||
constructor(props, context) { |
|
||||
super(props); |
|
||||
|
|
||||
this.fetchPost = this.fetchPost.bind(this); |
|
||||
if (props.getFocus){ |
|
||||
this.postRef = React.createRef(); |
|
||||
} |
|
||||
|
|
||||
this.orbitPostData = { |
|
||||
content: "", |
|
||||
subject: "" |
|
||||
}; |
|
||||
this.orbitPostDataFetchStatus = "pending"; |
|
||||
this.state = { |
|
||||
animateOnToggle: true |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async fetchPost(postID) { |
|
||||
this.orbitPostDataFetchStatus = "fetching"; |
|
||||
|
|
||||
if (this.props.blockchainData[0].returnData[1] === this.props.user.address) { |
|
||||
this.orbitPostData = this.props.orbitDB.postsDB.get(postID); |
|
||||
} else { |
|
||||
const fullAddress = "/orbitdb/" + this.props.blockchainData[0].returnData[0] + "/posts"; |
|
||||
const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress); |
|
||||
await store.load(); |
|
||||
|
|
||||
let localOrbitData = store.get(postID); |
|
||||
if (localOrbitData) { |
|
||||
this.orbitPostData = localOrbitData; |
|
||||
} else { |
|
||||
// Wait until we have received something from the network
|
|
||||
store.events.on('replicated', () => { |
|
||||
this.orbitPostData = store.get(postID); |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
this.orbitPostDataFetchStatus = "fetched"; |
|
||||
this.readyForAnimation = true; |
|
||||
} |
|
||||
|
|
||||
render(){ |
|
||||
let avatarView = (this.props.blockchainData[0].returnData |
|
||||
? <UserAvatar |
|
||||
size="52" |
|
||||
className="inline" |
|
||||
src={this.props.avatarUrl} |
|
||||
name={this.props.blockchainData[0].returnData[2]}/> |
|
||||
: <div></div> |
|
||||
); |
|
||||
|
|
||||
return ( |
|
||||
<Transition animation='tada' duration={500} visible={this.state.animateOnToggle}> |
|
||||
<div className="post" ref={this.postRef ? this.postRef : null}> |
|
||||
<Divider horizontal> |
|
||||
<span className="grey-text">#{this.props.postIndex}</span> |
|
||||
</Divider> |
|
||||
<Grid> |
|
||||
<Grid.Row columns={16} stretched> |
|
||||
<Grid.Column width={1} className="user-avatar"> |
|
||||
{this.props.blockchainData[0].returnData !== null |
|
||||
?<Link to={"/profile/" + this.props.blockchainData[0].returnData[1] |
|
||||
+ "/" + this.props.blockchainData[0].returnData[2]} |
|
||||
onClick={(event) => {event.stopPropagation()}}> |
|
||||
{avatarView} |
|
||||
</Link> |
|
||||
:avatarView |
|
||||
} |
|
||||
</Grid.Column> |
|
||||
<Grid.Column width={15}> |
|
||||
<div className=""> |
|
||||
<div className="stretch-space-between"> |
|
||||
<span className={this.props.blockchainData[0].returnData !== null ? "" : "grey-text"}> |
|
||||
<strong> |
|
||||
{this.props.blockchainData[0].returnData !== null |
|
||||
?this.props.blockchainData[0].returnData[2] |
|
||||
:"Username" |
|
||||
} |
|
||||
</strong> |
|
||||
</span> |
|
||||
<span className="grey-text"> |
|
||||
{this.props.blockchainData[0].returnData !== null && |
|
||||
<TimeAgo date={epochTimeConverter(this.props.blockchainData[0].returnData[3])}/> |
|
||||
} |
|
||||
</span> |
|
||||
</div> |
|
||||
<div className="stretch-space-between"> |
|
||||
<span className={this.orbitPostData.subject ? "" : "grey-text"}> |
|
||||
<strong> |
|
||||
Subject: {this.orbitPostData.subject} |
|
||||
</strong> |
|
||||
</span> |
|
||||
</div> |
|
||||
<div className="post-content"> |
|
||||
{this.orbitPostData.content |
|
||||
? <ReactMarkdown source={this.orbitPostData.content} /> |
|
||||
: <p className="grey-text">Post content...</p> |
|
||||
} |
|
||||
</div> |
|
||||
</div> |
|
||||
</Grid.Column> |
|
||||
</Grid.Row> |
|
||||
<Grid.Row> |
|
||||
<Grid.Column floated="right" textAlign="right"> |
|
||||
<Button icon size='mini' style={{marginRight: "0px"}}> |
|
||||
<Icon name='chevron up' /> |
|
||||
</Button> |
|
||||
<Label color="teal">8000</Label> |
|
||||
<Button icon size='mini'> |
|
||||
<Icon name='chevron down' /> |
|
||||
</Button> |
|
||||
<Button icon size='mini' |
|
||||
onClick={this.props.blockchainData[0].returnData |
|
||||
? () => { this.context.router.push("/topic/" |
|
||||
+ this.props.blockchainData[0].returnData[4] + "/" |
|
||||
+ this.props.postID)} |
|
||||
: () => {}}> |
|
||||
<Icon name='linkify' /> |
|
||||
</Button> |
|
||||
</Grid.Column> |
|
||||
</Grid.Row> |
|
||||
</Grid> |
|
||||
</div> |
|
||||
</Transition> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate() { |
|
||||
if (this.props.blockchainData[0].status === "success" |
|
||||
&& this.orbitPostDataFetchStatus === "pending") { |
|
||||
this.fetchPost(this.props.postID); |
|
||||
} |
|
||||
if (this.readyForAnimation){ |
|
||||
if (this.postRef){ |
|
||||
setTimeout(() => { |
|
||||
this.postRef.current.scrollIntoView({ block: 'start', behavior: 'smooth' }); |
|
||||
setTimeout(() => { |
|
||||
this.setState({ animateOnToggle: false }); |
|
||||
}, 300); |
|
||||
}, 100); |
|
||||
this.readyForAnimation = false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
Post.contextTypes = { |
|
||||
router: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user, |
|
||||
orbitDB: state.orbitDB |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default drizzleConnect(withRouter(Post), mapStateToProps); |
|
@ -1,36 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import WithBlockchainData from './WithBlockchainData'; |
|
||||
|
|
||||
import Post from './Post'; |
|
||||
|
|
||||
const PostList = (props) => { |
|
||||
const posts = props.postIDs.map((postID, index) => { |
|
||||
return ( |
|
||||
<WithBlockchainData |
|
||||
component={Post} |
|
||||
callsInfo={[{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getPost', |
|
||||
params: [postID] |
|
||||
}]} |
|
||||
avatarUrl={""} |
|
||||
postIndex={index} |
|
||||
postID={postID} |
|
||||
getFocus={props.focusOnPost === postID ? true : false} |
|
||||
key={postID} |
|
||||
/> |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
{props.recentToTheTop |
|
||||
?posts.slice(0).reverse() |
|
||||
:posts |
|
||||
} |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default PostList; |
|
@ -1,59 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import UserAvatar from 'react-user-avatar'; |
|
||||
|
|
||||
import epochTimeConverter from '../helpers/EpochTimeConverter'; |
|
||||
|
|
||||
import UsernameFormContainer from '../containers/UsernameFormContainer'; |
|
||||
|
|
||||
const ProfileInformation = (props) => { |
|
||||
let transaction = props.blockchainData |
|
||||
.find(transaction => transaction.callInfo.method === "getUserDateOfRegister"); |
|
||||
let dateOfRegister = transaction ? transaction.returnData : ""; |
|
||||
|
|
||||
transaction = props.blockchainData |
|
||||
.find(transaction => transaction.callInfo.method === "getOrbitDBId") |
|
||||
let orbitDBId = transaction ? transaction.returnData : ""; |
|
||||
|
|
||||
return ( |
|
||||
<div className="user-info"> |
|
||||
{props.avatarUrl && <UserAvatar |
|
||||
size="40" |
|
||||
className="inline user-avatar" |
|
||||
src={props.avatarUrl} |
|
||||
name={props.username}/>} |
|
||||
<table className="highlight centered responsive-table"> |
|
||||
<tbody> |
|
||||
<tr> |
|
||||
<td><strong>Username:</strong></td> |
|
||||
<td>{props.username}</td> |
|
||||
</tr> |
|
||||
<tr> |
|
||||
<td><strong>Account address:</strong></td> |
|
||||
<td>{props.address}</td> |
|
||||
</tr> |
|
||||
<tr> |
|
||||
<td><strong>OrbitDB:</strong></td> |
|
||||
<td>{orbitDBId}</td> |
|
||||
</tr> |
|
||||
<tr> |
|
||||
<td><strong>Number of topics created:</strong></td> |
|
||||
<td>{props.numberOfTopics}</td> |
|
||||
</tr> |
|
||||
<tr> |
|
||||
<td><strong>Number of posts:</strong></td> |
|
||||
<td>{props.numberOfPosts}</td> |
|
||||
</tr> |
|
||||
{dateOfRegister && |
|
||||
<tr> |
|
||||
<td><strong>Member since:</strong></td> |
|
||||
<td>{epochTimeConverter(dateOfRegister)}</td> |
|
||||
</tr> |
|
||||
} |
|
||||
</tbody> |
|
||||
</table> |
|
||||
{props.self && <UsernameFormContainer/>} |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default ProfileInformation; |
|
@ -1,100 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import { Card } from 'semantic-ui-react' |
|
||||
|
|
||||
import TimeAgo from 'react-timeago'; |
|
||||
import epochTimeConverter from '../helpers/EpochTimeConverter' |
|
||||
|
|
||||
class Topic extends Component { |
|
||||
constructor(props){ |
|
||||
super(props); |
|
||||
|
|
||||
this.fetchSubject = this.fetchSubject.bind(this); |
|
||||
|
|
||||
this.topicSubject = null; |
|
||||
this.topicSubjectFetchStatus = "pending"; |
|
||||
} |
|
||||
|
|
||||
async fetchSubject(topicID) { |
|
||||
this.topicSubjectFetchStatus = "fetching"; |
|
||||
|
|
||||
if (this.props.blockchainData[0].returnData[1] === this.props.user.address) { |
|
||||
let orbitData = this.props.orbitDB.topicsDB.get(topicID); |
|
||||
this.topicSubject = orbitData['subject']; |
|
||||
this.topicSubjectFetchStatus = "fetched"; |
|
||||
} else { |
|
||||
const fullAddress = "/orbitdb/" + this.props.blockchainData[0].returnData[0] + "/topics"; |
|
||||
const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress); |
|
||||
await store.load(); |
|
||||
|
|
||||
let localOrbitData = store.get(topicID); |
|
||||
if (localOrbitData) { |
|
||||
this.topicSubject = localOrbitData['subject']; |
|
||||
} else { |
|
||||
// Wait until we have received something from the network
|
|
||||
store.events.on('replicated', () => { |
|
||||
this.topicSubject = store.get(topicID)['subject']; |
|
||||
}) |
|
||||
} |
|
||||
this.topicSubjectFetchStatus = "fetched"; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render(){ |
|
||||
return ( |
|
||||
<Card link className="card" |
|
||||
onClick={() => {this.context.router.push("/topic/" + this.props.topicID)}}> |
|
||||
<Card.Content> |
|
||||
<div className={"topic-subject" + (this.topicSubject ? "" : " grey-text")}> |
|
||||
<p><strong> |
|
||||
{this.topicSubject !== null ? this.topicSubject : "Subject"} |
|
||||
</strong></p> |
|
||||
</div> |
|
||||
<hr/> |
|
||||
<div className="topic-meta"> |
|
||||
<p className={"no-margin" + |
|
||||
(this.props.blockchainData[0].returnData !== null ? "" : " grey-text")}> |
|
||||
{this.props.blockchainData[0].returnData !== null |
|
||||
?this.props.blockchainData[0].returnData[2] |
|
||||
:"Username" |
|
||||
} |
|
||||
</p> |
|
||||
<p className={"no-margin" + |
|
||||
(this.props.blockchainData[0].returnData !== null ? "" : " grey-text")}> |
|
||||
{"Number of replies: " + (this.props.blockchainData[0].returnData !== null |
|
||||
?this.props.blockchainData[0].returnData[4].length |
|
||||
:"") |
|
||||
} |
|
||||
</p> |
|
||||
<p className="topic-date grey-text"> |
|
||||
{this.props.blockchainData[0].returnData !== null && |
|
||||
<TimeAgo date={epochTimeConverter(this.props.blockchainData[0].returnData[3])}/> |
|
||||
} |
|
||||
</p> |
|
||||
</div> |
|
||||
</Card.Content> |
|
||||
</Card> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate(){ |
|
||||
if (this.props.blockchainData[0].returnData !== null && this.topicSubjectFetchStatus === "pending") { |
|
||||
this.fetchSubject(this.props.topicID); |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
Topic.contextTypes = { |
|
||||
router: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user, |
|
||||
orbitDB: state.orbitDB |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default drizzleConnect(Topic, mapStateToProps); |
|
@ -1,30 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import WithBlockchainData from './WithBlockchainData'; |
|
||||
|
|
||||
import Topic from './Topic'; |
|
||||
|
|
||||
const TopicList = (props) => { |
|
||||
const topics = props.topicIDs.map((topicID) => { |
|
||||
return ( |
|
||||
<WithBlockchainData |
|
||||
component={Topic} |
|
||||
callsInfo={[{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getTopic', |
|
||||
params: [topicID] |
|
||||
}]} |
|
||||
topicID={topicID} |
|
||||
key={topicID} |
|
||||
/> |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
return ( |
|
||||
<div className="topics-list"> |
|
||||
{topics.slice(0).reverse()} |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default TopicList; |
|
@ -1,80 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
class WithBlockchainData extends Component { |
|
||||
constructor(props, context) { |
|
||||
super(props); |
|
||||
|
|
||||
{ |
|
||||
let {component, callsInfo, ...rest } = this.props; |
|
||||
this.component = component; |
|
||||
this.callsInfo = callsInfo; |
|
||||
this.forwardedProps = rest; |
|
||||
} |
|
||||
|
|
||||
this.drizzle = context.drizzle; |
|
||||
this.dataKeys = []; |
|
||||
let blockchainData = this.callsInfo.map((call) => { |
|
||||
return ({ |
|
||||
callInfo: call, |
|
||||
status: "initialized", |
|
||||
returnData: null |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
//Initial call
|
|
||||
for (var i = 0; i < this.callsInfo.length; ++i){ |
|
||||
this.dataKeys[i] = this.drizzle |
|
||||
.contracts[this.callsInfo[i].contract] |
|
||||
.methods[this.callsInfo[i].method] |
|
||||
.cacheCall(...(this.callsInfo[i].params)); |
|
||||
blockchainData[i].status = "pending"; |
|
||||
} |
|
||||
|
|
||||
this.state = { |
|
||||
callState: new Array(this.callsInfo.length).fill("pending"), |
|
||||
blockchainData: blockchainData |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
let {component, callsInfo, ...rest } = this.props; //Update rest arguments
|
|
||||
return ( |
|
||||
<this.component blockchainData={this.state.blockchainData} {...rest}/> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
componentWillUpdate(){ |
|
||||
let currentDrizzleState = this.drizzle.store.getState(); |
|
||||
for (var i = 0; i < this.callsInfo.length; ++i){ |
|
||||
let dataFetched = (currentDrizzleState |
|
||||
.contracts[this.callsInfo[i].contract][this.callsInfo[i].method][this.dataKeys[i]]); |
|
||||
if (dataFetched && dataFetched.value !== this.state.blockchainData[i].returnData){ |
|
||||
/* There are new data in the blockchain*/ |
|
||||
|
|
||||
//Immutable update
|
|
||||
let newBlockchainData = this.state.blockchainData.map((callData, index) => { |
|
||||
if (index !== i) return callData; |
|
||||
return { |
|
||||
...callData, |
|
||||
returnData: dataFetched.value, |
|
||||
status: "success" |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
let newStates = this.state.callState.slice(); |
|
||||
newStates[i] = "success" |
|
||||
this.setState({ |
|
||||
callState: newStates, |
|
||||
blockchainData: newBlockchainData |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
WithBlockchainData.contextTypes = { |
|
||||
drizzle: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
export default WithBlockchainData; |
|
@ -1,117 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import { Header } from 'semantic-ui-react'; |
|
||||
|
|
||||
import WithBlockchainData from '../components/WithBlockchainData'; |
|
||||
import TopicList from '../components/TopicList'; |
|
||||
import FloatingButton from '../components/FloatingButton'; |
|
||||
|
|
||||
import { showProgressBar, hideProgressBar } from '../redux/actions/userInterfaceActions'; |
|
||||
|
|
||||
class Board extends Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
|
|
||||
this.props.store.dispatch(showProgressBar()); |
|
||||
|
|
||||
this.handleCreateTopicClick = this.handleCreateTopicClick.bind(this); |
|
||||
|
|
||||
this.state = { |
|
||||
pageLoaded: false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
handleCreateTopicClick() { |
|
||||
this.context.router.push("/startTopic"); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
var boardContents; |
|
||||
if (this.props.blockchainData[0].returnData !== '0'){ |
|
||||
this.topicIDs = []; |
|
||||
for (var i = 0; i < this.props.blockchainData[0].returnData; i++) { |
|
||||
this.topicIDs.push(i); |
|
||||
} |
|
||||
boardContents = ([ |
|
||||
<TopicList topicIDs={this.topicIDs} key="topicList"/>, |
|
||||
<div className="bottom-overlay-pad" key="pad"></div>, |
|
||||
this.props.user.hasSignedUp && |
|
||||
<FloatingButton onClick={this.handleCreateTopicClick} |
|
||||
key="createTopicButton"/> |
|
||||
]); |
|
||||
} else { |
|
||||
if (!this.props.user.hasSignedUp){ |
|
||||
boardContents = ( |
|
||||
<div className="vertical-center-in-parent"> |
|
||||
<Header color='teal' textAlign='center' as='h2'> |
|
||||
There are no topics yet! |
|
||||
</Header> |
|
||||
<Header color='teal' textAlign='center' as='h4'> |
|
||||
Sign up to be the first to post. |
|
||||
</Header> |
|
||||
</div> |
|
||||
); |
|
||||
} else { |
|
||||
boardContents = ( |
|
||||
<div className="vertical-center-in-parent"> |
|
||||
<Header color='teal' textAlign='center' as='h2'> |
|
||||
There are no topics yet! |
|
||||
</Header> |
|
||||
<Header color='teal' textAlign='center' as='h4'> |
|
||||
Click the add button at the bottom of the page to be the first to post. |
|
||||
</Header> |
|
||||
<FloatingButton onClick={this.handleCreateTopicClick} |
|
||||
key="createTopicButton"/> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div className="fill"> |
|
||||
{boardContents} |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate(){ |
|
||||
if (!this.state.pageLoaded && this.props.blockchainData[0].returnData){ |
|
||||
this.props.store.dispatch(hideProgressBar()); |
|
||||
this.setState({ pageLoaded: true }); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Board.contextTypes = { |
|
||||
drizzle: PropTypes.object, |
|
||||
router: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
class BoardContainer extends Component { |
|
||||
constructor(props){ |
|
||||
super(props); |
|
||||
|
|
||||
this.board = <WithBlockchainData |
|
||||
component={drizzleConnect(Board, mapStateToProps)} |
|
||||
callsInfo={[{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getNumberOfTopics', |
|
||||
params: [] |
|
||||
}]} |
|
||||
/>; |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return(this.board); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default BoardContainer; |
|
@ -1,35 +0,0 @@ |
|||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import React, { Component } from 'react'; |
|
||||
|
|
||||
import BoardContainer from './BoardContainer'; |
|
||||
|
|
||||
class Home extends Component { |
|
||||
render() { |
|
||||
//We can add a modal to tell the user to sign up
|
|
||||
|
|
||||
/*var modal = this.props.user.hasSignedUp && ( |
|
||||
<Modal dimmer='blurring' open={this.state.open}> |
|
||||
<Modal.Header>Select a Photo</Modal.Header> |
|
||||
<Modal.Content image> |
|
||||
<Image wrapped size='medium' src='/assets/images/avatar/large/rachel.png' /> |
|
||||
<Modal.Description> |
|
||||
<Header>Default Profile Image</Header> |
|
||||
<p>We've found the following gravatar image associated with your e-mail address.</p> |
|
||||
<p>Is it okay to use this photo?</p> |
|
||||
</Modal.Description> |
|
||||
</Modal.Content> |
|
||||
</Modal>);*/ |
|
||||
|
|
||||
return (<BoardContainer/>); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const HomeContainer = drizzleConnect(Home, mapStateToProps); |
|
||||
|
|
||||
export default HomeContainer; |
|
@ -1,88 +0,0 @@ |
|||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import React, { Children, Component } from 'react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import ipfs_logo from './../resources/ipfs_logo.png'; |
|
||||
|
|
||||
class LoadingContainer extends Component { |
|
||||
render() { |
|
||||
if (this.props.web3.status === 'failed') |
|
||||
{ |
|
||||
if (this.props.errorComp) { |
|
||||
return this.props.errorComp |
|
||||
} |
|
||||
|
|
||||
return( |
|
||||
<main className="loading-screen"> |
|
||||
<div> |
|
||||
<div> |
|
||||
<h1><span role="img" aria-label="Warning Sign">⚠</span></h1> |
|
||||
<p>This browser has no connection to the Ethereum network. Please use the Chrome/FireFox extension MetaMask, or dedicated Ethereum browsers Mist or Parity.</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</main> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
if (this.props.web3.status === 'initialized' && Object.keys(this.props.accounts).length === 0) |
|
||||
{ |
|
||||
return( |
|
||||
<main className="loading-screen"> |
|
||||
<div> |
|
||||
<div> |
|
||||
<h1><span role="img" aria-label="Fox Face">🦊</span></h1> |
|
||||
<p><strong>We can't find any Ethereum accounts!</strong> Please check and make sure Metamask or you browser are pointed at the correct network and your account is unlocked.</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</main> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
if (!this.props.orbitDB.ipfsInitialized) |
|
||||
{ |
|
||||
return( |
|
||||
<main className="loading-screen"> |
|
||||
<div> |
|
||||
<div> |
|
||||
<img src={ipfs_logo} alt="ipfs_logo" height="50"/> |
|
||||
<p><strong>Initializing IPFS...</strong></p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</main> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
if (this.props.drizzleStatus.initialized) |
|
||||
return Children.only(this.props.children); |
|
||||
|
|
||||
if (this.props.loadingComp) |
|
||||
return this.props.loadingComp; |
|
||||
|
|
||||
|
|
||||
return( |
|
||||
<main className="container loading-screen"> |
|
||||
<div> |
|
||||
<div> |
|
||||
<h1><span role="img" aria-label="Gear">⚙</span></h1> |
|
||||
<p>Loading dapp...</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</main> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
LoadingContainer.contextTypes = { |
|
||||
drizzle: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
accounts: state.accounts, |
|
||||
drizzleStatus: state.drizzleStatus, |
|
||||
web3: state.web3, |
|
||||
orbitDB: state.orbitDB |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default drizzleConnect(LoadingContainer, mapStateToProps) |
|
@ -1,195 +0,0 @@ |
|||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import React, { Component } from 'react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import { Tab } from 'semantic-ui-react' |
|
||||
|
|
||||
import WithBlockchainData from '../components/WithBlockchainData'; |
|
||||
import ProfileInformation from '../components/ProfileInformation'; |
|
||||
import TopicList from '../components/TopicList'; |
|
||||
import PostList from '../components/PostList'; |
|
||||
import LoadingSpinner from '../components/LoadingSpinner'; |
|
||||
import { |
|
||||
showProgressBar, |
|
||||
hideProgressBar, |
|
||||
setNavBarTitle |
|
||||
} from '../redux/actions/userInterfaceActions'; |
|
||||
|
|
||||
class Profile extends Component { |
|
||||
constructor(props, context) { |
|
||||
super(props); |
|
||||
|
|
||||
this.props.store.dispatch(showProgressBar()); |
|
||||
|
|
||||
this.propsToView = this.propsToView.bind(this); |
|
||||
|
|
||||
this.drizzle = context.drizzle; |
|
||||
|
|
||||
this.state = { |
|
||||
userAddress: this.props.params.address ? this.props.params.address : this.props.user.address |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
if (!this.props.user.hasSignedUp) { |
|
||||
this.context.router.push("/signup"); |
|
||||
return(null); |
|
||||
} |
|
||||
|
|
||||
this.propsToView(); |
|
||||
var infoTab = |
|
||||
(<WithBlockchainData |
|
||||
component={ProfileInformation} |
|
||||
callsInfo={[{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getUserDateOfRegister', |
|
||||
params: [this.state.userAddress] |
|
||||
},{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getOrbitDBId', |
|
||||
params: [this.state.userAddress] |
|
||||
}]} |
|
||||
address={this.state.userAddress} |
|
||||
username={this.username} |
|
||||
numberOfTopics={this.topicIDs && this.topicIDs.length} |
|
||||
numberOfPosts={this.postIDs && this.postIDs.length} |
|
||||
self={this.state.userAddress === this.props.user.address} |
|
||||
key="profileInfo" |
|
||||
/>); |
|
||||
var topicsTab = |
|
||||
(<div className="profile-tab"> |
|
||||
{this.topicIDs |
|
||||
? <TopicList topicIDs={this.topicIDs} /> |
|
||||
: <LoadingSpinner /> |
|
||||
} |
|
||||
</div>); |
|
||||
var postsTab = |
|
||||
(<div className="profile-tab"> |
|
||||
{this.postIDs |
|
||||
? <PostList postIDs={this.postIDs} recentToTheTop /> |
|
||||
: <LoadingSpinner /> |
|
||||
} |
|
||||
</div>); |
|
||||
|
|
||||
const profilePanes = [ |
|
||||
{ |
|
||||
menuItem: 'INFORMATION', |
|
||||
pane: { |
|
||||
key: 'INFORMATION', |
|
||||
content: (infoTab), |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
menuItem: 'TOPICS', |
|
||||
pane: { |
|
||||
key: 'TOPICS', |
|
||||
content: (topicsTab), |
|
||||
}, |
|
||||
}, |
|
||||
{ |
|
||||
menuItem: 'POSTS', |
|
||||
pane: { |
|
||||
key: 'POSTS', |
|
||||
content: (postsTab), |
|
||||
}, |
|
||||
}, |
|
||||
] |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
<Tab |
|
||||
menu={{ secondary: true, pointing: true }} |
|
||||
panes={profilePanes} |
|
||||
renderActiveOnly={false} /> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
propsToView(){ |
|
||||
if (!this.username){ |
|
||||
let transaction = this.props.blockchainData |
|
||||
.find(transaction => transaction.callInfo.method === "getUsername"); |
|
||||
if (transaction.returnData){ |
|
||||
this.username = transaction.returnData; |
|
||||
} |
|
||||
} |
|
||||
if (!this.topicIDs){ |
|
||||
let transaction = this.props.blockchainData |
|
||||
.find(transaction => transaction.callInfo.method === "getUserTopics"); |
|
||||
if (transaction.returnData){ |
|
||||
this.topicIDs = transaction.returnData; |
|
||||
} |
|
||||
} |
|
||||
if (!this.postIDs){ |
|
||||
let transaction = this.props.blockchainData |
|
||||
.find(transaction => transaction.callInfo.method === "getUserPosts"); |
|
||||
if (transaction.returnData){ |
|
||||
this.postIDs = transaction.returnData; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate(){ |
|
||||
if (this.username){ |
|
||||
this.props.store.dispatch(setNavBarTitle(this.username)); |
|
||||
if (this.topicIDs && this.postIDs){ |
|
||||
this.props.store.dispatch(hideProgressBar()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Profile.contextTypes = { |
|
||||
drizzle: PropTypes.object, |
|
||||
router: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user, |
|
||||
orbitDB: state.orbitDB |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
class ProfileContainer extends Component { |
|
||||
constructor(props){ |
|
||||
super(props); |
|
||||
|
|
||||
let userAddress; |
|
||||
if (this.props.params.address){ |
|
||||
userAddress = this.props.params.address; |
|
||||
} else { |
|
||||
userAddress = this.props.user.address; |
|
||||
} |
|
||||
|
|
||||
this.profile = <WithBlockchainData |
|
||||
component={drizzleConnect(Profile, mapStateToProps)} |
|
||||
callsInfo={[{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getUsername', |
|
||||
params: [userAddress] |
|
||||
},{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getUserTopics', |
|
||||
params: [userAddress] |
|
||||
},{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getUserPosts', |
|
||||
params: [userAddress] |
|
||||
}]} |
|
||||
params={this.props.params} |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return(this.profile); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const containerProps = state => { |
|
||||
return { |
|
||||
user: state.user |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default drizzleConnect(ProfileContainer, containerProps); |
|
@ -1,50 +0,0 @@ |
|||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import React, { Component } from 'react'; |
|
||||
import UsernameFormContainer from './UsernameFormContainer'; |
|
||||
|
|
||||
import { Header } from 'semantic-ui-react'; |
|
||||
|
|
||||
class SignUp extends Component { |
|
||||
constructor(props){ |
|
||||
super(props); |
|
||||
|
|
||||
this.signedUp = this.signedUp.bind(this); |
|
||||
} |
|
||||
|
|
||||
signedUp(){ |
|
||||
this.props.router.push("/home"); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return ( |
|
||||
this.props.user.hasSignedUp |
|
||||
?(<div className="vertical-center-in-parent"> |
|
||||
<Header color='teal' textAlign='center' as='h2'> |
|
||||
There is already an account for this addresss. |
|
||||
</Header> |
|
||||
<Header color='teal' textAlign='center' as='h4'> |
|
||||
If you want to create another account please change your address. |
|
||||
</Header> |
|
||||
</div>) |
|
||||
:(<div className="sign-up-container"> |
|
||||
<div> |
|
||||
<h1>Sign Up</h1> |
|
||||
<p className="no-margin"> |
|
||||
<strong>Account address:</strong> {this.props.user.address} |
|
||||
</p> |
|
||||
<UsernameFormContainer signedUp={this.signedUp}/> |
|
||||
</div> |
|
||||
</div>) |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const SignUpContainer = drizzleConnect(SignUp, mapStateToProps); |
|
||||
|
|
||||
export default SignUpContainer; |
|