Browse Source

init

develop
Ezerous 6 years ago
parent
commit
ee681534e7
No known key found for this signature in database GPG Key ID: 262B2954BBA319E3
  1. 9
      .gitignore
  2. 37
      app/package.json
  3. 0
      app/public/favicon.ico
  4. 38
      app/public/index.html
  5. 15
      app/public/manifest.json
  6. 17
      app/src/components/AppComponent.js
  7. 20
      app/src/config/drizzleOptions.js
  8. 14
      app/src/containers/AppContainer.js
  9. 23
      app/src/index.js
  10. 10
      app/src/redux/reducers/reducer.js
  11. 0
      app/src/redux/reducers/userReducer.js
  12. 4
      app/src/redux/sagas/rootSaga.js
  13. 66
      app/src/redux/sagas/userSaga.js
  14. 30
      app/src/redux/store.js
  15. 135
      app/src/utils/serviceWorker.js
  16. 156
      contracts/Forum.sol
  17. 12
      contracts/Migrations.sol
  18. 37
      package.json
  19. 44
      public/index.html
  20. 186
      src/assets/css/App.css
  21. 30
      src/assets/css/board-container.css
  22. 4
      src/assets/css/index.css
  23. 17
      src/assets/css/loading-container.css
  24. 5
      src/assets/css/profile-container.css
  25. 108
      src/assets/css/progress-bar.css
  26. 12
      src/assets/css/sign-up-container.css
  27. 6
      src/assets/css/start-topic-container.css
  28. 51
      src/assets/css/topic-container.css
  29. 3271
      src/assets/fonts/fontawesome-free-5.0.13/fontawesome-all.js
  30. 14
      src/components/FloatingButton.js
  31. 16
      src/components/LoadingSpinner.js
  32. 63
      src/components/NavBar.js
  33. 175
      src/components/NewPost.js
  34. 65
      src/components/NewTopicPreview.js
  35. 11
      src/components/NotFoundView.js
  36. 173
      src/components/Post.js
  37. 36
      src/components/PostList.js
  38. 59
      src/components/ProfileInformation.js
  39. 100
      src/components/Topic.js
  40. 30
      src/components/TopicList.js
  41. 80
      src/components/WithBlockchainData.js
  42. 117
      src/containers/BoardContainer.js
  43. 35
      src/containers/HomeContainer.js
  44. 88
      src/containers/LoadingContainer.js
  45. 195
      src/containers/ProfileContainer.js
  46. 50
      src/containers/SignUpContainer.js
  47. 139
      src/containers/StartTopicContainer.js
  48. 153
      src/containers/TopicContainer.js
  49. 199
      src/containers/TransactionsMonitorContainer.js
  50. 151
      src/containers/UsernameFormContainer.js
  51. 12
      src/helpers/EpochTimeConverter.js
  52. 52
      src/index.js
  53. 53
      src/layouts/CoreLayout/CoreLayout.js
  54. 54
      src/redux/actions/transactionsMonitorActions.js
  55. 20
      src/redux/actions/userInterfaceActions.js
  56. 16
      src/redux/reducer/contractReducer.js
  57. 21
      src/redux/reducer/reducer.js
  58. 39
      src/redux/reducer/transactionsMonitorReducer.js
  59. 31
      src/redux/reducer/userInterfaceReducer.js
  60. 19
      src/redux/sagas/contractSaga.js
  61. 70
      src/redux/sagas/userSaga.js
  62. 33
      src/redux/store.js
  63. BIN
      src/resources/PageNotFound.jpg
  64. BIN
      src/resources/ipfs_logo.png
  65. BIN
      src/resources/logo.png
  66. 25
      src/util/drizzleOptions.js
  67. 81
      src/util/orbit.js
  68. 49
      src/util/orbitReducer.js
  69. 42
      src/util/orbitSaga.js
  70. 62
      test/TestForum.sol
  71. 8
      truffle-config.js

9
.gitignore

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

37
app/package.json

@ -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"
]
}

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

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

38
app/public/index.html

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

15
app/public/manifest.json

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

17
app/src/components/AppComponent.js

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

20
app/src/config/drizzleOptions.js

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

14
app/src/containers/AppContainer.js

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

23
app/src/index.js

@ -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();

10
app/src/redux/reducers/reducer.js

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

0
src/redux/reducer/userReducer.js → app/src/redux/reducers/userReducer.js

4
src/redux/sagas/rootSaga.js → app/src/redux/sagas/rootSaga.js

@ -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))
) )

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

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

30
app/src/redux/store.js

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

135
app/src/utils/serviceWorker.js

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

156
contracts/Forum.sol

