mirror of https://gitlab.com/ecentrics/concordia
Ezerous
6 years ago
71 changed files with 437 additions and 6556 deletions
@ -0,0 +1,37 @@ |
|||||
|
{ |
||||
|
"name": "apella", |
||||
|
"version": "0.1.0", |
||||
|
"private": true, |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "https://gitlab.com/Ezerous/Apella.git" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@drizzle-utils/get-web3": "^0.1.4-alpha.0", |
||||
|
"@drizzle-utils/get-contract-instance": "^0.1.4-alpha.0", |
||||
|
"@drizzle-utils/get-accounts": "0.1.4-alpha.0", |
||||
|
"drizzle": "^1.3.3", |
||||
|
"drizzle-react": "^1.2.0", |
||||
|
"drizzle-react-components": "^1.2.1", |
||||
|
"react": "^16.8.1", |
||||
|
"react-dom": "^16.8.1", |
||||
|
"react-scripts": "^2.1.5", |
||||
|
"redux": "^4.0.1", |
||||
|
"redux-saga": "^0.16.0" |
||||
|
}, |
||||
|
"scripts": { |
||||
|
"start": "react-scripts start", |
||||
|
"build": "react-scripts build", |
||||
|
"test": "react-scripts test", |
||||
|
"eject": "react-scripts eject" |
||||
|
}, |
||||
|
"eslintConfig": { |
||||
|
"extends": "react-app" |
||||
|
}, |
||||
|
"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,38 @@ |
|||||
|
<!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`. |
||||
|
--> |
||||
|
<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,17 @@ |
|||||
|
import React from "react"; |
||||
|
import { AccountData, ContractData } from "drizzle-react-components"; |
||||
|
|
||||
|
export default ({ accounts }) => ( |
||||
|
<div className="App"> |
||||
|
<div className="section"> |
||||
|
<h1>Active Account</h1> |
||||
|
<AccountData accountIndex="0" units="ether" precision="3" /> |
||||
|
</div> |
||||
|
<div className="section"> |
||||
|
<h1>Has user signed up?</h1> |
||||
|
<p> |
||||
|
<ContractData contract="Forum" method="hasUserSignedUp" methodArgs={[accounts[0],{from: accounts[0]}]} /> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
@ -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'] |
||||
|
}, |
||||
|
polls: { |
||||
|
accounts: 2000, |
||||
|
blocks: 2000 |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
export default drizzleOptions; |
@ -0,0 +1,14 @@ |
|||||
|
import AppComponent from "../components/AppComponent"; |
||||
|
import { drizzleConnect } from "drizzle-react"; |
||||
|
|
||||
|
const mapStateToProps = state => { |
||||
|
return { |
||||
|
accounts: state.accounts, |
||||
|
Forum: state.contracts.Forum, |
||||
|
drizzleStatus: state.drizzleStatus |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
const AppContainer = drizzleConnect(AppComponent, mapStateToProps); |
||||
|
|
||||
|
export default AppContainer; |
@ -0,0 +1,23 @@ |
|||||
|
import React from "react"; |
||||
|
import { render } from "react-dom"; |
||||
|
import { DrizzleProvider } from "drizzle-react"; |
||||
|
import { LoadingContainer } from "drizzle-react-components"; |
||||
|
import drizzleOptions from "./config/drizzleOptions"; |
||||
|
import AppContainer from "./containers/AppContainer"; |
||||
|
import store from './redux/store'; |
||||
|
|
||||
|
import * as serviceWorker from "./utils/serviceWorker"; |
||||
|
|
||||
|
render( |
||||
|
<DrizzleProvider options={drizzleOptions} store={store}> |
||||
|
<LoadingContainer> |
||||
|
<AppContainer /> |
||||
|
</LoadingContainer> |
||||
|
</DrizzleProvider>, |
||||
|
document.getElementById('root') |
||||
|
); |
||||
|
|
||||
|
// If you want your app to work offline and load faster, you can change
|
||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
|
// Learn more about service workers: http://bit.ly/CRA-PWA
|
||||
|
serviceWorker.unregister(); |
@ -0,0 +1,10 @@ |
|||||
|
import { combineReducers } from 'redux'; |
||||
|
import { drizzleReducers } from 'drizzle'; |
||||
|
import userReducer from "./userReducer"; |
||||
|
|
||||
|
const reducer = combineReducers({ |
||||
|
user: userReducer, |
||||
|
...drizzleReducers |
||||
|
}); |
||||
|
|
||||
|
export default reducer; |
@ -1,11 +1,9 @@ |
|||||
import { all, fork } from 'redux-saga/effects' |
import { all, fork } from 'redux-saga/effects' |
||||
import { drizzleSagas } from 'drizzle' |
import { drizzleSagas } from 'drizzle' |
||||
import { contractSaga } from "./contractSaga"; |
|
||||
import userSaga from "./userSaga"; |
import userSaga from "./userSaga"; |
||||
import orbitSaga from "../../util/orbitSaga"; |
|
||||
|
|
||||
export default function* root() { |
export default function* root() { |
||||
let sagas = [...drizzleSagas,userSaga,orbitSaga,contractSaga]; |
let sagas = [...drizzleSagas, userSaga]; |
||||
yield all( |
yield all( |
||||
sagas.map(saga => fork(saga)) |
sagas.map(saga => fork(saga)) |
||||
) |
) |
@ -0,0 +1,66 @@ |
|||||
|
import getWeb3 from "@drizzle-utils/get-web3"; |
||||
|
import getContractInstance from "@drizzle-utils/get-contract-instance"; |
||||
|
import getAccounts from "@drizzle-utils/get-accounts"; |
||||
|
import { call, put, take, takeLatest, takeEvery } from 'redux-saga/effects' |
||||
|
|
||||
|
import Forum from "../../contracts/Forum.json"; |
||||
|
|
||||
|
let initFlag, web3, contract, account; |
||||
|
|
||||
|
function* initUser() { |
||||
|
if(!initFlag) { |
||||
|
web3 = yield call(getWeb3); |
||||
|
contract = yield call(getContractInstance,{ |
||||
|
web3, |
||||
|
artifact: Forum |
||||
|
}); |
||||
|
initFlag=true; |
||||
|
yield put({type: 'USER_SAGA_INITIALIZED', ...[]}); |
||||
|
} |
||||
|
else |
||||
|
console.warn("Attempted to reinitialize userSaga!"); |
||||
|
} |
||||
|
|
||||
|
function* updateUserData() { |
||||
|
if(initFlag){ |
||||
|
const currentAccount = (yield call(getAccounts, {web3}))[0]; |
||||
|
if(currentAccount!==account) { |
||||
|
account = currentAccount; |
||||
|
yield put({type: 'ACCOUNT_CHANGED', ...[]}); |
||||
|
} |
||||
|
const txObj1 = yield call(contract.methods["hasUserSignedUp"], ...[account]); |
||||
|
try { |
||||
|
const callResult = yield call(txObj1.call, {address:account}); |
||||
|
if(callResult) { |
||||
|
const txObj2 = yield call(contract.methods["getUsername"], ...[account]); |
||||
|
const username = yield call(txObj2.call, {address:account}); |
||||
|
const dispatchArgs = { |
||||
|
address: account, |
||||
|
username: username |
||||
|
}; |
||||
|
yield put({type: 'USER_HAS_SIGNED_UP', ...dispatchArgs}); |
||||
|
} |
||||
|
else{ |
||||
|
const dispatchArgs = { |
||||
|
address: account |
||||
|
}; |
||||
|
yield put({type: 'USER_IS_GUEST', ...dispatchArgs}); |
||||
|
} |
||||
|
} |
||||
|
catch (error) { |
||||
|
console.error(error); |
||||
|
yield put({type: 'USER_FETCHING_ERROR', ...[]}) |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
console.warn("Attempted to fetch data without initializing!"); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
function* userSaga() { |
||||
|
yield takeLatest("DRIZZLE_INITIALIZED", initUser); |
||||
|
yield take("USER_SAGA_INITIALIZED"); |
||||
|
yield takeEvery("ACCOUNTS_FETCHED", updateUserData); |
||||
|
} |
||||
|
|
||||
|
export default userSaga; |
@ -0,0 +1,30 @@ |
|||||
|
import { createStore, applyMiddleware, compose } from 'redux'; |
||||
|
import reducer from './reducers/reducer'; |
||||
|
import rootSaga from './sagas/rootSaga'; |
||||
|
import createSagaMiddleware from 'redux-saga'; |
||||
|
import { generateContractsInitialState } from 'drizzle'; |
||||
|
|
||||
|
import drizzleOptions from '../config/drizzleOptions'; |
||||
|
|
||||
|
// Redux DevTools
|
||||
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; |
||||
|
|
||||
|
const sagaMiddleware = createSagaMiddleware(); |
||||
|
|
||||
|
const initialState = { |
||||
|
contracts: generateContractsInitialState(drizzleOptions) |
||||
|
}; |
||||
|
|
||||
|
const store = createStore( |
||||
|
reducer, |
||||
|
initialState, |
||||
|
composeEnhancers( |
||||
|
applyMiddleware( |
||||
|
sagaMiddleware |
||||
|
) |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
sagaMiddleware.run(rootSaga); |
||||
|
|
||||
|
export default store; |
@ -0,0 +1,135 @@ |
|||||
|
// This optional code is used to register a service worker.
|
||||
|
// register() is not called by default.
|
||||
|
|
||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||
|
// resources are updated in the background.
|
||||
|
|
||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||
|
// opt-in, read http://bit.ly/CRA-PWA
|
||||
|
|
||||
|
const isLocalhost = Boolean( |
||||
|
window.location.hostname === 'localhost' || |
||||
|
// [::1] is the IPv6 localhost address.
|
||||
|
window.location.hostname === '[::1]' || |
||||
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
|
window.location.hostname.match( |
||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
export function register(config) { |
||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { |
||||
|
// The URL constructor is available in all browsers that support SW.
|
||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); |
||||
|
if (publicUrl.origin !== window.location.origin) { |
||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
window.addEventListener('load', () => { |
||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; |
||||
|
|
||||
|
if (isLocalhost) { |
||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
|
checkValidServiceWorker(swUrl, config); |
||||
|
|
||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||
|
// service worker/PWA documentation.
|
||||
|
navigator.serviceWorker.ready.then(() => { |
||||
|
console.log( |
||||
|
'This web app is being served cache-first by a service ' + |
||||
|
'worker. To learn more, visit http://bit.ly/CRA-PWA' |
||||
|
); |
||||
|
}); |
||||
|
} else { |
||||
|
// Is not localhost. Just register service worker
|
||||
|
registerValidSW(swUrl, config); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function registerValidSW(swUrl, config) { |
||||
|
navigator.serviceWorker |
||||
|
.register(swUrl) |
||||
|
.then(registration => { |
||||
|
registration.onupdatefound = () => { |
||||
|
const installingWorker = registration.installing; |
||||
|
if (installingWorker == null) { |
||||
|
return; |
||||
|
} |
||||
|
installingWorker.onstatechange = () => { |
||||
|
if (installingWorker.state === 'installed') { |
||||
|
if (navigator.serviceWorker.controller) { |
||||
|
// At this point, the updated precached content has been fetched,
|
||||
|
// but the previous service worker will still serve the older
|
||||
|
// content until all client tabs are closed.
|
||||
|
console.log( |
||||
|
'New content is available and will be used when all ' + |
||||
|
'tabs for this page are closed. See http://bit.ly/CRA-PWA.' |
||||
|
); |
||||
|
|
||||
|
// Execute callback
|
||||
|
if (config && config.onUpdate) { |
||||
|
config.onUpdate(registration); |
||||
|
} |
||||
|
} else { |
||||
|
// At this point, everything has been precached.
|
||||
|
// It's the perfect time to display a
|
||||
|
// "Content is cached for offline use." message.
|
||||
|
console.log('Content is cached for offline use.'); |
||||
|
|
||||
|
// Execute callback
|
||||
|
if (config && config.onSuccess) { |
||||
|
config.onSuccess(registration); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}; |
||||
|
}) |
||||
|
.catch(error => { |
||||
|
console.error('Error during service worker registration:', error); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function checkValidServiceWorker(swUrl, config) { |
||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||
|
fetch(swUrl) |
||||
|
.then(response => { |
||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
|
const contentType = response.headers.get('content-type'); |
||||
|
if ( |
||||
|
response.status === 404 || |
||||
|
(contentType != null && contentType.indexOf('javascript') === -1) |
||||
|
) { |
||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||
|
navigator.serviceWorker.ready.then(registration => { |
||||
|
registration.unregister().then(() => { |
||||
|
window.location.reload(); |
||||
|
}); |
||||
|
}); |
||||
|
} else { |
||||
|
// Service worker found. Proceed as normal.
|
||||
|
registerValidSW(swUrl, config); |
||||
|
} |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
console.log( |
||||
|
'No internet connection found. App is running in offline mode.' |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function unregister() { |
||||
|
if ('serviceWorker' in navigator) { |
||||
|
navigator.serviceWorker.ready.then(registration => { |
||||
|
registration.unregister(); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -1,40 +1,17 @@ |
|||||
{ |
{ |
||||
"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" |
||||
}, |
}, |
||||
"dependencies": { |
"main": "truffle-config.js", |
||||
"drizzle": "^1.1.5", |
"directories": { |
||||
"drizzle-react": "^1.1.1", |
"test": "test" |
||||
"drizzle-react-components": "^1.1.0", |
|
||||
"eth-block-tracker-es5": "^2.3.2", |
|
||||
"ipfs": "^0.30.0", |
|
||||
"orbit-db": "^0.19.9", |
|
||||
"orbit-db-keystore": "^0.1.0", |
|
||||
"prop-types": "^15.6.1", |
|
||||
"react": "^16.3.2", |
|
||||
"react-dom": "^16.3.2", |
|
||||
"react-markdown": "^3.3.2", |
|
||||
"react-redux": "^5.0.7", |
|
||||
"react-router": "^3.2.1", |
|
||||
"react-router-dom": "^4.2.2", |
|
||||
"react-router-redux": "^4.0.8", |
|
||||
"react-scripts": "^1.1.4", |
|
||||
"react-timeago": "^4.1.9", |
|
||||
"react-user-avatar": "^1.10.0", |
|
||||
"redux": "^3.7.2", |
|
||||
"redux-saga": "0.16.0", |
|
||||
"semantic-ui-react": "^0.81.1", |
|
||||
"uuid": "^3.2.1", |
|
||||
"web3": "^1.0.0-beta.34" |
|
||||
}, |
}, |
||||
"scripts": { |
"dependencies": { |
||||
"start": "react-scripts start", |
"openzeppelin-solidity": "^2.1.2" |
||||
"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,5 +0,0 @@ |
|||||
/* PROFILE SCREEN */ |
|
||||
|
|
||||
.profile-tab { |
|
||||
width: 100%; |
|
||||
} |
|
@ -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; |
|
||||
} |
|
File diff suppressed because one or more lines are too long
@ -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; |
|
@ -1,139 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import { Form, TextArea, Button, Icon } from 'semantic-ui-react' |
|
||||
import NewTopicPreview from '../components/NewTopicPreview' |
|
||||
|
|
||||
import { createTopic } from '../redux/actions/transactionsMonitorActions'; |
|
||||
|
|
||||
class StartTopic 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.state = { |
|
||||
topicSubjectInput: '', |
|
||||
topicMessageInput: '', |
|
||||
topicSubjectInputEmptySubmit: false, |
|
||||
topicMessageInputEmptySubmit: false, |
|
||||
previewEnabled: false, |
|
||||
previewDate: "" |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
async validateAndPost() { |
|
||||
if (this.state.topicSubjectInput === '' || this.state.topicMessageInput === ''){ |
|
||||
this.setState({ |
|
||||
topicSubjectInputEmptySubmit: this.state.topicSubjectInput === '', |
|
||||
topicMessageInputEmptySubmit: this.state.topicMessageInput === '' |
|
||||
}); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
this.props.store.dispatch( |
|
||||
createTopic( |
|
||||
{ |
|
||||
topicSubject: this.state.topicSubjectInput, |
|
||||
topicMessage: this.state.topicMessageInput |
|
||||
} |
|
||||
) |
|
||||
); |
|
||||
this.context.router.push("/home"); |
|
||||
} |
|
||||
|
|
||||
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() { |
|
||||
if (!this.props.user.hasSignedUp) { |
|
||||
this.context.router.push("/signup"); |
|
||||
return(null); |
|
||||
} |
|
||||
|
|
||||
var previewEditText = this.state.previewEnabled ? "Edit" : "Preview"; |
|
||||
return ( |
|
||||
<div> |
|
||||
{this.state.previewEnabled && |
|
||||
<NewTopicPreview |
|
||||
date={this.state.previewDate} |
|
||||
subject={this.state.topicSubjectInput} |
|
||||
content={this.state.topicMessageInput} |
|
||||
/> |
|
||||
} |
|
||||
<Form> |
|
||||
{!this.state.previewEnabled && |
|
||||
[<Form.Field key={"topicSubjectInput"}> |
|
||||
<Form.Input name={"topicSubjectInput"} |
|
||||
error={this.state.topicSubjectInputEmptySubmit} |
|
||||
type="text" |
|
||||
value={this.state.topicSubjectInput} |
|
||||
placeholder="Subject" |
|
||||
id="topicSubjectInput" |
|
||||
onChange={this.handleInputChange} /> |
|
||||
</Form.Field>, |
|
||||
<TextArea key={"topicMessageInput"} |
|
||||
name={"topicMessageInput"} |
|
||||
className={this.state.topicMessageInputEmptySubmit ? "form-textarea-required" : ""} |
|
||||
value={this.state.topicMessageInput} |
|
||||
placeholder="Post" |
|
||||
id="topicMessageInput" |
|
||||
rows={5} |
|
||||
autoHeight |
|
||||
onChange={this.handleInputChange} />] |
|
||||
} |
|
||||
<br/><br/> |
|
||||
<Button.Group> |
|
||||
<Button animated key="submit" type="button" color='teal' |
|
||||
onClick={this.validateAndPost}> |
|
||||
<Button.Content visible>Post</Button.Content> |
|
||||
<Button.Content hidden> |
|
||||
<Icon name='send' /> |
|
||||
</Button.Content> |
|
||||
</Button> |
|
||||
<Button type="button" color='yellow' |
|
||||
onClick={this.handlePreviewToggle}> |
|
||||
{previewEditText} |
|
||||
</Button> |
|
||||
</Button.Group> |
|
||||
</Form> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
StartTopic.contextTypes = { |
|
||||
router: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
orbitDB: state.orbitDB, |
|
||||
user: state.user |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const StartTopicContainer = drizzleConnect(StartTopic, mapStateToProps) |
|
||||
|
|
||||
export default StartTopicContainer; |
|
@ -1,153 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
|
|
||||
import WithBlockchainData from '../components/WithBlockchainData'; |
|
||||
import PostList from '../components/PostList'; |
|
||||
import NewPost from '../components/NewPost'; |
|
||||
import FloatingButton from '../components/FloatingButton'; |
|
||||
|
|
||||
import { |
|
||||
showProgressBar, |
|
||||
hideProgressBar, |
|
||||
setNavBarTitle |
|
||||
} from '../redux/actions/userInterfaceActions'; |
|
||||
|
|
||||
class Topic extends Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
|
|
||||
this.props.store.dispatch(showProgressBar()); |
|
||||
|
|
||||
this.fetchTopicSubject = this.fetchTopicSubject.bind(this); |
|
||||
this.togglePostingState = this.togglePostingState.bind(this); |
|
||||
this.postCreated = this.postCreated.bind(this); |
|
||||
|
|
||||
this.state = { |
|
||||
topicID: this.props.params.topicId, |
|
||||
topicSubject: null, |
|
||||
postFocus: this.props.params.postId && /^[0-9]+$/.test(this.props.params.postId) |
|
||||
? this.props.params.postId |
|
||||
: null, |
|
||||
fetchTopicSubjectStatus: null, |
|
||||
posting: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
async fetchTopicSubject(orbitDBAddress) { |
|
||||
let orbitData; |
|
||||
if (this.props.blockchainData[0].returnData[1] === this.props.user.address) { |
|
||||
orbitData = this.props.orbitDB.topicsDB.get(this.state.topicID); |
|
||||
} else { |
|
||||
const fullAddress = "/orbitdb/" + orbitDBAddress + "/topics"; |
|
||||
const store = await this.props.orbitDB.orbitdb.keyvalue(fullAddress); |
|
||||
await store.load(); |
|
||||
|
|
||||
let localOrbitData = store.get(this.state.topicID); |
|
||||
if (localOrbitData) { |
|
||||
orbitData = localOrbitData; |
|
||||
} else { |
|
||||
// Wait until we have received something from the network
|
|
||||
store.events.on('replicated', () => { |
|
||||
orbitData = store.get(this.state.topicID); |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
this.props.store.dispatch(hideProgressBar()); |
|
||||
this.props.store.dispatch(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() { |
|
||||
var topicContents; |
|
||||
if (this.props.blockchainData[0].status === "success") { |
|
||||
topicContents = ( |
|
||||
(<div> |
|
||||
<PostList postIDs={this.props.blockchainData[0].returnData[4]} |
|
||||
focusOnPost={this.state.postFocus ? this.state.postFocus : null}/> |
|
||||
{this.state.posting && |
|
||||
<NewPost topicID={this.state.topicID} |
|
||||
subject={this.state.topicSubject} |
|
||||
postIndex={this.props.blockchainData[0].returnData[4].length} |
|
||||
onCancelClick={() => {this.togglePostingState()}} |
|
||||
onPostCreated={() => {this.postCreated()}} |
|
||||
/> |
|
||||
} |
|
||||
<div className="posts-list-spacer"></div> |
|
||||
{this.props.user.hasSignedUp && !this.state.posting && |
|
||||
<FloatingButton onClick={this.togglePostingState}/> |
|
||||
} |
|
||||
</div>) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div className="fill"> |
|
||||
{topicContents} |
|
||||
{!this.state.posting && |
|
||||
<div className="bottom-overlay-pad"></div> |
|
||||
} |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate() { |
|
||||
if (this.props.blockchainData[0].status === "success") { |
|
||||
if (this.state.fetchTopicSubjectStatus === null){ |
|
||||
this.setState({ fetchTopicSubjectStatus: "fetching"}) |
|
||||
this.fetchTopicSubject(this.props.blockchainData[0].returnData[0]); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user, |
|
||||
orbitDB: state.orbitDB |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
class TopicContainer extends Component { |
|
||||
constructor(props){ |
|
||||
super(props); |
|
||||
|
|
||||
if (!/^[0-9]+$/.test(props.params.topicId)){ //Topic ID should be a positive integer
|
|
||||
this.props.router.push("/404"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return( |
|
||||
<WithBlockchainData |
|
||||
component={drizzleConnect(Topic, mapStateToProps)} |
|
||||
callsInfo={[{ |
|
||||
contract: 'Forum', |
|
||||
method: 'getTopic', |
|
||||
params: [this.props.params.topicId] |
|
||||
}]} |
|
||||
params={this.props.params} |
|
||||
/> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default TopicContainer; |
|
@ -1,199 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import { Message } from 'semantic-ui-react'; |
|
||||
|
|
||||
import { updateTransaction } from '../redux/actions/transactionsMonitorActions'; |
|
||||
|
|
||||
class RightSideBar extends Component { |
|
||||
constructor(props, context) { |
|
||||
super(props); |
|
||||
|
|
||||
this.handleMessageDismiss = this.handleMessageDismiss.bind(this); |
|
||||
this.updateTransactions = this.updateTransactions.bind(this); |
|
||||
this.completeWithOrbitInteractions = this.completeWithOrbitInteractions.bind(this); |
|
||||
|
|
||||
this.drizzle = context.drizzle; |
|
||||
this.transactionsStackIds = []; |
|
||||
this.transactionsTxHashes = []; |
|
||||
|
|
||||
this.state = { |
|
||||
transactionsCompletionTime: [], |
|
||||
isTransactionMessageActive: [] |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
handleMessageDismiss(messageIndex) { |
|
||||
let isTransactionMessageActiveShallowCopy = this.state.isTransactionMessageActive.slice(); |
|
||||
isTransactionMessageActiveShallowCopy[messageIndex] = false; |
|
||||
this.setState({ |
|
||||
isTransactionMessageActive: isTransactionMessageActiveShallowCopy |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
let transactionMessages = this.props.transactionsQueue.map((transaction, index) => { |
|
||||
if (!this.state.isTransactionMessageActive[index]){ |
|
||||
return null; |
|
||||
} |
|
||||
let color = 'black'; |
|
||||
let message = []; |
|
||||
|
|
||||
while(true) { |
|
||||
if (transaction.status === 'initialized') break; |
|
||||
message.push("New transaction has been queued and is waiting your confirmation."); |
|
||||
|
|
||||
if (transaction.status === 'acceptance_pending') break; |
|
||||
message.push(<br key="confirmed"/>); |
|
||||
message.push("- transaction confirmed"); |
|
||||
|
|
||||
if (transaction.status === 'mining_pending') break; |
|
||||
message.push(<br key="mined"/>); |
|
||||
message.push("- transaction mined"); |
|
||||
|
|
||||
if (transaction.status === 'success') { |
|
||||
color = 'green'; |
|
||||
message.push(<br key="success"/>); |
|
||||
message.push("- transaction completed successfully"); |
|
||||
break; |
|
||||
} |
|
||||
if (transaction.status === 'error') { |
|
||||
color = 'red'; |
|
||||
message.push(<br key="fail"/>); |
|
||||
message.push("Transaction failed to complete!"); |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div className="sidebar-message" key={index}> |
|
||||
<Message color={color} onDismiss={() => {this.handleMessageDismiss(index)}}> |
|
||||
{message} |
|
||||
</Message> |
|
||||
</div> |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
return (transactionMessages); |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate(){ //Maybe change to componentWillReceiveProps()
|
|
||||
this.updateTransactions(); |
|
||||
} |
|
||||
|
|
||||
updateTransactions(){ |
|
||||
for (var index = 0; index < this.props.transactionsQueue.length; ++index) { |
|
||||
let transaction = this.props.transactionsQueue[index]; |
|
||||
|
|
||||
if (transaction.status === 'initialized' && |
|
||||
this.transactionsStackIds[index] === undefined){ |
|
||||
/* User submitted a new transaction */ |
|
||||
|
|
||||
let isTransactionMessageActiveShallowCopy = this.state |
|
||||
.isTransactionMessageActive.slice(); |
|
||||
isTransactionMessageActiveShallowCopy[index] = true; |
|
||||
this.setState({ |
|
||||
isTransactionMessageActive: isTransactionMessageActiveShallowCopy |
|
||||
}); |
|
||||
|
|
||||
this.transactionsStackIds[index] = (this.drizzle |
|
||||
.contracts[transaction.contract] |
|
||||
.methods[transaction.method] |
|
||||
.cacheSend(...(transaction.params))); |
|
||||
this.props.store.dispatch(updateTransaction(index, { |
|
||||
status: 'acceptance_pending' |
|
||||
})); |
|
||||
} else if (transaction.status === 'acceptance_pending'){ |
|
||||
if (this.props.transactionStack[this.transactionsStackIds[index]]){ |
|
||||
/* User confirmed the transaction */ |
|
||||
|
|
||||
//Gets transaction's hash
|
|
||||
this.transactionsTxHashes[index] = (this.props |
|
||||
.transactionStack[this.transactionsStackIds[index]]); |
|
||||
this.props.store.dispatch(updateTransaction(index, { |
|
||||
status: 'mining_pending' |
|
||||
})); |
|
||||
} |
|
||||
} else if (transaction.status === 'mining_pending'){ |
|
||||
if (this.props.transactions[this.transactionsTxHashes[index]] |
|
||||
.status === "success"){ |
|
||||
/* Transaction completed successfully */ |
|
||||
|
|
||||
//Gets returned data by contract
|
|
||||
let data = this.props.transactions[this.transactionsTxHashes[index]] |
|
||||
.receipt.events[transaction.event].returnValues; |
|
||||
|
|
||||
this.props.store.dispatch(updateTransaction(index, { |
|
||||
status: 'success', |
|
||||
returnData: data |
|
||||
})); |
|
||||
|
|
||||
let transactionsCompletionTimeShallowCopy = this.state |
|
||||
.transactionsCompletionTime.slice(); |
|
||||
transactionsCompletionTimeShallowCopy[index] = new Date().getTime(); |
|
||||
this.setState({ |
|
||||
transactionsCompletionTime: transactionsCompletionTimeShallowCopy |
|
||||
}); |
|
||||
|
|
||||
this.completeWithOrbitInteractions(this.props.transactionsQueue[index], data); |
|
||||
} else if (this.props.transactions[this.transactionsTxHashes[index]] |
|
||||
.status === "error"){ |
|
||||
/* Transaction failed to complete */ |
|
||||
|
|
||||
this.props.store.dispatch(updateTransaction(index, { |
|
||||
status: 'error' |
|
||||
})); |
|
||||
|
|
||||
let transactionsCompletionTimeShallowCopy = this.state |
|
||||
.transactionsCompletionTime.slice(); |
|
||||
transactionsCompletionTimeShallowCopy[index] = new Date().getTime(); |
|
||||
this.setState({ |
|
||||
transactionsCompletionTime: transactionsCompletionTimeShallowCopy |
|
||||
}); |
|
||||
//TODO handle this gracefully
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async completeWithOrbitInteractions(transaction, returnData){ |
|
||||
switch (transaction.event){ |
|
||||
case 'TopicCreated': |
|
||||
await this.props.orbitDB.topicsDB.put(returnData.topicID, { |
|
||||
subject: transaction.userInputs.topicSubject |
|
||||
}); |
|
||||
|
|
||||
await this.props.orbitDB.postsDB.put(returnData.postID, { |
|
||||
subject: transaction.userInputs.topicSubject, |
|
||||
content: transaction.userInputs.topicMessage |
|
||||
}); |
|
||||
break; |
|
||||
case 'PostCreated': |
|
||||
await this.props.orbitDB.postsDB.put(returnData.postID, { |
|
||||
subject: transaction.userInputs.postSubject, |
|
||||
content: transaction.userInputs.postMessage |
|
||||
}); |
|
||||
break; |
|
||||
default: |
|
||||
break; //This transaction doesn't need a DB interaction to complete
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
RightSideBar.contextTypes = { |
|
||||
drizzle: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
orbitDB: state.orbitDB, |
|
||||
transactionsQueue: state.transactionsQueue.transactions, |
|
||||
transactions: state.transactions, |
|
||||
transactionStack: state.transactionStack |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const RightSideBarContainer = drizzleConnect(RightSideBar, mapStateToProps); |
|
||||
|
|
||||
export default RightSideBarContainer; |
|
@ -1,151 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
import PropTypes from 'prop-types'; |
|
||||
|
|
||||
import { Button, Message, Form, Dimmer, Loader, Header } from 'semantic-ui-react'; |
|
||||
|
|
||||
import { createDatabases } from './../util/orbit'; |
|
||||
import { updateUsername } from '../redux/actions/transactionsMonitorActions'; |
|
||||
|
|
||||
const contract = "Forum"; |
|
||||
const checkUsernameTakenMethod = "isUserNameTaken"; |
|
||||
const signUpMethod = "signUp"; |
|
||||
|
|
||||
class UsernameFormContainer extends Component { |
|
||||
constructor(props, context) { |
|
||||
super(props); |
|
||||
|
|
||||
this.handleInputChange = this.handleInputChange.bind(this); |
|
||||
this.handleSubmit = this.handleSubmit.bind(this); |
|
||||
this.completeAction = this.completeAction.bind(this); |
|
||||
|
|
||||
this.drizzle = context.drizzle; |
|
||||
this.contracts = this.drizzle.contracts; |
|
||||
|
|
||||
this.state = { |
|
||||
usernameInput: '', |
|
||||
error: false, |
|
||||
errorHeader: "", |
|
||||
errorMessage: "", |
|
||||
signingUp: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
handleInputChange(e, { name, value }) { |
|
||||
this.setState({ [name]: value }) |
|
||||
} |
|
||||
|
|
||||
handleSubmit() { |
|
||||
if (this.state.usernameInput === ''){ |
|
||||
this.setState({ |
|
||||
error: true, |
|
||||
errorHeader: "Data Incomplete", |
|
||||
errorMessage: "You need to provide a username" |
|
||||
}); |
|
||||
} else { |
|
||||
this.checkUsernameTakenDataKey = this.contracts[contract].methods[checkUsernameTakenMethod] |
|
||||
.cacheCall(this.state.usernameInput); |
|
||||
this.setState({ |
|
||||
error: false |
|
||||
}); |
|
||||
this.checkingUsernameTaken = true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async completeAction() { |
|
||||
if(this.props.user.hasSignedUp){ |
|
||||
this.props.store.dispatch(updateUsername(...[this.state.usernameInput], null)); |
|
||||
} else { |
|
||||
this.setState({ signingUp: true }); |
|
||||
const orbitdbInfo = await createDatabases(); |
|
||||
this.contracts[contract].methods[signUpMethod] |
|
||||
.cacheSend(...[this.state.usernameInput, |
|
||||
orbitdbInfo.id, |
|
||||
orbitdbInfo.topicsDB, |
|
||||
orbitdbInfo.postsDB, |
|
||||
orbitdbInfo.publicKey, |
|
||||
orbitdbInfo.privateKey |
|
||||
]); |
|
||||
} |
|
||||
this.setState({ usernameInput: '' }); |
|
||||
} |
|
||||
|
|
||||
componentWillReceiveProps(nextProps) { |
|
||||
if (this.state.signingUp && nextProps.user.hasSignedUp){ |
|
||||
this.props.signedUp(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentWillUpdate() { |
|
||||
if (this.checkingUsernameTaken){ |
|
||||
let dataFetched = this.drizzle.store.getState() |
|
||||
.contracts[contract][checkUsernameTakenMethod][this.checkUsernameTakenDataKey]; |
|
||||
if (dataFetched){ |
|
||||
this.checkingUsernameTaken = false; |
|
||||
if (dataFetched.value){ |
|
||||
this.setState({ |
|
||||
error: true, |
|
||||
errorHeader: "Data disapproved", |
|
||||
errorMessage: "This username is already taken" |
|
||||
}); |
|
||||
} else { |
|
||||
this.setState({ |
|
||||
error: false |
|
||||
}); |
|
||||
this.completeAction(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const hasSignedUp = this.props.user.hasSignedUp; |
|
||||
|
|
||||
if(hasSignedUp !== null) { |
|
||||
const buttonText = hasSignedUp ? "Update" : "Sign Up"; |
|
||||
const placeholderText = hasSignedUp ? this.props.user.username : "Username"; |
|
||||
var withError = this.state.error && {error: true}; |
|
||||
|
|
||||
return( |
|
||||
<div> |
|
||||
<Form onSubmit={this.handleSubmit} {...withError}> |
|
||||
<Form.Field required> |
|
||||
<label>Username</label> |
|
||||
<Form.Input |
|
||||
placeholder={placeholderText} |
|
||||
name='usernameInput' |
|
||||
value={this.state.usernameInput} |
|
||||
onChange={this.handleInputChange} |
|
||||
/> |
|
||||
</Form.Field> |
|
||||
<Message |
|
||||
error |
|
||||
header={this.state.errorHeader} |
|
||||
content={this.state.errorMessage} |
|
||||
/> |
|
||||
<Button type='submit'>{buttonText}</Button> |
|
||||
</Form> |
|
||||
<Dimmer active={this.state.signingUp || this.checkingUsernameTaken} page> |
|
||||
<Header as='h2' inverted> |
|
||||
<Loader size='large'>Magic elfs are processing your nobel request.</Loader> |
|
||||
</Header> |
|
||||
</Dimmer> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
return(null); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
UsernameFormContainer.contextTypes = { |
|
||||
drizzle: PropTypes.object |
|
||||
}; |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
user: state.user |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default drizzleConnect(UsernameFormContainer, mapStateToProps) |
|
@ -1,12 +0,0 @@ |
|||||
const epochTimeConverter = (timestamp) => { |
|
||||
var timestampDate = new Date(0); |
|
||||
timestampDate.setUTCSeconds(timestamp); |
|
||||
return ((timestampDate.getMonth() + 1) + " " |
|
||||
+ timestampDate.getDate() + ", " |
|
||||
+ timestampDate.getFullYear() + ", " |
|
||||
+ timestampDate.getHours() + ":" |
|
||||
+ timestampDate.getMinutes() + ":" |
|
||||
+ timestampDate.getSeconds()) |
|
||||
} |
|
||||
|
|
||||
export default epochTimeConverter; |
|
@ -1,52 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import { render } from 'react-dom'; |
|
||||
import { Router, IndexRedirect, browserHistory } from 'react-router'; |
|
||||
import { Route } from 'react-router-dom'; |
|
||||
import { syncHistoryWithStore } from 'react-router-redux'; |
|
||||
import { DrizzleProvider } from 'drizzle-react'; |
|
||||
|
|
||||
// Layout
|
|
||||
import CoreLayout from './layouts/CoreLayout/CoreLayout'; |
|
||||
|
|
||||
// Containers
|
|
||||
import LoadingContainer from './containers/LoadingContainer'; |
|
||||
import SignUpContainer from './containers/SignUpContainer'; |
|
||||
import HomeContainer from './containers/HomeContainer'; |
|
||||
import TopicContainer from './containers/TopicContainer'; |
|
||||
import StartTopicContainer from './containers/StartTopicContainer'; |
|
||||
import ProfileContainer from './containers/ProfileContainer'; |
|
||||
import NotFoundView from './components/NotFoundView'; |
|
||||
|
|
||||
import store from './redux/store'; |
|
||||
import drizzleOptions from './util/drizzleOptions'; |
|
||||
|
|
||||
import './assets/css/index.css'; |
|
||||
|
|
||||
// Initialize react-router-redux.
|
|
||||
const history = syncHistoryWithStore(browserHistory, store); |
|
||||
|
|
||||
render(( |
|
||||
<DrizzleProvider options={drizzleOptions} store={store}> |
|
||||
<LoadingContainer> |
|
||||
<Router history={history}> |
|
||||
<Route path="/" component={CoreLayout}> |
|
||||
<IndexRedirect to="/home" /> |
|
||||
<Route path="/home" |
|
||||
component={HomeContainer} /> |
|
||||
<Route path="/signup" |
|
||||
component={SignUpContainer} /> |
|
||||
<Route path="/topic/:topicId(/:postId)" |
|
||||
component={TopicContainer} /> |
|
||||
<Route path='/profile(/:address)(/:username)' |
|
||||
component={ProfileContainer} /> |
|
||||
<Route path='/startTopic' |
|
||||
component={StartTopicContainer} /> |
|
||||
<Route path='/404' component={NotFoundView} /> |
|
||||
<Route path='*' component={NotFoundView} /> |
|
||||
</Route> |
|
||||
</Router> |
|
||||
</LoadingContainer> |
|
||||
</DrizzleProvider> |
|
||||
), |
|
||||
document.getElementById('root') |
|
||||
); |
|
@ -1,53 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { drizzleConnect } from 'drizzle-react'; |
|
||||
|
|
||||
import NavBar from '../../components/NavBar'; |
|
||||
import TransactionsMonitorContainer from '../../containers/TransactionsMonitorContainer'; |
|
||||
|
|
||||
// Styles
|
|
||||
import '../../assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js'; |
|
||||
import '../../assets/css/App.css'; |
|
||||
import '../../assets/css/loading-container.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 '../../assets/css/progress-bar.css'; |
|
||||
|
|
||||
class CoreLayout extends Component { |
|
||||
render() { |
|
||||
return ( |
|
||||
<div className="App"> |
|
||||
<NavBar/> |
|
||||
<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"> |
|
||||
</aside> |
|
||||
<div className="main-panel"> |
|
||||
<div className="view-container"> |
|
||||
{this.props.children} |
|
||||
</div> |
|
||||
</div> |
|
||||
<aside className="right-side-panel"> |
|
||||
<TransactionsMonitorContainer/> |
|
||||
</aside> |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const mapStateToProps = state => { |
|
||||
return { |
|
||||
isProgressBarVisible: state.interface.displayProgressBar |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default drizzleConnect(CoreLayout, mapStateToProps) |
|
@ -1,54 +0,0 @@ |
|||||
//Action creators
|
|
||||
|
|
||||
export const INIT_TRANSACTION = 'INIT_TRANSACTION'; |
|
||||
export const UPDATE_TRANSACTION = 'UPDATE_TRANSACTION'; |
|
||||
|
|
||||
export function updateUsername(newUsername, callback){ |
|
||||
return { |
|
||||
type: INIT_TRANSACTION, |
|
||||
transactionDescriptor: |
|
||||
{ |
|
||||
contract: 'Forum', |
|
||||
method: 'updateUsername', |
|
||||
params: [newUsername], |
|
||||
event: 'UsernameUpdated' |
|
||||
}, |
|
||||
callback: callback |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export function createTopic(userInputs){ |
|
||||
return { |
|
||||
type: INIT_TRANSACTION, |
|
||||
transactionDescriptor: |
|
||||
{ |
|
||||
contract: 'Forum', |
|
||||
method: 'createTopic', |
|
||||
params: [], |
|
||||
event: 'TopicCreated' |
|
||||
}, |
|
||||
userInputs: userInputs |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export function createPost(topicID, userInputs){ |
|
||||
return { |
|
||||
type: INIT_TRANSACTION, |
|
||||
transactionDescriptor: |
|
||||
{ |
|
||||
contract: 'Forum', |
|
||||
method: 'createPost', |
|
||||
params: [topicID], |
|
||||
event: 'PostCreated' |
|
||||
}, |
|
||||
userInputs: userInputs |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export function updateTransaction(transactionIndex, updateDescriptor){ |
|
||||
return { |
|
||||
type: UPDATE_TRANSACTION, |
|
||||
index: transactionIndex, |
|
||||
transactionUpdates: updateDescriptor |
|
||||
}; |
|
||||
} |
|
@ -1,20 +0,0 @@ |
|||||
//Action creators
|
|
||||
|
|
||||
export const SHOW_PROGRESS_BAR = 'SHOW_PROGRESS_BAR'; |
|
||||
export const HIDE_PROGRESS_BAR = 'HIDE_PROGRESS_BAR'; |
|
||||
export const SET_NAVBAR_TITLE = 'SET_NAVBAR_TITLE'; |
|
||||
|
|
||||
export function showProgressBar(){ |
|
||||
return { type: SHOW_PROGRESS_BAR}; |
|
||||
} |
|
||||
|
|
||||
export function hideProgressBar(){ |
|
||||
return { type: HIDE_PROGRESS_BAR}; |
|
||||
} |
|
||||
|
|
||||
export function setNavBarTitle(newTitle){ |
|
||||
return { |
|
||||
type: SET_NAVBAR_TITLE, |
|
||||
title: newTitle |
|
||||
}; |
|
||||
} |
|
@ -1,16 +0,0 @@ |
|||||
const initialState = { |
|
||||
grabbed: false |
|
||||
}; |
|
||||
|
|
||||
const contractReducer = (state = initialState, action) => { |
|
||||
switch (action.type) { |
|
||||
case 'CONTRACT_GRABBED': |
|
||||
return { |
|
||||
grabbed: true, |
|
||||
}; |
|
||||
default: |
|
||||
return state |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default contractReducer; |
|
@ -1,21 +0,0 @@ |
|||||
import { combineReducers } from 'redux'; |
|
||||
import { routerReducer } from 'react-router-redux'; |
|
||||
import { drizzleReducers } from 'drizzle'; |
|
||||
import userReducer from "./userReducer"; |
|
||||
import contractReducer from "./contractReducer"; |
|
||||
import orbitReducer from "../../util/orbitReducer"; |
|
||||
import userInterfaceReducer from "./userInterfaceReducer"; |
|
||||
import transactionsMonitorReducer from "./transactionsMonitorReducer"; |
|
||||
|
|
||||
|
|
||||
const reducer = combineReducers({ |
|
||||
routing: routerReducer, |
|
||||
user: userReducer, |
|
||||
orbitDB: orbitReducer, |
|
||||
forumContract: contractReducer, |
|
||||
interface: userInterfaceReducer, |
|
||||
transactionsQueue: transactionsMonitorReducer, |
|
||||
...drizzleReducers |
|
||||
}); |
|
||||
|
|
||||
export default reducer; |
|
@ -1,39 +0,0 @@ |
|||||
import { INIT_TRANSACTION, UPDATE_TRANSACTION } from '../actions/transactionsMonitorActions'; |
|
||||
|
|
||||
const initialState = { |
|
||||
transactions: [] |
|
||||
}; |
|
||||
|
|
||||
const transactionsReducer = (state = initialState, action) => { |
|
||||
switch (action.type) { |
|
||||
case INIT_TRANSACTION: |
|
||||
let transactionsShallowCopy = state.transactions.slice(); |
|
||||
transactionsShallowCopy.push({ |
|
||||
status: 'initialized', |
|
||||
contract: action.transactionDescriptor.contract, |
|
||||
method: action.transactionDescriptor.method, |
|
||||
params: action.transactionDescriptor.params, |
|
||||
event: action.transactionDescriptor.event, |
|
||||
returnData: null, |
|
||||
userInputs: action.userInputs |
|
||||
}); |
|
||||
return { |
|
||||
transactions: transactionsShallowCopy |
|
||||
}; |
|
||||
case UPDATE_TRANSACTION: |
|
||||
return { transactions: state.transactions.map( (transaction, index) => { |
|
||||
if (index !== action.index){ |
|
||||
return transaction; |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
...transaction, |
|
||||
...action.transactionUpdates |
|
||||
} |
|
||||
})}; |
|
||||
default: |
|
||||
return state; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default transactionsReducer; |
|
@ -1,31 +0,0 @@ |
|||||
import { |
|
||||
SHOW_PROGRESS_BAR, |
|
||||
HIDE_PROGRESS_BAR, |
|
||||
SET_NAVBAR_TITLE |
|
||||
} from '../actions/userInterfaceActions'; |
|
||||
|
|
||||
const initialState = { |
|
||||
displayProgressBar: false, |
|
||||
navBarTitle: '' |
|
||||
}; |
|
||||
|
|
||||
const userInterfaceReducer = (state = initialState, action) => { |
|
||||
switch (action.type) { |
|
||||
case SHOW_PROGRESS_BAR: |
|
||||
return { |
|
||||
displayProgressBar: true |
|
||||
}; |
|
||||
case HIDE_PROGRESS_BAR: |
|
||||
return { |
|
||||
displayProgressBar: false |
|
||||
}; |
|
||||
case SET_NAVBAR_TITLE: |
|
||||
return { |
|
||||
navBarTitle: action.title |
|
||||
} |
|
||||
default: |
|
||||
return state; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default userInterfaceReducer; |
|
@ -1,19 +0,0 @@ |
|||||
import { put, takeLatest } from 'redux-saga/effects' |
|
||||
|
|
||||
let contractGrabbed=false; |
|
||||
let grabbedContract; |
|
||||
|
|
||||
function* grabContract({contract}) { |
|
||||
if(!contractGrabbed) |
|
||||
{ |
|
||||
contractGrabbed=true; |
|
||||
grabbedContract = contract; |
|
||||
yield put({type: 'CONTRACT_GRABBED', ...[]}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function* contractSaga() { |
|
||||
yield takeLatest('LISTEN_FOR_EVENT', grabContract); |
|
||||
} |
|
||||
|
|
||||
export { contractSaga, grabbedContract }; |
|
@ -1,70 +0,0 @@ |
|||||
import { call, put, select, takeLatest, takeEvery } from 'redux-saga/effects' |
|
||||
import {grabbedContract as contract} from "./contractSaga"; |
|
||||
|
|
||||
const contractWasGrabbed = (state) => state.forumContract.grabbed; |
|
||||
const accounts = (state) => state.accounts; |
|
||||
let account; |
|
||||
|
|
||||
let initFlag = false; |
|
||||
|
|
||||
|
|
||||
function* initUser() { |
|
||||
if(!initFlag) |
|
||||
{ |
|
||||
while(true) |
|
||||
if(yield select(contractWasGrabbed)) |
|
||||
{ |
|
||||
yield call(getUserData); |
|
||||
initFlag=true; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
function* updateUserData() { |
|
||||
if(initFlag) |
|
||||
yield call(getUserData); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
function* getUserData() { |
|
||||
const currentAccount = (yield select(accounts))[0]; |
|
||||
if(currentAccount!==account) |
|
||||
{ |
|
||||
account = currentAccount; |
|
||||
yield put({type: 'ACCOUNT_CHANGED', ...[]}); |
|
||||
} |
|
||||
const txObj1 = yield call(contract.methods["hasUserSignedUp"], ...[account]); |
|
||||
try { |
|
||||
const callResult = yield call(txObj1.call, {address:account}); |
|
||||
if(callResult) |
|
||||
{ |
|
||||
const txObj2 = yield call(contract.methods["getUsername"], ...[account]); |
|
||||
const username = yield call(txObj2.call, {address:account}); |
|
||||
const dispatchArgs = { |
|
||||
address: account, |
|
||||
username: username |
|
||||
}; |
|
||||
yield put({type: 'USER_HAS_SIGNED_UP', ...dispatchArgs}); |
|
||||
} |
|
||||
else{ |
|
||||
const dispatchArgs = { |
|
||||
address: account, |
|
||||
}; |
|
||||
yield put({type: 'USER_IS_GUEST', ...dispatchArgs}); |
|
||||
} |
|
||||
} |
|
||||
catch (error) { |
|
||||
console.error(error); |
|
||||
yield put({type: 'USER_FETCHING_ERROR', ...[]}) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
function* userSaga() { |
|
||||
yield takeLatest("DRIZZLE_INITIALIZED", initUser); |
|
||||
yield takeEvery("ACCOUNTS_FETCHED", updateUserData); |
|
||||
} |
|
||||
|
|
||||
export default userSaga; |
|
@ -1,33 +0,0 @@ |
|||||
import { browserHistory } from 'react-router'; |
|
||||
import { createStore, applyMiddleware, compose } from 'redux'; |
|
||||
import { routerMiddleware } from 'react-router-redux'; |
|
||||
import reducer from './reducer/reducer'; |
|
||||
import rootSaga from './sagas/rootSaga'; |
|
||||
import createSagaMiddleware from 'redux-saga'; |
|
||||
import { generateContractsInitialState } from 'drizzle'; |
|
||||
import drizzleOptions from '../util/drizzleOptions'; |
|
||||
|
|
||||
// Redux DevTools
|
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; |
|
||||
|
|
||||
const routingMiddleware = routerMiddleware(browserHistory); |
|
||||
const sagaMiddleware = createSagaMiddleware(); |
|
||||
|
|
||||
const initialState = { |
|
||||
contracts: generateContractsInitialState(drizzleOptions) |
|
||||
}; |
|
||||
|
|
||||
const store = createStore( |
|
||||
reducer, |
|
||||
initialState, |
|
||||
composeEnhancers( |
|
||||
applyMiddleware( |
|
||||
routingMiddleware, |
|
||||
sagaMiddleware |
|
||||
) |
|
||||
) |
|
||||
); |
|
||||
|
|
||||
sagaMiddleware.run(rootSaga); |
|
||||
|
|
||||
export default store; |
|
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 216 KiB |
@ -1,25 +0,0 @@ |
|||||
import Forum from './../build/contracts/Forum.json' |
|
||||
|
|
||||
const drizzleOptions = { |
|
||||
web3: { |
|
||||
fallback: { |
|
||||
type: 'ws', |
|
||||
url: 'ws://127.0.0.1:8545' |
|
||||
} |
|
||||
}, |
|
||||
contracts: [ |
|
||||
Forum |
|
||||
], |
|
||||
events: { |
|
||||
Forum: ['UserSignedUp', |
|
||||
'UsernameUpdated', |
|
||||
'TopicCreated', |
|
||||
'PostCreated'] |
|
||||
}, |
|
||||
polls: { |
|
||||
accounts: 3000, |
|
||||
blocks: 3000 |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
export default drizzleOptions; |
|
@ -1,81 +0,0 @@ |
|||||
import IPFS from 'ipfs'; |
|
||||
import OrbitDB from 'orbit-db'; |
|
||||
import Keystore from 'orbit-db-keystore'; |
|
||||
import path from 'path'; |
|
||||
import store from './../redux/store'; |
|
||||
|
|
||||
// OrbitDB uses Pubsub which is an experimental feature
|
|
||||
// and need to be turned on manually.
|
|
||||
// Note that these options need to be passed to IPFS in
|
|
||||
// all examples in this document even if not specified so.
|
|
||||
const ipfsOptions = { |
|
||||
EXPERIMENTAL: { |
|
||||
pubsub: true |
|
||||
}, config: { |
|
||||
Addresses: { |
|
||||
Swarm: [ |
|
||||
// Use IPFS dev signal server
|
|
||||
// Prefer websocket over webrtc
|
|
||||
//
|
|
||||
// Websocket:
|
|
||||
// '/dns4/ws-star-signal-2.servep2p.com/tcp/443//wss/p2p-websocket-star',
|
|
||||
'/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star', |
|
||||
// Local signal server
|
|
||||
//'/ip4/127.0.0.1/tcp/4711/ws/p2p-websocket-star'
|
|
||||
//
|
|
||||
// WebRTC:
|
|
||||
// '/dns4/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star',
|
|
||||
// Local signal server
|
|
||||
// '/ip4/127.0.0.1/tcp/1337/ws/p2p-webrtc-star'
|
|
||||
] |
|
||||
} |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
// Create IPFS instance
|
|
||||
const ipfs = new IPFS(ipfsOptions); |
|
||||
let orbitdb, topicsDB, postsDB; |
|
||||
|
|
||||
ipfs.on('ready', async () => { |
|
||||
store.dispatch({type: "IPFS_INITIALIZED"}); |
|
||||
}); |
|
||||
|
|
||||
async function createDatabases() { |
|
||||
orbitdb = new OrbitDB(ipfs); |
|
||||
topicsDB = await orbitdb.keyvalue('topics'); |
|
||||
postsDB = await orbitdb.keyvalue('posts'); |
|
||||
store.dispatch({ |
|
||||
type: "DATABASES_CREATED", |
|
||||
orbitdb: orbitdb, |
|
||||
topicsDB: topicsDB, |
|
||||
postsDB: postsDB, |
|
||||
id: orbitdb.id |
|
||||
}); |
|
||||
return {id: orbitdb.id, topicsDB: topicsDB.address.root, postsDB: postsDB.address.root, |
|
||||
publicKey: orbitdb.key.getPublic('hex'), privateKey:orbitdb.key.getPrivate('hex')}; |
|
||||
} |
|
||||
|
|
||||
async function loadDatabases(id,mTopicsDB, mPostsDB,publicKey,privateKey) { |
|
||||
let directory = "./orbitdb"; |
|
||||
let keystore = Keystore.create(path.join(directory, id, '/keystore')); |
|
||||
keystore._storage.setItem(id, JSON.stringify({ |
|
||||
publicKey: publicKey, |
|
||||
privateKey: privateKey |
|
||||
})); |
|
||||
orbitdb = new OrbitDB(ipfs,directory,{peerId:id, keystore:keystore}); |
|
||||
topicsDB = await orbitdb.keyvalue('/orbitdb/' + mTopicsDB +'/topics'); |
|
||||
postsDB = await orbitdb.keyvalue('/orbitdb/' + mPostsDB +'/posts'); |
|
||||
|
|
||||
topicsDB.load(); |
|
||||
postsDB.load(); |
|
||||
|
|
||||
store.dispatch({ |
|
||||
type: "DATABASES_LOADED", |
|
||||
orbitdb: orbitdb, |
|
||||
topicsDB: topicsDB, |
|
||||
postsDB: postsDB, |
|
||||
id: orbitdb.id |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
export { createDatabases, loadDatabases }; |
|
@ -1,49 +0,0 @@ |
|||||
const initialState = { |
|
||||
ipfsInitialized: false, |
|
||||
ready: false, |
|
||||
orbitdb: null, |
|
||||
topicsDB: null, |
|
||||
postsDB: null, |
|
||||
id: null |
|
||||
}; |
|
||||
|
|
||||
const orbitReducer = (state = initialState, action) => { |
|
||||
switch (action.type) { |
|
||||
case 'IPFS_INITIALIZED': |
|
||||
return { |
|
||||
...state, |
|
||||
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 |
|
||||
}; |
|
||||
default: |
|
||||
return state |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default orbitReducer; |
|
@ -1,42 +0,0 @@ |
|||||
import { loadDatabases } from './../util/orbit' |
|
||||
import { call, put, select, takeLatest } from 'redux-saga/effects' |
|
||||
import {grabbedContract as contract} from "../redux/sagas/contractSaga"; |
|
||||
|
|
||||
const accounts = (state) => state.accounts; |
|
||||
|
|
||||
let latestAccount; |
|
||||
|
|
||||
function* getOrbitDBInfo() { |
|
||||
yield put({type: 'ORRBIT_GETTING_INFO', ...[]}); |
|
||||
const account = (yield select(accounts))[0]; |
|
||||
if(account!==latestAccount) |
|
||||
{ |
|
||||
console.log("Deleting local storage.."); |
|
||||
localStorage.clear(); |
|
||||
const txObj1 = yield call(contract.methods["hasUserSignedUp"], ...[account]); |
|
||||
try { |
|
||||
const callResult = yield call(txObj1.call, {address:account}); |
|
||||
if(callResult) { |
|
||||
const txObj2 = yield call(contract.methods["getOrbitDBInfo"], ...[account]); |
|
||||
const info = yield call(txObj2.call, {address: account}); |
|
||||
//TODO: update localStorage OrbitDB stuff
|
|
||||
yield call(loadDatabases, info[0], info[1], info[2],info[3], info[4]); |
|
||||
} |
|
||||
else |
|
||||
yield put({type: 'DATABASES_NOT_READY', ...[]}); |
|
||||
|
|
||||
latestAccount=account; |
|
||||
} |
|
||||
catch (error) { |
|
||||
console.error(error); |
|
||||
yield put({type: 'ORBIT_SAGA_ERROR', ...[]}); |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function* orbitSaga() { |
|
||||
yield takeLatest("ACCOUNT_CHANGED", getOrbitDBInfo); |
|
||||
} |
|
||||
|
|
||||
export default orbitSaga; |
|
@ -1,62 +0,0 @@ |
|||||
pragma solidity ^0.4.17; |
|
||||
|
|
||||
import "truffle/Assert.sol"; |
|
||||
import "truffle/DeployedAddresses.sol"; |
|
||||
import "../contracts/Forum.sol"; |
|
||||
|
|
||||
contract TestForum { |
|
||||
Forum forumContract = Forum(DeployedAddresses.Forum()); |
|
||||
|
|
||||
function testUserCanSignUp() public { |
|
||||
//Try to sign up |
|
||||
bool expected = true; |
|
||||
bool userSignUpStatus = forumContract.signUp("MrAwesome"); |
|
||||
Assert.equal(userSignUpStatus, expected, "Sign-up failed"); |
|
||||
} |
|
||||
|
|
||||
function testHasUserSignedUp() public { |
|
||||
//Check if sign-up succeeded |
|
||||
address myAddress = this; |
|
||||
require(forumContract.hasUserSignedUp(myAddress)); |
|
||||
} |
|
||||
|
|
||||
/* function testGetUsername() public { |
|
||||
//require (forumContract.getUsername(this) == "MrAwesome"); |
|
||||
} */ |
|
||||
|
|
||||
function testGetUserAddress() public { |
|
||||
//Try to get user address from user-name |
|
||||
address expected = this; |
|
||||
address userAddress = forumContract.getUserAddress("MrAwesome"); |
|
||||
Assert.equal(userAddress, expected, "Getting user address from user-name failed"); |
|
||||
} |
|
||||
|
|
||||
function testIsUserNameTaken() public view { |
|
||||
//Try to test if a user-name is taken |
|
||||
bool expected = false; |
|
||||
bool result = forumContract.isUserNameTaken("somethingElse"); |
|
||||
Assert.equal(result, expected, "Testing if user-name is taken failed"); |
|
||||
|
|
||||
/* expected = true; |
|
||||
result = forumContract.isUserNameTaken("MrAwesome"); |
|
||||
Assert.equal(result, expected, "Testing if user-name is taken failed"); */ |
|
||||
} |
|
||||
|
|
||||
/* function testCreateTopic() public { |
|
||||
uint expected = 1; |
|
||||
uint topicId = forumContract.createTopic(); |
|
||||
Assert.equal(topicId, expected, "whatevs"); |
|
||||
} |
|
||||
|
|
||||
function testCreatePost() public { |
|
||||
uint expected = 1; |
|
||||
uint postId = forumContract.createPost(1); |
|
||||
Assert.equal(postId, expected, "whatevs"); |
|
||||
} */ |
|
||||
|
|
||||
/* function testGetTopicPosts() public { |
|
||||
} */ |
|
||||
|
|
||||
/* function test () public { |
|
||||
} */ |
|
||||
} |
|
Loading…
Reference in new issue