@ -1,11 +1,10 @@
pragma solidity ^0.4.25; pragma solidity ^0.5.4;
contract Forum { contract Forum {
//----------------------------------------USER---------------------------------------- //----------------------------------------USER----------------------------------------
struct User { struct User {
string username; // TODO: set an upper bound instead of arbitrary string string username;
OrbitDB orbitdb;
uint[] topicIDs; // IDs of the topics the user created uint[] topicIDs; // IDs of the topics the user created
uint[] postIDs; // IDs of the posts the user created uint[] postIDs; // IDs of the posts the user created
uint timestamp; uint timestamp;
@ -18,18 +17,17 @@ contract Forum {
event UserSignedUp(string username, address userAddress); event UserSignedUp(string username, address userAddress);
event UsernameUpdated(string newName, string oldName,address userAddress); event UsernameUpdated(string newName, string oldName,address userAddress);
function signUp(string username, string orbitDBId, string orbitTopicsDB, string orbitPostsDB, string orbitPublicKey, string orbitPrivateKey) public returns (bool) { function signUp(string memory username) public returns (bool) {
require (!hasUserSignedUp(msg.sender), "User has already signed up."); require (!hasUserSignedUp(msg.sender), "User has already signed up.");
require(!isUserNameTaken(username), "Username is already taken."); require(!isUserNameTaken(username), "Username is already taken.");
users[msg.sender] = User(username, users[msg.sender] = User(username,
OrbitDB(orbitDBId,orbitTopicsDB, orbitPostsDB, orbitPublicKey, orbitPrivateKey),
new uint[](0), new uint[](0), block.timestamp, true); new uint[](0), new uint[](0), block.timestamp, true);
userAddresses[username] = msg.sender; userAddresses[username] = msg.sender;
emit UserSignedUp(username, msg.sender); emit UserSignedUp(username, msg.sender);
return true; return true;
} }
function updateUsername(string newUsername) public returns (bool) { function updateUsername(string memory newUsername) public returns (bool) {
require (hasUserSignedUp(msg.sender), "User hasn't signed up yet."); require (hasUserSignedUp(msg.sender), "User hasn't signed up yet.");
require(!isUserNameTaken(newUsername), "Username is already taken."); require(!isUserNameTaken(newUsername), "Username is already taken.");
string memory oldUsername = getUsername(msg.sender); string memory oldUsername = getUsername(msg.sender);
@ -40,12 +38,12 @@ contract Forum {
return true; return true;
} }
function getUsername(address userAddress) public view returns (string) { function getUsername(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
return users[userAddress].username; return users[userAddress].username;
} }
function getUserAddress(string username) public view returns (address) { function getUserAddress(string memory username) public view returns (address) {
return userAddresses[username]; return userAddresses[username];
} }
@ -53,155 +51,15 @@ contract Forum {
return users[userAddress].signedUp; return users[userAddress].signedUp;
} }
function isUserNameTaken(string username) public view returns (bool) { function isUserNameTaken(string memory username) public view returns (bool) {
if (getUserAddress(username)!=address(0)) if (getUserAddress(username)!=address(0))
return true; return true;
return false; return false;
} }
function getUserTopics(address userAddress) public view returns (uint[]) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
return users[userAddress].topicIDs;
}
function getUserPosts(address userAddress) public view returns (uint[]) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
return users[userAddress].postIDs;
}
function getUserDateOfRegister(address userAddress) public view returns (uint) { function getUserDateOfRegister(address userAddress) public view returns (uint) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); require (hasUserSignedUp(userAddress), "User hasn't signed up yet.");
return users[userAddress].timestamp; return users[userAddress].timestamp;
} }
//----------------------------------------OrbitDB----------------------------------------
struct OrbitDB {
string id; // TODO: set an upper bound instead of arbitrary string
string topicsDB; //TODO: not sure yet which of these are actually needed
string postsDB;
string publicKey;
string privateKey;
}
function getOrbitDBId(address userAddress) public view returns (string) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.id;
}
function getOrbitTopicsDB(address userAddress) public view returns (string) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.topicsDB;
}
function getOrbitPostsDB(address userAddress) public view returns (string) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.postsDB;
}
function getOrbitPublicKey(address userAddress) public view returns (string) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.publicKey;
}
//TODO: encrypt using Metamask in the future
function getOrbitPrivateKey(address userAddress) public view returns (string) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return users[userAddress].orbitdb.privateKey;
}
function getOrbitDBInfo(address userAddress) public view returns (string, string, string, string, string) {
require (hasUserSignedUp(userAddress), "User hasn't signed up.");
return (
users[userAddress].orbitdb.id,
users[userAddress].orbitdb.topicsDB,
users[userAddress].orbitdb.postsDB,
users[userAddress].orbitdb.publicKey,
users[userAddress].orbitdb.privateKey
);
}
//----------------------------------------POSTING----------------------------------------
struct Topic {
uint topicID;
address author;
uint timestamp;
uint[] postIDs;
}
struct Post {
uint postID;
address author;
uint timestamp;
uint topicID;
}
uint numTopics; // Total number of topics
uint numPosts; // Total number of posts
mapping (uint => Topic) topics;
mapping (uint => Post) posts;
event TopicCreated(uint topicID, uint postID);
event PostCreated(uint postID, uint topicID);
function createTopic() public returns (uint, uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create topics
//Creates topic
uint topicID = numTopics++;
topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0));
users[msg.sender].topicIDs.push(topicID);
//Adds first post to topic
uint postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp, topicID);
topics[topicID].postIDs.push(postID);
users[msg.sender].postIDs.push(postID);
emit TopicCreated(topicID, postID);
return (topicID, postID);
}
function createPost(uint topicID) public returns (uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create posts
require(topicID<numTopics); // Only allow posting to a topic that exists
uint postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp, topicID);
topics[topicID].postIDs.push(postID);
users[msg.sender].postIDs.push(postID);
emit PostCreated(postID, topicID);
return postID;
}
function getNumberOfTopics() public view returns (uint) {
return numTopics;
}
function getTopic(uint topicID) public view returns (string, address, string, uint, uint[]) {
//require(hasUserSignedUp(msg.sender)); needed?
require(topicID<numTopics);
return (getOrbitTopicsDB(topics[topicID].author),
topics[topicID].author,
users[topics[topicID].author].username,
topics[topicID].timestamp,
topics[topicID].postIDs
);
}
function getTopicPosts(uint topicID) public view returns (uint[]) {
require(topicID<numTopics); // Topic should exist
return topics[topicID].postIDs;
}
function getPost(uint postID) public view returns (string, address, string, uint, uint) {
//require(hasUserSignedUp(msg.sender)); needed?
require(postID<numPosts);
return (getOrbitPostsDB(posts[postID].author),
posts[postID].author,
users[posts[postID].author].username,
posts[postID].timestamp,
posts[postID].topicID
);
}
} }

12
contracts/Migrations.sol

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

37
package.json

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

44
public/index.html

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

186
src/assets/css/App.css

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

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

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

4
src/assets/css/index.css

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

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

14
src/components/FloatingButton.js

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

16
src/components/LoadingSpinner.js

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

63
src/components/NavBar.js

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

175
src/components/NewPost.js

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

65
src/components/NewTopicPreview.js

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

11
src/components/NotFoundView.js

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

173
src/components/Post.js

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

36
src/components/PostList.js

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

59
src/components/ProfileInformation.js

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

100
src/components/Topic.js

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

30
src/components/TopicList.js

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

80
src/components/WithBlockchainData.js

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

117
src/containers/BoardContainer.js

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

35
src/containers/HomeContainer.js

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

88
src/containers/LoadingContainer.js

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

195
src/containers/ProfileContainer.js

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

50
src/containers/SignUpContainer.js

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

139
src/containers/StartTopicContainer.js

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

153
src/containers/TopicContainer.js

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

199
src/containers/TransactionsMonitorContainer.js

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

151
src/containers/UsernameFormContainer.js

@ -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)

12
src/helpers/EpochTimeConverter.js

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

52
src/index.js

@ -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')
);

53
src/layouts/CoreLayout/CoreLayout.js

@ -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)

54
src/redux/actions/transactionsMonitorActions.js

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

20
src/redux/actions/userInterfaceActions.js

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

16
src/redux/reducer/contractReducer.js

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

21
src/redux/reducer/reducer.js

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

39
src/redux/reducer/transactionsMonitorReducer.js

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

31
src/redux/reducer/userInterfaceReducer.js

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

19
src/redux/sagas/contractSaga.js

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

70
src/redux/sagas/userSaga.js

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

33
src/redux/store.js

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

BIN
src/resources/PageNotFound.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

BIN
src/resources/ipfs_logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src/resources/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

25
src/util/drizzleOptions.js

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

81
src/util/orbit.js

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

49
src/util/orbitReducer.js

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

42
src/util/orbitSaga.js

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

62
test/TestForum.sol

@ -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 {
} */
}

8
truffle.js → truffle-config.js

@ -1,7 +1,9 @@
const path = require("path");
module.exports = { module.exports = {
// See <http://truffleframework.com/docs/advanced/configuration> // See <http://truffleframework.com/docs/advanced/configuration>
// for more about customizing your Truffle configuration! // to customize your Truffle configuration!
contracts_build_directory: path.join(__dirname, "src/build/contracts"), contracts_build_directory: path.join(__dirname, "app/src/contracts"),
networks: { networks: {
development: { development: {
host: "localhost", host: "localhost",
@ -11,7 +13,7 @@ module.exports = {
}, },
compilers:{ compilers:{
solc: { solc: {
version: "0.4.25", version: "0.5.4",
settings:{ settings:{
optimizer: { optimizer: {
enabled: true, enabled: true,
Loading…
Cancel
Save