Browse Source

Merge branch 'develop' into 'master'

fix: add missing concordia prefix to package name directory, account for...

See merge request ecentrics/concordia!27
master
Apostolos Fanakis 3 years ago
parent
commit
3333e7dd63
  1. 25
      .dockerignore
  2. 9
      .gitattributes
  3. 36
      .gitignore
  4. 21
      LICENSE
  5. 19
      README.md
  6. 88
      contracts/Forum.sol
  7. 23
      contracts/Migrations.sol
  8. 95
      docker/Makefile
  9. 262
      docker/README.md
  10. 97
      docker/concordia-app/Dockerfile
  11. 14
      docker/concordia-app/create-environment.sh
  12. 22
      docker/concordia-app/nginx.conf
  13. 3
      docker/concordia-app/run.sh
  14. 11
      docker/concordia-app/test-app.sh
  15. 36
      docker/concordia-contracts-provider/Dockerfile
  16. 70
      docker/concordia-contracts/Dockerfile
  17. 8
      docker/concordia-contracts/migrate.sh
  18. 22
      docker/concordia-contracts/test-contracts.sh
  19. 34
      docker/concordia-pinner/Dockerfile
  20. 33
      docker/docker-compose.yml
  21. 5
      docker/env/concordia.env
  22. 3
      docker/env/contracts-provider.env
  23. 4
      docker/env/contracts-test.env
  24. 8
      docker/env/contracts.env
  25. 5
      docker/env/ganache.env
  26. 6
      docker/env/ganache.test.env
  27. 17
      docker/env/pinner.env
  28. 10
      docker/ganache/Dockerfile
  29. 37
      docker/ganache/start-blockchain.sh
  30. 781
      jenkins/Jenkinsfile
  31. 36
      jenkins/check_package_changed.sh
  32. 16
      jenkins/env/concordia.production.jenkins.env
  33. 16
      jenkins/env/concordia.staging.jenkins.env
  34. 8
      jenkins/env/contracts.production.jenkins.env
  35. 9
      jenkins/env/contracts.provider.production.env
  36. 9
      jenkins/env/contracts.provider.staging.env
  37. 8
      jenkins/env/contracts.staging.jenkins.env
  38. 4
      jenkins/env/contracts.test.jenkins.env
  39. 10
      jenkins/env/ganache.production.jenkins.env
  40. 10
      jenkins/env/ganache.staging.jenkins.env
  41. 6
      jenkins/env/ganache.test.jenkins.env
  42. 22
      jenkins/env/pinner.production.jenkins.env
  43. 22
      jenkins/env/pinner.staging.jenkins.env
  44. 5
      jenkins/env/rendezvous.jenkins.env
  45. 18
      jenkins/hash_build_properties.sh
  46. 5
      jenkins/map_to_thousand.sh
  47. 5
      migrations/1_initial_migration.js
  48. 5
      migrations/2_deploy_contracts.js
  49. 34
      package.json
  50. 0
      packages/concordia-app/.dockerignore
  51. 11
      packages/concordia-app/.env.development.example
  52. 60
      packages/concordia-app/.eslintrc.js
  53. 22
      packages/concordia-app/.gitignore
  54. 36
      packages/concordia-app/CONCORDIA.md
  55. 71
      packages/concordia-app/package.json
  56. 15
      packages/concordia-app/patches/web3-eth+1.3.4.patch
  57. 0
      packages/concordia-app/public/environment.js
  58. BIN
      packages/concordia-app/public/favicon.ico
  59. 16
      packages/concordia-app/public/index.html
  60. 106
      packages/concordia-app/public/locales/en/translation.json
  61. 6
      packages/concordia-app/public/manifest.json
  62. 2
      packages/concordia-app/public/robots.txt
  63. 25
      packages/concordia-app/src/App.jsx
  64. 22
      packages/concordia-app/src/ErrorBoundary.jsx
  65. 95
      packages/concordia-app/src/Routes.jsx
  66. 35
      packages/concordia-app/src/assets/About.md
  67. 72
      packages/concordia-app/src/assets/css/index.css
  68. BIN
      packages/concordia-app/src/assets/images/PageNotFound.jpg
  69. 1
      packages/concordia-app/src/assets/images/app_logo.svg
  70. 1
      packages/concordia-app/src/assets/images/app_logo_circle.svg
  71. 1
      packages/concordia-app/src/assets/images/ethereum_logo.svg
  72. 20
      packages/concordia-app/src/assets/images/ipfs_logo.svg
  73. 1
      packages/concordia-app/src/assets/images/metamask_logo.svg
  74. 1
      packages/concordia-app/src/assets/images/orbitdb_logo.svg
  75. 32
      packages/concordia-app/src/assets/particles.js
  76. 81
      packages/concordia-app/src/components/AppContext.jsx
  77. 151
      packages/concordia-app/src/components/ClearDatabasesModal/index.jsx
  78. 46
      packages/concordia-app/src/components/CustomLoadingTabPane.jsx
  79. 76
      packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx
  80. 25
      packages/concordia-app/src/components/InitializationScreen/CustomLoader/style.css
  81. 160
      packages/concordia-app/src/components/InitializationScreen/index.jsx
  82. 8
      packages/concordia-app/src/components/LoadingScreen.jsx
  83. 13
      packages/concordia-app/src/components/NotFound.jsx
  84. 23
      packages/concordia-app/src/components/PaginationComponent.jsx
  85. 50
      packages/concordia-app/src/components/PollCreate/PollOption/index.jsx
  86. 4
      packages/concordia-app/src/components/PollCreate/PollOption/styles.css
  87. 190
      packages/concordia-app/src/components/PollCreate/index.jsx
  88. 12
      packages/concordia-app/src/components/PollCreate/styles.css
  89. 23
      packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx
  90. 67
      packages/concordia-app/src/components/PollView/PollGraph/PollChartBar/index.jsx
  91. 63
      packages/concordia-app/src/components/PollView/PollGraph/PollChartDonut/index.jsx
  92. 140
      packages/concordia-app/src/components/PollView/PollGraph/index.jsx
  93. 10
      packages/concordia-app/src/components/PollView/PollGraph/styles.css
  94. 26
      packages/concordia-app/src/components/PollView/PollGuestView/index.jsx
  95. 116
      packages/concordia-app/src/components/PollView/PollVote/index.jsx
  96. 3
      packages/concordia-app/src/components/PollView/PollVote/styles.css
  97. 211
      packages/concordia-app/src/components/PollView/index.jsx
  98. 162
      packages/concordia-app/src/components/PostCreate/index.jsx
  99. 18
      packages/concordia-app/src/components/PostCreate/styles.css
  100. 260
      packages/concordia-app/src/components/PostList/PostListRow/index.jsx

25
.dockerignore

@ -0,0 +1,25 @@
node_modules
.idea
.git
docker/
!docker/concordia-contracts/migrate.sh
!docker/concordia-contracts/test-contracts.sh
!docker/concordia-app/test-app.sh
!docker/concordia-app/nginx.conf
!docker/concordia-app/create-environment.sh
!docker/concordia-app/run.sh
!docker/ganache/start-blockchain.sh
packages/*/node_modules
packages/*/dist
packages/*/coverage
packages/*/*.env*
# TO-NEVER-DO: exclude the build folder of the contracts package, it's needed for building the application image.
packages/concordia-app/build
Jenkinsfile
README.md
packages/*/README.md

9
.gitattributes

@ -1 +1,10 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.ico binary
# Solidity
*.sol linguist-language=Solidity

36
.gitignore

@ -1,26 +1,38 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# Node
/node_modules
package-lock.json
packages/*/node_modules
packages/concordia-contracts/build
# Testing
/coverage
# IDE
.DS_Store
.idea
# Production
# Build Directories
/build
/src/build
/packages/concordia-app/build
/packages/concordia-contracts/build
# Logs
/log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Docker volumes
docker/volumes
docker/ganache/volumes
docker/concordia-contracts-provider/volumes
docker/concordia-pinner/volumes
docker/reports
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Lerna
*.lerna_backup
# Jetbrains
.idea
yarn-clean.sh

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 ECEntrics
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

19
README.md

@ -1,3 +1,18 @@
# Apella
# Concordia
> A distributed forum using Blockchain, supporting direct democratic voting
*Note: This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).*
## Setup
You can find detailed instructions on how to get started with each service in the respective package directories. The
overall setup pipeline **for development** is this though:
- start rendezvous server and ganache
- migrate contracts
- start application
## Using Docker images
This project provides docker images for a number of services required to set up Concordia, as well as for Concordia
itself.
Check out the README.md in the `./docker` directory

88
contracts/Forum.sol

@ -1,88 +0,0 @@
pragma solidity ^0.4.17;
contract Forum {
//----------------------------------------USER----------------------------------------
struct User {
string userName; // TODO: set an upper bound instead of arbitrary string
// TODO: orbitDBAddress;
uint[] topicIDs; // IDs of the topics the user created
uint[] postIDs; // IDs of the posts the user created
}
mapping (address => User) users;
mapping (string => address) userAddresses;
function signUp(string userName) public returns (bool) { // Also allows user to update his name - TODO: his previous name will appear as taken
require(!isUserNameTaken(userName));
users[msg.sender] = User(userName, new uint[](0), new uint[](0));
userAddresses[userName] = msg.sender;
return true;
}
function login() public view returns (string) {
require (hasUserSignedUp(msg.sender));
return users[msg.sender].userName;
}
function getUsername(address userAddress) public view returns (string) {
return users[userAddress].userName;
}
function getUserAddress(string userName) public view returns (address) {
return userAddresses[userName];
}
function hasUserSignedUp(address userAddress) public view returns (bool) {
if (bytes(getUsername(userAddress)).length!=0)
return true;
return false;
}
function isUserNameTaken(string userName) public view returns (bool) {
if (getUserAddress(userName)!=0)
return true;
return false;
}
//----------------------------------------TOPIC----------------------------------------
struct Topic {
uint topicID;
address author;
uint timestamp;
uint[] postIDs;
}
struct Post {
uint postID;
address author;
uint timestamp;
}
uint numTopics; // Total number of topics
uint numPosts; // Total number of posts
mapping (uint => Topic) topics;
mapping (uint => Post) posts;
function createTopic() public returns (uint topicID) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create topics
topicID = numTopics++;
topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0));
users[msg.sender].topicIDs.push(topicID);
}
function createPost(uint topicID) public returns (uint postID) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create posts
require(topicID<numTopics); // Only allow posting to a topic that exists
postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp);
topics[topicID].postIDs.push(postID);
users[msg.sender].postIDs.push(postID);
}
function getTopicPosts (uint topicID) public view returns (uint[]) {
require(topicID<numTopics); // Topic should exist
return topics[topicID].postIDs;
}
}

23
contracts/Migrations.sol

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

95
docker/Makefile

@ -0,0 +1,95 @@
.EXPORT_ALL_VARIABLES:
PACKAGES := $(abspath ${CURDIR}/../packages)
REPORTS := $(abspath ${CURDIR}/reports)
GANACHE_VOLUMES := $(abspath ${CURDIR}/ganache/volumes)
CONTRACTS_PROVIDER_VOLUMES := $(abspath ${CURDIR}/concordia-contracts-provider/volumes)
PINNER_VOLUMES := $(abspath ${CURDIR}/concordia-pinner/volumes)
DOCKER_BUILDKIT = 1
run: compose-run build-contracts-provider run-contracts-provider build-contracts-migrate run-contracts-migrate build-pinner run-pinner build-app run-app
@echo "Concordia is up and running, head over to http://localhost:7777."
run-staging: compose-run build-contracts-provider run-contracts-provider-staging build-contracts-migrate run-contracts-migrate build-pinner run-pinner-staging build-app-staging run-app-staging
@echo "Concordia is up and running, head over to http://localhost:7000."
prepare-network:
@docker network create --driver bridge concordia_concordia_network || true
# Targets for building/running/stopping the blockchain and rendezvous server (using the docker-compose file)
compose-build:
@docker-compose -f ./docker-compose.yml -p concordia build
compose-run:
@docker-compose -f ./docker-compose.yml -p concordia up -d
compose-stop:
@docker-compose -f ./docker-compose.yml -p concordia down
compose-stop-clean-data:
@docker-compose -f ./docker-compose.yml -p concordia down -v
# Ganache targets
build-ganache:
@docker build ../ -f ./ganache/Dockerfile -t ecentrics/concordia-ganache
run-ganache:
@docker run -d -v ${GANACHE_VOLUMES}/ganache_keys:/mnt/concordia/ganache_keys -p 8545:8545 --env-file=./env/ganache.env --name concordia-ganache --net=concordia_concordia_network ecentrics/concordia-ganache:latest
run-ganache-test:
@docker run --rm -d -p 8546:8546 --env-file=./env/ganache.test.env --name concordia-ganache-test --net=concordia_concordia_network ecentrics/concordia-ganache:latest
# Rendezvous targets
run-rendezvous:
@docker run -d -p 9090:9090 --name concordia-rendezvous --net=concordia_concordia_network libp2p/js-libp2p-webrtc-star:version-0.21.1
# Contracts targets
build-contracts:
@docker build ../ -f ./concordia-contracts/Dockerfile --target compile -t ecentrics/concordia-contracts --build-arg TZ=Europe/Athens
build-contracts-migrate:
@docker build ../ -f ./concordia-contracts/Dockerfile -t ecentrics/concordia-contracts-migrate --build-arg TZ=Europe/Athens
build-contracts-tests:
@docker build ../ -f ./concordia-contracts/Dockerfile --target test -t ecentrics/concordia-contracts-tests --build-arg TZ=Europe/Athens
run-contracts-tests:
@docker run --rm -v ${REPORTS}/contracts/:/mnt/concordia/test-reports/ --env-file=./env/contracts-test.env --net=concordia_concordia_network ecentrics/concordia-contracts-tests:latest
run-contracts-tests-host-chain:
@docker run --rm -v ${REPORTS}/contracts/:/mnt/concordia/test-reports/ --env-file=./env/contracts-test.env --net=host ecentrics/concordia-contracts-tests:latest
run-contracts-migrate:
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/usr/src/concordia/packages/concordia-contracts/build/ --env-file=./env/contracts.env --net=concordia_concordia_network ecentrics/concordia-contracts-migrate:latest
run-contracts-migrate-host-chain:
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/usr/src/concordia/packages/concordia-contracts/build/ --env-file=./env/contracts.env --net=host ecentrics/concordia-contracts-migrate:latest
get-contracts:
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/mnt/concordia/build --entrypoint=sh ecentrics/concordia-contracts:latest -c 'cp /usr/src/concordia/packages/concordia-contracts/build/* /mnt/concordia/build'
# App targets
build-app:
@docker build ../ -f ./concordia-app/Dockerfile -t ecentrics/concordia-app --build-arg TZ=Europe/Athens
build-app-staging:
@docker build ../ -f ./concordia-app/Dockerfile --target staging -t ecentrics/concordia-app-staging --build-arg TZ=Europe/Athens
build-app-tests:
@docker build ../ -f ./concordia-app/Dockerfile --target test -t ecentrics/concordia-app-tests --build-arg TZ=Europe/Athens
run-app-tests:
@docker run --rm -v ${REPORTS}/app/:/mnt/concordia/test-reports/ --env-file=./env/concordia.env ecentrics/concordia-app-tests:latest
run-app:
@docker run -d --env-file=./env/concordia.env -p 7777:80 --name concordia-app ecentrics/concordia-app:latest
run-app-staging:
@docker run -itd --env-file=./env/concordia.env -p 7000:3000 --name concordia-app-staging ecentrics/concordia-app-staging:latest
run-app-host-chain:
@docker run -d --env-file=./env/concordia.env --name concordia-app --net=host ecentrics/concordia-app:latest
# Contracts provider targets
build-contracts-provider:
@docker build ../ -f ./concordia-contracts-provider/Dockerfile -t ecentrics/concordia-contracts-provider --build-arg TZ=Europe/Athens
run-contracts-provider-staging:
@docker run -d -v ${CONTRACTS_PROVIDER_VOLUMES}:/mnt/concordia --env-file=./env/contracts-provider.env -p 8400:8400 --name concordia-contracts-provider --net=concordia_concordia_network ecentrics/concordia-contracts-provider:latest
run-contracts-provider:
@docker run -d -v ${CONTRACTS_PROVIDER_VOLUMES}:/mnt/concordia --env-file=./env/contracts-provider.env -e NODE_ENV=production -p 8400:8400 --name concordia-contracts-provider --net=concordia_concordia_network ecentrics/concordia-contracts-provider:latest
# Pinner targets
build-pinner:
@docker build ../ -f ./concordia-pinner/Dockerfile -t ecentrics/concordia-pinner --build-arg TZ=Europe/Athens
run-pinner-staging:
@docker run -d -v ${PINNER_VOLUMES}:/mnt/concordia --env-file=./env/pinner.env -p 4444:4444 --name concordia-pinner --net=concordia_concordia_network ecentrics/concordia-pinner:latest
run-pinner:
@docker run -d -v ${PINNER_VOLUMES}:/mnt/concordia --env-file=./env/pinner.env -e NODE_ENV=production -p 4444:4444 --name concordia-pinner --net=concordia_concordia_network ecentrics/concordia-pinner:latest
run-pinner-staging-host:
@docker run -d -v ${PINNER_VOLUMES}:/mnt/concordia --env-file=./env/pinner.env --net=host --name concordia-pinner ecentrics/concordia-pinner:latest
run-pinner-host:
@docker run -d -v ${PINNER_VOLUMES}:/mnt/concordia --env-file=./env/pinner.env -e NODE_ENV=production --net=host --name concordia-pinner ecentrics/concordia-pinner:latest
# Other
clean-images:
@docker rmi `docker images -q -f "dangling=true"`

262
docker/README.md

@ -0,0 +1,262 @@
# Concordia Dockerized
This document gives information about the provided docker images, their configuration and supported deployment
strategies.
TLDR: head down to [Putting it all together/Scripts](#piat-mkfile-targets) for a quick setup.
## Services
Concordia requires at the minimum two services to work, a blockchain and a rendezvous server.
Additionally, the Concordia application code must be provided to the user. Currently, the only way of distributing the
application code is via a webserver as a web application.
### Ganache
Ganache is a personal blockchain software used during development. It is a very convenient way of developing and testing
dApps. More information can be found in the project's [website](https://www.trufflesuite.com/ganache).
Note that any other Ethereum compliant blockchain can be used.
### Rendezvous
Concordia uses a distributed database to store forum data. A rendezvous server is needed in order for users to discover
peers in the network and get access to the data.
### Contracts Provider
Contracts provider is a **very** simple resource provider server that handles saving the contract artifacts produced
during contracts migration and serving them to the users (and pinner).
### Pinner
Pinner is a bot node of the network that pins all IPFS content, so the forum data may be available even if the users
that create it are not.
### Application
The Concordia application is a React app that handles interactions with the contracts and the distributed database used.
## Docker images
This repository provides docker images to easily setup (and destroy) instances of all required services Concordia.
Furthermore, we provide an image that builds the contracts and handles their migration to the blockchain in use.
### Ganache
The Dockerfile is provided in the path `./ganache`. The image makes use of the environment variables described
below.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| ACCOUNTS_NUMBER | `10` | Set the number of accounts generated |
| ACCOUNTS_ETHER | `100` | Set the amount of ETH assigned to each account |
| MNEMONIC | `NaN` | The mnemonic phrase sued as a seed for deterministic account generation |
| HOST | `0.0.0.0` | The hostname to listen on |
| PORT | `8545` | The port to listen on |
| NETWORK_ID | `5778` | The network id used to identify ganache |
Note that the Ganache instance running inside the container will save the generated blockchain keys in the path
`/mnt/concordia/ganache_keys/keys.json`. If you need to access the keys (e.g. for getting a private key and importing in
Metamask) you can mount a volume to this path to have easier access.
Also, the database used by Ganache for storing blockchain information is placed in the path
`/mnt/concordia/ganache_db/`. You can maintain the blockchain state between runs by mounting a volume to the database
path. To do that, add the docker flag `-v host/absolute/path/to/ganache_db:/mnt/concordia/ganache_db`.
### Rendezvous
The rendezvous server used here is `js-libp2p-webrtc-star`. The server listens on port 9090. More information can be
found on the github page of the project [here](https://github.com/libp2p/js-libp2p-webrtc-star).
### Contracts provider
A Dockerfile for the contracts provider service is provided in the path `./concordia-contracts-provider`. The Dockerfile
contains only one stage, that is the runtime.
The image makes use of the environment variables described below.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider application |
| UPLOAD_CONTRACTS_DIRECTORY | `concordia/packages/concordia-contracts-provider/contracts-uploads` | Set the directory where the uploaded contracts are saved |
| LOGS_PATH | `concordia/packages/concordia-contracts-provider/logs` | Set the directory where the application logs are saved |
| CORS_ALLOWED_ORIGINS | `http://127.0.0.1:7000`, `http://localhost:7000`, `https://127.0.0.1:7000`, `https://localhost:7000`, `http://127.0.0.1:4444`, `http://localhost:4444`, `https://127.0.0.1:4444`, `https://localhost:4444` | Set the list of addresses allowed by CORS* |
### Contracts
This is a provision system that compiles and deploys the contracts to any Ethereum blockchain. It also uploads the
deployed contract artifacts to the contracts-provider service.
**Attention**: the contracts-provider instance targeted by the environment variables MUST be up and running before
attempting to migrate the contracts.
A Dockerfile is provided in the path `./concordia-contracts` that will build the contracts used by Concordia during
image build and handle their deployment to any Ethereum network defined using env-vars upon container run. Dockerfile
contains three useful stages, described in the table below.
| Stage name | Entrypoint | Usage |
| --- | --- | --- |
| compile | Exits immediately | Compiles the contracts |
| test | Runs contract tests | Compiles contracts and runs tests using blockchain defined by env vars |
| runtime | Migrates contracts | Compiles contracts and migrates to the blockchain defined by env vars. Does **not** run tests |
The image makes use of the environment variables described below.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| MIGRATE_NETWORK | `develop` | Set the network where the contracts will be deployed/tested (set this to "env" unless you know what you're doing) |
| WEB3_HOST | `127.0.0.1` | Set the hostname of the blockchain network that will be used for deployment (requires network to be "env") |
| WEB3_PORT | `8545` | `NaN` | Set the port of the blockchain network that will be used for deployment (requires network to be "env") |
| CONTRACTS_PROVIDER_HOST | `http://127.0.0.1` | Set the hostname of the contracts provider |
| CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider |
| CONTRACTS_VERSION_TAG | `NaN` | Set the tag of the contracts uploaded to provided |
You can find the contract artifacts in the directory `/usr/src/concordia/packages/concordia-contracts/build/` inside
the image.
**Attention**: make sure the targeted blockchain is up and running before trying to migrate the contracts.
### Pinner
A Dockerfile for the pinner service is provided in the path `./concordia-pinner`. The Dockerfile contains only one
stage, that is the runtime.
The image makes use of the environment variables described below.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| PINNER_API_HOST | `127.0.0.1` | Set the hostname of the pinner application |
| PINNER_API_PORT | `4444` | Set the port of the pinner application |
| USE_EXTERNAL_CONTRACTS_PROVIDER | `false` | Enable/Disable use of external contracts provider |
| IPFS_DIRECTORY | `concordia/packages/concordia-pinner/ipfs` | Set the directory where the ipfs blocks are saved |
| ORBIT_DIRECTORY | `concordia/packages/concordia-pinner/orbitdb` | Set the directory where the orbitdb data are saved |
| LOGS_PATH | `concordia/packages/concordia-pinner/logs` | Set the directory where the application logs are saved |
| WEB3_HOST | `127.0.0.1` | Set the hostname of the blockchain |
| WEB3_PORT | `8545` | Set the port of the blockchain |
| RENDEZVOUS_HOST | `/ip4/127.0.0.1` | Set the hostname of the rendezvous server |
| RENDEZVOUS_PORT | `9090` | Set the port of the rendezvous server |
| CONTRACTS_PROVIDER_HOST | `http://127.0.0.1` | Set the hostname of the contracts provider service |
| CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider service |
| CONTRACTS_VERSION_HASH | `latest` | Set the contracts tag that will be pulled |
### Application
The Dockerfile provided in the path `./concordia-application` either builds the application for production and serves
the resulting build using an nginx server or simply runs the node development server. Dockerfile contains four stages,
described in the table below.
| Stage name | Entrypoint | Usage |
| --- | --- | --- |
| test | Runs tests | Fetches npm packages and runs tests |
| staging | Serves application for development | Starts the node development server |
| runtime | Serves application for production | Builds for production and serves it through nginx |
The image makes use of the environment variables described below.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| REACT_APP_CONCORDIA_HOST | `127.0.0.1` | Set the hostname of the concordia application |
| REACT_APP_CONCORDIA_PORT | `7000` | Set the port of the concordia application |
| REACT_APP_RENDEZVOUS_HOST | `/ip4/127.0.0.1` | Set the hostname of the rendezvous server |
| REACT_APP_RENDEZVOUS_PORT | `9090` | Set the port of the rendezvous server |
| REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER | `false` | Enable/Disable use of external contracts provider |
| REACT_APP_CONTRACTS_PROVIDER_HOST | `http://127.0.0.1` | Set the hostname of the contracts provider service |
| REACT_APP_CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider service |
| REACT_APP_CONTRACTS_VERSION_HASH | `latest` | Set the contracts tag that will be pulled |
**Attention**: this image will copy the contract artifacts from the directory `/packages/concordia-contracts/build` if
available. The image can then use these artifacts after build or pull new artifacts from a contracts provider.
**Attention**: if you plan to use the imported contract artifacts instead of a provider, make sure the contracts have
been deployed before **building** this image. Also, make sure the rendezvous server is up and running.
## Docker Compose
A docker-compose file also is provided. The docker-compose handles the lifecycle of the Ganache and Rendezvous server
containers.
## Putting it all together
You can find some ready to use scripts for common scenarios like dev deploys and testing in the `./docker` directory.
These scripts are documented in the following chapters.
### <a name="piat-mkfile-targets"></a> Makefile targets
Concordia uses blockchain and other distributed technologies. There are a number of ways to set up a running instance of
this application.
This chapter will guide you through simple setups for testing and production that depend on local blockchain (ganache)
instances which do not require real ETH to work or have any other charges.
#### Testing the contracts
Build the ganache image and spin up a blockchain for testing:
```shell
make build-ganache run-ganache-test
```
Build the testing stage of the contracts image:
```shell
make build-contracts-tests
```
Run the tests:
```shell
make run-contracts-tests
```
The results should be printed in the terminal, but are also available in the directory `./reports/contracts`.
#### Testing the application
Build the testing stage of the application image:
```shell
make build-app-tests
```
Run the test:
```shell
make run-app-tests
```
The results should be printed in the terminal, but are also available in the directory `./reports/app`.
#### Production
Just run the target:
```shell
make run
```
And you' re done! Head to [localhost:7777](localhost:7777) and voilà, a working Concordia instance appears! The
blockchain is exposed in the address `localhost:8545`.
**Tip**: the accounts (private keys) generated by Ganache are available in the file `./volumes/ganache_keys/keys.json`.
Note that the `make run` command might take several minutes to execute (depending on your system). What happens under
the hood is that:
- the ganache image is built
- blockchain and rendezvous server containers are started
- migration stage of the contracts image is built
- the contracts are deployed to the blockchain:
- the application image is built and then deployed
### Env Files
Targets in the Makefile make use of env files located in the directory `./env`. Using this environment variables, you
can change various configuration options of the testing/production deploys.
Makefile targets suffixed with `host-chain` will try to use a blockchain and rendezvous server running in the host
machine. They use the `--net=host` docker option. In order to work with these targets you need to create and use your
own env files (or modify the existing ones). The environment variables values will largely depend on how you choose to
run the services in your system (which ports you use etc.), so be sure to create them before running any `host-chain`
target.

97
docker/concordia-app/Dockerfile

@ -0,0 +1,97 @@
# --------------------------------------------------
# Stage 1 (Init application build base)
# --------------------------------------------------
FROM node:14-alpine@sha256:51e341881c2b77e52778921c685e711a186a71b8c6f62ff2edfc6b6950225a2f as base
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>"
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>"
LABEL gr.thmmy.ecentrics.concordia-image.name="app"
WORKDIR /usr/src/concordia
# Copy the root package.json and yarn.lock
COPY ./package.json .
COPY ./yarn.lock .
# Copy package.json files from contracts, shared and app, then install modules
COPY ./packages/concordia-contracts/package.json ./packages/concordia-contracts/package.json
COPY ./packages/concordia-shared/package.json ./packages/concordia-shared/package.json
COPY ./packages/concordia-app/package.json ./packages/concordia-app/
# Install required packages
RUN apk update && apk --no-cache add g++ make python
RUN yarn install --frozen-lockfile
# Gets the rest of the source code
COPY ./packages/concordia-contracts ./packages/concordia-contracts
COPY ./packages/concordia-shared ./packages/concordia-shared
COPY ./packages/concordia-app ./packages/concordia-app
# Fix timezome
ARG TZ
ENV TZ=${TZ}
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# --------------------------------------------------
# Stage 2 (Test)
# --------------------------------------------------
FROM base as test
WORKDIR /opt/concordia-app
COPY ./docker/concordia-app/test-app.sh .
WORKDIR /usr/src/concordia/packages/concordia-app
RUN ["chmod", "+x", "/opt/concordia-app/test-app.sh"]
ENTRYPOINT ["/opt/concordia-app/test-app.sh"]
# --------------------------------------------------
# Stage 3 (Build)
# --------------------------------------------------
FROM base as build
WORKDIR /usr/src/concordia/packages/concordia-app
RUN yarn build
# --------------------------------------------------
# Stage 4 (Staging runtime)
# --------------------------------------------------
FROM base as staging
WORKDIR /usr/src/concordia/packages/concordia-app
ENTRYPOINT ["yarn", "start"]
# --------------------------------------------------
# Stage 4 (Production runtime)
# --------------------------------------------------
FROM nginx:1.17-alpine@sha256:01747306a7247dbe928db991eab42e4002118bf636dd85b4ffea05dd907e5b66 as production
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>"
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com"
LABEL gr.thmmy.ecentrics.concordia-image.name="app"
# Fix timezome
ARG TZ
RUN apk add -U tzdata \
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \
&& apk del tzdata \
&& rm -rf /var/cache/apk/*
COPY ./docker/concordia-app/create-environment.sh /opt/concordia/create-environment.sh
COPY ./docker/concordia-app/run.sh /opt/concordia/run.sh
RUN ["chmod", "+x", "/opt/concordia/create-environment.sh"]
RUN ["chmod", "+x", "/opt/concordia/run.sh"]
COPY ./docker/concordia-app/nginx.conf /etc/nginx/conf.d/default.conf
WORKDIR "/var/www/concordia-app"
COPY --chown=nginx:nginx --from=build /usr/src/concordia/packages/concordia-app/build .
CMD ["/opt/concordia/run.sh"]

14
docker/concordia-app/create-environment.sh

@ -0,0 +1,14 @@
#!/bin/sh
echo "window.runtimeEnv = { \
REACT_APP_CONCORDIA_HOST: \"${REACT_APP_CONCORDIA_HOST}\", \
REACT_APP_CONCORDIA_PORT: \"${REACT_APP_CONCORDIA_PORT}\", \
REACT_APP_WEB3_HOST: \"${REACT_APP_WEB3_HOST}\", \
REACT_APP_WEB3_PORT: \"${REACT_APP_WEB3_PORT}\", \
REACT_APP_RENDEZVOUS_HOST: \"${REACT_APP_RENDEZVOUS_HOST}\", \
REACT_APP_RENDEZVOUS_PORT: \"${REACT_APP_RENDEZVOUS_PORT}\", \
REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER: \"${REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER}\", \
REACT_APP_CONTRACTS_PROVIDER_HOST: \"${REACT_APP_CONTRACTS_PROVIDER_HOST}\", \
REACT_APP_CONTRACTS_PROVIDER_PORT: \"${REACT_APP_CONTRACTS_PROVIDER_PORT}\", \
REACT_APP_CONTRACTS_VERSION_HASH: \"${REACT_APP_CONTRACTS_VERSION_HASH}\", \
}" >/var/www/concordia-app/environment.js

22
docker/concordia-app/nginx.conf

@ -0,0 +1,22 @@
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location / {
root /var/www/concordia-app;
index index.html index.htm;
try_files "$uri" "$uri/" /index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

3
docker/concordia-app/run.sh

@ -0,0 +1,3 @@
sh /opt/concordia/create-environment.sh
exec "$(which nginx)" -g "daemon off;"

11
docker/concordia-app/test-app.sh

@ -0,0 +1,11 @@
#!/bin/sh
yarn lint -f html -o /mnt/concordia/test-reports/concordia-app-eslint.html --no-color
if [ $? -eq 0 ]; then
echo "TESTS RAN SUCCESSFULLY!"
exit 0
else
echo "SOME TESTS FAILED!"
exit 1
fi

36
docker/concordia-contracts-provider/Dockerfile

@ -0,0 +1,36 @@
# --------------------------------------------------
# Stage 1 (Runtime)
# --------------------------------------------------
FROM node:14-alpine@sha256:51e341881c2b77e52778921c685e711a186a71b8c6f62ff2edfc6b6950225a2f as runtime
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>"
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>"
LABEL gr.thmmy.ecentrics.concordia-image.name="contracts-provider"
# Fix timezome (needed for timestamps on report files)
ARG TZ
RUN apk add -U tzdata \
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \
&& apk del tzdata \
&& rm -rf /var/cache/apk/*
WORKDIR /usr/src/concordia
# Copy the root package.json and yarn.lock
COPY ./package.json .
COPY ./yarn.lock .
# Copy package.json files from shared and contracts provider, then install modules
COPY ./packages/concordia-shared/package.json ./packages/concordia-shared/
COPY ./packages/concordia-contracts-provider/package.json ./packages/concordia-contracts-provider/
RUN yarn install --frozen-lockfile --network-timeout 100000
# Gets the rest of the source code
COPY ./packages/concordia-shared ./packages/concordia-shared
COPY ./packages/concordia-contracts-provider ./packages/concordia-contracts-provider
WORKDIR /usr/src/concordia/packages/concordia-contracts-provider
ENTRYPOINT ["yarn", "start"]

70
docker/concordia-contracts/Dockerfile

@ -0,0 +1,70 @@
# --------------------------------------------------
# Stage 1 (Init contracts build base)
# --------------------------------------------------
FROM node:14-alpine@sha256:51e341881c2b77e52778921c685e711a186a71b8c6f62ff2edfc6b6950225a2f as base
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>"
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>"
LABEL gr.thmmy.ecentrics.concordia-image.name="contracts"
# Fix timezome (needed for timestamps on report files)
ARG TZ
RUN apk add -U tzdata \
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \
&& apk del tzdata \
&& rm -rf /var/cache/apk/*
WORKDIR /usr/src/concordia
# Copy the root package.json and yarn.lock
COPY ./package.json .
COPY ./yarn.lock .
# Copy package.json files from shared and contracts, then install modules
COPY ./packages/concordia-shared/package.json ./packages/concordia-shared/
COPY ./packages/concordia-contracts/package.json ./packages/concordia-contracts/
RUN yarn install --frozen-lockfile --network-timeout 100000
# Gets the rest of the source code
COPY ./packages/concordia-shared ./packages/concordia-shared
COPY ./packages/concordia-contracts ./packages/concordia-contracts
# --------------------------------------------------
# Stage 2 (Compile)
# --------------------------------------------------
FROM base as compile
WORKDIR /usr/src/concordia/packages/concordia-contracts
RUN yarn compile
# --------------------------------------------------
# Stage 3 (Test)
# --------------------------------------------------
FROM compile as test
WORKDIR /opt/concordia-contracts
COPY ./docker/concordia-contracts/test-contracts.sh .
WORKDIR /usr/src/concordia/packages/concordia-contracts
RUN ["chmod", "+x", "/opt/concordia-contracts/test-contracts.sh"]
ENTRYPOINT ["/opt/concordia-contracts/test-contracts.sh"]
# --------------------------------------------------
# Stage 4 (Runtime)
# --------------------------------------------------
FROM compile as runtime
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>"
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>"
LABEL gr.thmmy.ecentrics.concordia-image.name="contracts"
WORKDIR /opt/concordia-contracts
COPY ./docker/concordia-contracts/migrate.sh .
RUN ["chmod", "+x", "/opt/concordia-contracts/migrate.sh"]
ENTRYPOINT ["/opt/concordia-contracts/migrate.sh"]

8
docker/concordia-contracts/migrate.sh

@ -0,0 +1,8 @@
#!/bin/sh
export CHAIN_HOST="$DEPLOY_CHAIN_HOST"
export CHAIN_PORT="$DEPLOY_CHAIN_PORT"
cd /usr/src/concordia/packages/concordia-contracts &&
yarn _migrate --network "${MIGRATE_NETWORK}" --reset &&
yarn _upload ${CONTRACTS_VERSION_HASH} ${CONTRACTS_VERSION_TAG}

22
docker/concordia-contracts/test-contracts.sh

@ -0,0 +1,22 @@
#!/bin/sh
export CHAIN_HOST="$TEST_CHAIN_HOST"
export CHAIN_PORT="$TEST_CHAIN_PORT"
yarn _eslint -f html -o /mnt/concordia/test-reports/concordia-contracts-eslint.html --no-color
ESLINT_EXIT_STATUS=$?
yarn _solhint >/mnt/concordia/test-reports/concordia-contracts-solhint.report
SOLHINT_EXIT_STATUS=$?
yarn test --network env >/mnt/concordia/test-reports/concordia-contracts-truffle-tests.report
grep -qE failing /mnt/concordia/test-reports/concordia-contracts-truffle-tests.report
TRUFFLE_TEST_FAILING=$?
if [ $ESLINT_EXIT_STATUS -eq 0 ] && [ "$SOLHINT_EXIT_STATUS" -eq 0 ] && [ "$TRUFFLE_TEST_FAILING" -eq 1 ]; then
echo "TESTS RAN SUCCESSFULLY!"
exit 0
else
echo "SOME TESTS FAILED!"
exit 1
fi

34
docker/concordia-pinner/Dockerfile

@ -0,0 +1,34 @@
# --------------------------------------------------
# Stage 1 (Runtime)
# --------------------------------------------------
FROM node:14-buster@sha256:32362e2ea89c62d77c86c8f26ad936dbbdc170cd6c06c4d7ff7a809012bb0c32 as runtime
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>"
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>"
LABEL gr.thmmy.ecentrics.concordia-image.name="pinner"
# Fix timezome (needed for timestamps on report files)
ARG TZ
ENV TZ=${TZ}
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /usr/src/concordia
# Copy the root package.json and yarn.lock
COPY ./package.json .
COPY ./yarn.lock .
# Copy package.json files from contracts, shared and pinner, then install modules
COPY ./packages/concordia-pinner/package.json ./packages/concordia-pinner/
COPY ./packages/concordia-contracts/package.json ./packages/concordia-contracts/
COPY ./packages/concordia-shared/package.json ./packages/concordia-shared/
RUN yarn install --frozen-lockfile --network-timeout 100000
# Gets the rest of the source code
COPY ./packages/concordia-shared ./packages/concordia-shared
COPY ./packages/concordia-contracts ./packages/concordia-contracts
COPY ./packages/concordia-pinner ./packages/concordia-pinner
WORKDIR /usr/src/concordia/packages/concordia-pinner
ENTRYPOINT ["yarn", "start"]

33
docker/docker-compose.yml

@ -0,0 +1,33 @@
version: '3.8'
services:
ganache:
build:
context: ../
dockerfile: ./docker/ganache/Dockerfile
image: ecentrics/concordia-ganache
container_name: concordia-ganache
env_file:
- env/ganache.env
expose:
- 8545
ports:
- 8545:8545
user: root
volumes:
- ./ganache/volumes/ganache_keys:/mnt/concordia/ganache_keys
networks:
concordia_network:
restart: always
rendezvous:
image: libp2p/js-libp2p-webrtc-star:version-0.21.1
container_name: concordia-rendezvous
networks:
concordia_network:
ports:
- 9090:9090
restart: always
networks:
concordia_network:

5
docker/env/concordia.env

@ -0,0 +1,5 @@
# Variables needed in runtime (in browser)
REACT_APP_RENDEZVOUS_HOST=/ip4/127.0.0.1
REACT_APP_RENDEZVOUS_PORT=9090
REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER=true
REACT_APP_CONTRACTS_VERSION_HASH=docker

3
docker/env/contracts-provider.env

@ -0,0 +1,3 @@
UPLOAD_CONTRACTS_DIRECTORY=/mnt/concordia/contracts/
LOGS_PATH=/mnt/concordia/logs/
#CORS_ALLOWED_ORIGINS="http://127.0.0.1:7000;http://localhost:7000;http://127.0.0.1:7777;http://localhost:7777;http://127.0.0.1:4444;127.0.0.1:4444"

4
docker/env/contracts-test.env

@ -0,0 +1,4 @@
# Variables needed in runtime
MIGRATE_NETWORK=env
WEB3_HOST=concordia-ganache-test
WEB3_PORT=8546

8
docker/env/contracts.env

@ -0,0 +1,8 @@
# Variables needed in runtime
MIGRATE_NETWORK=env
WEB3_HOST=concordia-ganache
WEB3_PORT=8545
CONTRACTS_PROVIDER_HOST=http://concordia-contracts-provider
CONTRACTS_PROVIDER_PORT=8400
CONTRACTS_VERSION_HASH=docker

5
docker/env/ganache.env

@ -0,0 +1,5 @@
ACCOUNTS_NUMBER=10
ACCOUNTS_ETHER=100
HOST=0.0.0.0
PORT=8545
NETWORK_ID=5778

6
docker/env/ganache.test.env

@ -0,0 +1,6 @@
ACCOUNTS_NUMBER=5
ACCOUNTS_ETHER=100
MNEMONIC="myth like bonus scare over problem client lizard pioneer submit female collect"
HOST=0.0.0.0
PORT=8546
NETWORK_ID=5778

17
docker/env/pinner.env

@ -0,0 +1,17 @@
USE_EXTERNAL_CONTRACTS_PROVIDER=true
IPFS_DIRECTORY=/mnt/concordia/ipfs
ORBIT_DIRECTORY=/mnt/concordia/orbitdb
LOGS_PATH=/mnt/concordia/logs/
CONTRACTS_PROVIDER_HOST=http://concordia-contracts-provider
CONTRACTS_PROVIDER_PORT=8400
CONTRACTS_VERSION_HASH=docker
PINNER_API_HOST=127.0.0.1
PINNER_API_PORT=4444
RENDEZVOUS_HOST=/docker/concordia-rendezvous
RENDEZVOUS_PORT=9090
WEB3_HOST=concordia-ganache
WEB3_PORT=8545

10
docker/ganache/Dockerfile

@ -0,0 +1,10 @@
FROM trufflesuite/ganache-cli:v6.12.2@sha256:c062707f17f355872d703cde3de6a12fc45a027ed42857c72514171a5f466ab7
RUN mkdir -p /mnt/concordia/ganache_db /mnt/concordia/ganache_keys
WORKDIR /opt/concordia-ganache
COPY ./docker/ganache/start-blockchain.sh .
RUN ["chmod", "+x", "/opt/concordia-ganache/start-blockchain.sh"]
ENTRYPOINT ["/opt/concordia-ganache/start-blockchain.sh"]

37
docker/ganache/start-blockchain.sh

@ -0,0 +1,37 @@
#!/bin/sh
N_ACCOUNTS="${ACCOUNTS_NUMBER:-10}"
ETHER="${ACCOUNTS_ETHER:-10}"
HOST="${HOST:-"0.0.0.0"}"
PORT="${PORT:-8545}"
ID="${NETWORK_ID:-5778}"
if [ -z "${MNEMONIC}" ]; then
echo "Starting Ganache with non deterministic address generation"
node /app/ganache-core.docker.cli.js \
--accounts "$N_ACCOUNTS" \
--defaultBalanceEther "$ETHER" \
--host "$HOST" \
--port "$PORT" \
--networkId "$ID" \
--account_keys_path "/mnt/concordia/ganache_keys/keys.json" \
--db "/mnt/concordia/ganache_db/" \
--allowUnlimitedContractSize \
--noVMErrorsOnRPCResponse \
--verbose
else
echo "Starting Ganache with deterministic address generation"
node /app/ganache-core.docker.cli.js \
--accounts "$N_ACCOUNTS" \
--defaultBalanceEther "$ETHER" \
--mnemonic "$MNEMONIC" \
--host "$HOST" \
--port "$PORT" \
--networkId "$ID" \
--account_keys_path "/mnt/concordia/ganache_keys/keys.json" \
--db "/mnt/concordia/ganache_db/" \
--allowUnlimitedContractSize \
--noVMErrorsOnRPCResponse \
--deterministic \
--verbose
fi

781
jenkins/Jenkinsfile

@ -0,0 +1,781 @@
#!groovy
def cleanSlateEnabled
def sanitizedBranchName
// Package change state
def appPackageChanged
def contractsPackageChanged
def contractsProviderPackageChanged
def pinnerPackageChanged
def sharedPackageChanged
// Package versions
def appPackageVersion
def contractsPackageVersion
def contractsProviderPackageVersion
def pinnerPackageVersion
def sharedPackageVersion
// Docker images
def appImage
def contractsImage
def contractsProviderImage
def pinnerImage
def freshGanacheStagingRunning = false
def freshGanacheProductionRunning = false
def successResultGif = "https://media.giphy.com/media/o75ajIFH0QnQC3nCeD/giphy.gif"
def failResultGif = "https://media.giphy.com/media/ljtfkyTD3PIUZaKWRi/giphy.gif"
def abortResultGif = "https://media.giphy.com/media/IzXmRTmKd0if6/giphy.gif"
pipeline {
agent any
post {
failure {
updateGitlabCommitStatus name: 'build', state: 'failed'
discordSend footer: "Visit Jenkins for more information", result: currentBuild.currentResult, link: env.BUILD_URL, description: """Jenkins Pipeline Build
Last commit included is [${GIT_COMMIT[0..7]}](https://gitlab.com/ecentrics/concordia/-/commit/$GIT_COMMIT)
Build status: ${currentBuild.currentResult}
""", image: failResultGif, thumbnail: "$CONCORDIA_LOGO_URL", title: JOB_NAME, webhookURL: "${DISCORD_WEBHOOK_URL}"
}
success {
updateGitlabCommitStatus name: 'build', state: 'success'
discordSend footer: "Visit Jenkins for more information", result: currentBuild.currentResult, link: env.BUILD_URL, description: """Jenkins Pipeline Build
Last commit included is [${GIT_COMMIT[0..7]}](https://gitlab.com/ecentrics/concordia/-/commit/$GIT_COMMIT)
Build status: ${currentBuild.currentResult}
""", image: successResultGif, thumbnail: "$CONCORDIA_LOGO_URL", title: JOB_NAME, webhookURL: "${DISCORD_WEBHOOK_URL}"
}
aborted {
discordSend footer: "Visit Jenkins for more information", result: currentBuild.currentResult, link: env.BUILD_URL, description: """Jenkins Pipeline Build
Last commit included is [${GIT_COMMIT[0..7]}](https://gitlab.com/ecentrics/concordia/-/commit/$GIT_COMMIT)
Build status: ${currentBuild.currentResult}
""", image: abortResultGif, thumbnail: "$CONCORDIA_LOGO_URL", title: JOB_NAME, webhookURL: "${DISCORD_WEBHOOK_URL}"
}
always {
archiveArtifacts artifacts: "reports/${BUILD_NUMBER}/**/* , build/**/*, ganache/*", fingerprint: true, allowEmptyArchive: true
sleep 2
sh 'docker images | grep -E "ecentrics/concordia.+tests" | tr -s \' \' | cut -d \' \' -f 3 | xargs --no-run-if-empty docker rmi -f || true'
sh "docker images | tr -s \' \' | grep -E \'ecentrics/concordia.+staging-b\' | sed -r \'s/(v.*-staging-b)([0-9]*)/\\1\\2 \\2/g\' | awk \'{ if (\$3 < ($BUILD_NUMBER - 2)) print \$1\":\"\$2}\' | xargs --no-run-if-empty docker rmi -f || true"
sh "docker images | tr -s \' \' | grep -E \'ecentrics/concordia.+production-b\' | sed -r \'s/(v.*-production-b)([0-9]*)/\\1\\2 \\2/g\' | awk \'{ if (\$3 < ($BUILD_NUMBER - 2)) print \$1\":\"\$2}\' | xargs --no-run-if-empty docker rmi -f || true"
sh 'docker system prune -f'
sh 'rm -rf reports'
sh 'rm -rf build'
}
}
options {
gitLabConnection('apella')
}
triggers {
gitlab(triggerOnPush: true, triggerOnMergeRequest: true, branchFilterType: 'All')
}
environment {
DOCKER_BUILDKIT='1'
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/810180975580938290/HYYeK8Nqwt0h8Arx3qPpF-szjgLkPDTqbVVKLkzcmqY7ourTpKJCAc6IuCXHd_cxowuK"
CONCORDIA_LOGO_URL="https://i.postimg.cc/MGvgy9Lp/app-logo-circle.png"
}
stages {
stage ('VERSION') {
steps {
script {
cleanSlateEnabled = sh (script: "git log -1 | grep -qE 'ci: force'", returnStatus: true)
sanitizedBranchName = sh(script: 'echo $GIT_BRANCH | sed -e "s:.*/::g"', returnStdout: true).trim()
appPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh app "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim()
contractsPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh contracts "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim()
contractsProviderPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh concordia-contracts-provider "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim()
pinnerPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh pinner "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim()
sharedPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh shared "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim()
appPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-app/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim()
contractsPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-contracts/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim()
contractsProviderPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-contracts-provider/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim()
pinnerPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-pinner/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim()
sharedPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-shared/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim()
echo "Package: app, Version: ${appPackageVersion}, Changed: ${appPackageChanged}"
echo "Package: contracts, Version: ${contractsPackageVersion}, Changed: ${contractsPackageChanged}"
echo "Package: contracts-provider, Version: ${contractsProviderPackageVersion}, Changed: ${contractsProviderPackageChanged}"
echo "Package: pinner, Version: ${pinnerPackageVersion}, Changed: ${pinnerPackageChanged}"
echo "Package: shared, Version: ${sharedPackageVersion}, Changed: ${sharedPackageChanged}"
}
}
}
stage('TEST') {
parallel {
stage('TEST CONTRACTS') {
steps {
script {
def ganacheTestPort = sh(script: "bash ./jenkins/hash_build_properties.sh ${BRANCH_NAME} ${BUILD_NUMBER} | xargs bash ./jenkins/map_to_thousand.sh", returnStdout: true).trim()
def ganacheTestImage = docker.build(
"ecentrics/concordia-ganache",
"-f docker/ganache/Dockerfile \
./"
)
docker.build(
"ecentrics/concordia-contracts-tests:${sanitizedBranchName}-v${contractsPackageVersion}-b${BUILD_NUMBER}-tests",
"-f docker/concordia-contracts/Dockerfile \
./ \
--target test \
--build-arg TZ=Europe/Athens"
)
sh 'docker network create --driver bridge concordia_ganache_test_network || true'
ganacheTestImage.withRun("""-d -p 6${ganacheTestPort}:8546 \
--env-file=./jenkins/env/ganache.test.jenkins.env \
--name concordia-ganache-test-6${ganacheTestPort} \
--net=concordia_ganache_test_network""") { concordiaGanacheTest ->
try {
sh """docker run \
--rm \
-v ecentrics_janus_common:/mnt/concordia/test-reports/ \
--env-file=./jenkins/env/contracts.test.jenkins.env \
-e WEB3_HOST=concordia-ganache-test-6${ganacheTestPort} \
-e WEB3_PORT=6${ganacheTestPort} \
--net=concordia_ganache_test_network \
ecentrics/concordia-contracts-tests:${sanitizedBranchName}-v${contractsPackageVersion}-b${BUILD_NUMBER}-tests"""
} catch (e) {
error('Some tests failed!')
error('Aborting the build.')
throw e
} finally {
sh 'mkdir -p ./reports/${BUILD_NUMBER}/contracts'
sh 'find /mnt/janus/common/ -name "concordia-contracts-*" -exec cp \'{}\' ./reports/${BUILD_NUMBER}/contracts/ \\;'
}
}
}
}
}
stage('TEST APP') {
steps {
script {
docker.build(
"ecentrics/concordia-app:${sanitizedBranchName}-v${appPackageVersion}-b${BUILD_NUMBER}-tests",
"-f docker/concordia-app/Dockerfile \
./ \
--target test \
--build-arg TZ=Europe/Athens"
)
try {
sh """docker run \
--rm \
-v ecentrics_janus_common:/mnt/concordia/test-reports/ \
ecentrics/concordia-app:${sanitizedBranchName}-v${appPackageVersion}-b${BUILD_NUMBER}-tests"""
} catch (e) {
error('Some tests failed!')
error('Aborting the build.')
throw e
} finally {
sh 'mkdir -p ./reports/${BUILD_NUMBER}/app'
sh 'find /mnt/janus/common/ -name "concordia-app-*" -exec cp \'{}\' ./reports/${BUILD_NUMBER}/app/ \\;'
}
}
}
}
}
}
stage('BUILD FOR PRODUCTION') {
when {
branch 'master'
}
parallel {
stage('BUILD CONTRACTS') {
when {
expression {
return "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
contractsImage = docker.build(
"ecentrics/concordia-contracts-migrate:v${contractsPackageVersion}",
"-f docker/concordia-contracts/Dockerfile \
./ \
-t ecentrics/concordia-contracts-migrate:latest \
--build-arg TZ=Europe/Athens"
)
contractsImage.run('--rm \
-v ecentrics_janus_common:/mnt/concordia/build \
--entrypoint=sh',
"-c 'mkdir -p /mnt/concordia/build/contract-artifacts && cp /usr/src/concordia/packages/concordia-contracts/build/* /mnt/concordia/build/contract-artifacts'")
sh 'mkdir -p ./build/${BUILD_NUMBER}/contracts'
sh 'cp /mnt/janus/common/contract-artifacts/* ./build/${BUILD_NUMBER}/contracts'
}
}
}
stage('BUILD APP') {
when {
expression {
return "${appPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
appImage = docker.build(
"ecentrics/concordia-app:v${appPackageVersion}",
"-f docker/concordia-app/Dockerfile \
./ \
-t ecentrics/concordia-app:latest \
--build-arg TZ=Europe/Athens"
)
}
}
}
stage('BUILD CONTRACTS PROVIDER') {
when {
expression {
return "${contractsProviderPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
contractsProviderImage = docker.build(
"ecentrics/concordia-contracts-provider:v${contractsProviderPackageVersion}",
"-f docker/concordia-contracts-provider/Dockerfile \
./ \
-t ecentrics/concordia-contracts-provider:latest \
--build-arg TZ=Europe/Athens"
)
}
}
}
stage('BUILD PINNER') {
when {
expression {
return "${pinnerPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
pinnerImage = docker.build(
"ecentrics/concordia-pinner:v${pinnerPackageVersion}",
"-f docker/concordia-pinner/Dockerfile \
./ \
-t ecentrics/concordia-pinner:latest \
--build-arg TZ=Europe/Athens"
)
}
}
}
}
}
stage('BUILD FOR STAGING') {
when {
branch 'develop'
}
parallel {
stage('BUILD CONTRACTS') {
when {
expression {
return "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
contractsImage = docker.build(
"ecentrics/concordia-contracts-migrate:v${contractsPackageVersion}-staging-b${BUILD_NUMBER}",
"-f docker/concordia-contracts/Dockerfile \
./ \
-t ecentrics/concordia-contracts-migrate:staging-latest \
--build-arg TZ=Europe/Athens"
)
// Get contract artifacts
contractsImage.run('--rm \
-v ecentrics_janus_common:/mnt/concordia/build \
--entrypoint=sh',
"-c 'mkdir -p /mnt/concordia/build/contract-artifacts && cp /usr/src/concordia/packages/concordia-contracts/build/* /mnt/concordia/build/contract-artifacts'")
sh 'mkdir -p ./build/${BUILD_NUMBER}/contracts'
sh 'cp /mnt/janus/common/contract-artifacts/* ./build/${BUILD_NUMBER}/contracts'
}
}
}
stage('BUILD APP') {
when {
expression {
return "${appPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
appImage = docker.build(
"ecentrics/concordia-app:v${appPackageVersion}-staging-b${BUILD_NUMBER}",
"-f docker/concordia-app/Dockerfile \
./ \
-t ecentrics/concordia-app:staging-latest \
--target staging \
--build-arg TZ=Europe/Athens"
)
}
}
}
stage('BUILD CONTRACTS PROVIDER') {
when {
expression {
return "${contractsProviderPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
contractsProviderImage = docker.build(
"ecentrics/concordia-contracts-provider:v${contractsProviderPackageVersion}-staging-b${BUILD_NUMBER}",
"-f docker/concordia-contracts-provider/Dockerfile \
./ \
-t ecentrics/concordia-contracts-provider:staging-latest \
--build-arg TZ=Europe/Athens"
)
}
}
}
stage('BUILD PINNER') {
when {
expression {
return "${pinnerPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
pinnerImage = docker.build(
"ecentrics/concordia-pinner:v${pinnerPackageVersion}-staging-b${BUILD_NUMBER}",
"-f docker/concordia-pinner/Dockerfile \
./ \
-t ecentrics/concordia-pinner:staging-latest \
--build-arg TZ=Europe/Athens"
)
}
}
}
}
}
stage('PUBLISH') {
when {
branch 'master'
}
parallel {
stage('PUBLISH CONTRACTS') {
when {
expression {
return "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
docker.withRegistry('https://registry.hub.docker.com/', 'docker-hub-concordia') {
contractsImage.push()
contractsImage.push('latest')
}
}
}
}
stage('PUBLISH APP') {
when {
expression {
return "${appPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
docker.withRegistry('https://registry.hub.docker.com/', 'docker-hub-concordia') {
appImage.push()
appImage.push('latest')
}
}
}
}
stage('PUBLISH CONTRACTS PROVIDER') {
when {
expression {
return "${contractsProviderPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
docker.withRegistry('https://registry.hub.docker.com/', 'docker-hub-concordia') {
contractsProviderImage.push()
contractsProviderImage.push('latest')
}
}
}
}
stage('PUBLISH PINNER') {
when {
expression {
return "${pinnerPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
docker.withRegistry('https://registry.hub.docker.com/', 'docker-hub-concordia') {
pinnerImage.push()
pinnerImage.push('latest')
}
}
}
}
}
}
stage('DEPLOY STAGING') {
when {
branch 'develop'
}
stages {
stage('STAGING DEPLOY PREPARATION') {
steps {
script {
sh 'docker network create --driver bridge ecentrics_concordia_staging_network || true'
def rendezvousServerRunning = sh (script: 'docker ps -f name=concordia-rendezvous | \
grep -qE concordia-rendezvous', returnStatus: true)
if ("$rendezvousServerRunning" == '1' || "$cleanSlateEnabled" == '0') {
sh 'docker stop concordia-rendezvous || true \
&& docker rm concordia-rendezvous || true'
sh 'docker run \
-d \
--env-file=./jenkins/env/rendezvous.jenkins.env \
-p 9090:9090 \
--name concordia-rendezvous \
--net=ecentrics_concordia_staging_network \
libp2p/js-libp2p-webrtc-star:version-0.21.1'
} else {
sh 'docker network connect ecentrics_concordia_staging_network concordia-rendezvous || true'
}
}
}
}
stage('DEPLOY CONTRACTS PROVIDER') {
when {
expression {
def contractsProviderStagingRunning = sh (script: 'docker ps -f name=concordia-contracts-provider-staging | \
grep -qE concordia-contracts-provider-staging', returnStatus: true)
return "${contractsProviderPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "$contractsProviderStagingRunning" == '1' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
sh 'docker stop concordia-contracts-provider-staging || true \
&& docker rm concordia-contracts-provider-staging || true'
sh 'if [ "$cleanSlateEnabled" -eq "0" ]; then \
docker volume rm concordia-contracts-provider-staging || true; \
fi'
sh (script: """docker run \
-d \
-v concordia-contracts-provider-staging:/mnt/concordia \
--env-file=./jenkins/env/contracts.provider.staging.env \
-p 8450:8450 \
--name concordia-contracts-provider-staging \
--net=ecentrics_concordia_staging_network \
ecentrics/concordia-contracts-provider:staging-latest""")
}
}
}
stage('RECREATE GANACHE') {
when {
expression {
def ganacheStagingRunning = sh (script: 'docker ps -f name=concordia-ganache-staging | \
grep -qE concordia-ganache-staging', returnStatus: true)
return "$cleanSlateEnabled" == '0' || "$ganacheStagingRunning" == '1';
}
}
steps {
script {
sh 'docker stop concordia-ganache-staging || true \
&& docker rm concordia-ganache-staging || true'
sh 'docker volume rm concordia-ganache-staging || true'
sh (script: 'docker run \
-d \
-v concordia-ganache-staging:/mnt/concordia \
-p 8555:8555 \
--env-file=./jenkins/env/ganache.staging.jenkins.env \
--name concordia-ganache-staging \
--net=ecentrics_concordia_staging_network \
ecentrics/concordia-ganache:latest')
// Ganache image might take a while to come alive
sleep 10
sh 'mkdir -p ./ganache/ && docker cp concordia-ganache-staging:/mnt/concordia/ganache_keys/keys.json ./ganache/'
freshGanacheStagingRunning = true
}
}
}
stage('DEPLOY CONTRACTS') {
when {
expression {
return "${contractsPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "$freshGanacheStagingRunning" || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
sh """docker run \
--rm \
--env-file=./jenkins/env/contracts.staging.jenkins.env \
-e CONTRACTS_VERSION_HASH=${contractsPackageVersion}-dev \
--net=ecentrics_concordia_staging_network \
ecentrics/concordia-contracts-migrate:staging-latest"""
}
}
}
stage('DEPLOY PINNER') {
when {
expression {
def pinnerStagingRunning = sh (script: 'docker ps -f name=concordia-pinner-staging | \
grep -qE concordia-pinner-staging', returnStatus: true)
return "${pinnerPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
sh 'docker stop concordia-pinner-staging || true \
&& docker rm concordia-pinner-staging || true'
sh 'if [ "$cleanSlateEnabled" -eq "0" ]; then \
docker volume rm concordia-pinner-staging || true; \
fi'
sh """docker run \
-d \
-v concordia-pinner-staging:/mnt/concordia/ \
-p 5555:5555 \
--env-file=./jenkins/env/pinner.staging.jenkins.env \
--name concordia-pinner-staging \
--net=ecentrics_concordia_staging_network \
ecentrics/concordia-pinner:staging-latest"""
}
}
}
stage('DEPLOY APP') {
when {
expression {
def pinnerStagingRunning = sh (script: 'docker ps -f name=concordia-app-staging | \
grep -qE concordia-app-staging', returnStatus: true)
return "${appPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
sh 'docker stop concordia-app-staging || true \
&& docker rm concordia-app-staging || true'
sh """docker run \
-itd \
-p 7000:3000 \
--env-file=./jenkins/env/concordia.staging.jenkins.env \
--name concordia-app-staging \
--net=ecentrics_concordia_staging_network \
ecentrics/concordia-app:staging-latest"""
}
}
}
}
}
stage('DEPLOY PRODUCTION') {
when {
branch 'master'
}
stages {
stage('PRODUCTION DEPLOY PREPARATION') {
steps {
script {
sh 'docker network create --driver bridge ecentrics_concordia_production_network || true'
def rendezvousServerRunning = sh (script: 'docker ps -f name=concordia-rendezvous | \
grep -qE concordia-rendezvous', returnStatus: true)
if ("$rendezvousServerRunning" == '1' || "$cleanSlateEnabled" == '0') {
sh 'docker stop concordia-rendezvous || true \
&& docker rm concordia-rendezvous || true'
sh 'docker run \
-d \
--env-file=./jenkins/env/rendezvous.jenkins.env \
-p 9090:9090 \
--name concordia-rendezvous \
--net=ecentrics_concordia_production_network \
libp2p/js-libp2p-webrtc-star:version-0.21.1'
} else {
sh 'docker network connect ecentrics_concordia_production_network concordia-rendezvous || true'
}
}
}
}
stage('DEPLOY CONTRACTS PROVIDER') {
when {
expression {
def contractsProviderProductionRunning = sh (script: 'docker ps -f name=concordia-contracts-provider-production | \
grep -qE concordia-contracts-provider-production', returnStatus: true)
return "${contractsProviderPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "$contractsProviderProductionRunning" == '1' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
sh 'docker stop concordia-contracts-provider-production || true \
&& docker rm concordia-contracts-provider-production || true'
sh 'if [ "$cleanSlateEnabled" -eq "0" ]; then \
docker volume rm concordia-contracts-provider-production || true; \
fi'
sh (script: """docker run \
-d \
-v concordia-contracts-provider-production:/mnt/concordia \
--env-file=./jenkins/env/contracts.provider.production.env \
-e NODE_ENV=production \
-p 8400:8400 \
--name concordia-contracts-provider-production \
--net=ecentrics_concordia_production_network \
ecentrics/concordia-contracts-provider:latest""")
}
}
}
stage('RECREATE GANACHE') {
when {
expression {
def ganacheProductionRunning = sh (script: 'docker ps -f name=concordia-ganache-production | \
grep -qE concordia-ganache-production', returnStatus: true)
return "$cleanSlateEnabled" == '0' || "$ganacheProductionRunning" == '1';
}
}
steps {
script {
sh 'docker stop concordia-ganache-production || true \
&& docker rm concordia-ganache-production || true'
sh 'docker volume rm concordia-ganache-production || true'
sh (script: 'docker run \
-d \
-v concordia-ganache-production:/mnt/concordia \
-p 8545:8545 \
--env-file=./jenkins/env/ganache.production.jenkins.env \
--name concordia-ganache-production \
--net=ecentrics_concordia_production_network \
ecentrics/concordia-ganache:latest')
// Ganache image might take a while to come alive
sleep 10
sh 'mkdir -p ./ganache/ && docker cp concordia-ganache-production:/mnt/concordia/ganache_keys/keys.json ./ganache/'
freshGanacheProductionRunning = true
}
}
}
stage('DEPLOY CONTRACTS') {
when {
expression {
return "${contractsPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "$freshGanacheProductionRunning" || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
sh """docker run \
--rm \
--env-file=./jenkins/env/contracts.production.jenkins.env \
-e CONTRACTS_VERSION_HASH=${contractsPackageVersion} \
--net=ecentrics_concordia_production_network \
ecentrics/concordia-contracts-migrate:latest"""
}
}
}
stage('DEPLOY PINNER') {
when {
expression {
def pinnerProductionRunning = sh (script: 'docker ps -f name=concordia-pinner-production | \
grep -qE concordia-pinner-production', returnStatus: true)
return "${pinnerPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "${pinnerProductionRunning}" == '1' || "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
sh 'docker stop concordia-pinner-production || true \
&& docker rm concordia-pinner-production || true'
sh 'if [ "$cleanSlateEnabled" -eq "0" ]; then \
docker volume rm concordia-pinner-production || true; \
fi'
sh """docker run \
-d \
-v concordia-pinner-production:/mnt/concordia \
-p 4444:4444 \
-e NODE_ENV=production \
--env-file=./jenkins/env/pinner.production.jenkins.env \
--name concordia-pinner-production \
--net=ecentrics_concordia_production_network \
ecentrics/concordia-pinner:latest"""
}
}
}
stage('DEPLOY APP') {
when {
expression {
def appProductionRunning = sh (script: 'docker ps -f name=concordia-app-production | \
grep -qE concordia-app-production', returnStatus: true)
return "${appPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' ||"${appProductionRunning}" == '1' || "${sharedPackageChanged}" == '0';
}
}
steps {
script {
sh 'docker stop concordia-app-production || true \
&& docker rm concordia-app-production || true'
sh """docker run \
-d \
-p 7777:80 \
--env-file=./jenkins/env/concordia.production.jenkins.env \
--name concordia-app-production \
--net=ecentrics_concordia_production_network \
ecentrics/concordia-app:latest"""
}
}
}
}
}
}
}

36
jenkins/check_package_changed.sh

@ -0,0 +1,36 @@
#!/bin/bash
# Based on this post:
# https://engineering.brigad.co/only-deploy-services-impacted-by-changes-in-a-mono-repository-18f54b8ac109
APP=$1
# Jenkins should provide these in the environment by default
GIT_COMMIT=$2
GIT_PREVIOUS_COMMIT=$3
ROOT_FILES_AND_FOLDERS=${4:-"package.json" "yarn.lock" ".dockerignore" "docker" "jenkins"}
function join_by() {
local IFS="$1"
shift
echo "$*"
}
function package_changed() {
git diff --name-only "$COMMIT_RANGE" | grep -qE "^packages/concordia-$1/" && echo true || echo false
}
if [ "$GIT_COMMIT" == "$GIT_PREVIOUS_COMMIT" ]; then
# Probably a manual re-run, set the range to just the last commit
COMMIT_RANGE="$GIT_COMMIT"
else
COMMIT_RANGE="$GIT_PREVIOUS_COMMIT...$GIT_COMMIT"
fi
ROOT_FILES_AND_FOLDERS_ARRAY=($ROOT_FILES_AND_FOLDERS)
ROOT_FILES_AND_FOLDERS_JOINED=$(join_by "|" ${ROOT_FILES_AND_FOLDERS_ARRAY[*]})
ROOT_FILES_CHANGED=$(git diff --name-only "$COMMIT_RANGE" | grep -qE "^($ROOT_FILES_AND_FOLDERS_JOINED)" && echo true || echo false)
IS_FORCE_BUILD=$(git log --oneline "$COMMIT_RANGE" | grep -qE "ci: force" && echo true || echo false)
APP_FILES_CHANGED=$(package_changed ${APP})
($IS_FORCE_BUILD || $ROOT_FILES_CHANGED || $APP_FILES_CHANGED) && echo 0 || echo 1

16
jenkins/env/concordia.production.jenkins.env

@ -0,0 +1,16 @@
VIRTUAL_HOST=concordia.ecentrics.net,www.concordia.ecentrics.net
VIRTUAL_PORT=7777
LETSENCRYPT_HOST=concordia.ecentrics.net,www.concordia.ecentrics.net
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com
# Variables needed in runtime (in browser)
REACT_APP_CONCORDIA_HOST=https://concordia.ecentrics.net
REACT_APP_CONCORDIA_PORT=443
REACT_APP_RENDEZVOUS_HOST=/dns4/rendezvous.ecentrics.net
REACT_APP_RENDEZVOUS_PORT=443
REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER=true
REACT_APP_CONTRACTS_PROVIDER_HOST=https://contracts.concordia.ecentrics.net
REACT_APP_CONTRACTS_PROVIDER_PORT=443
REACT_APP_CONTRACTS_VERSION_HASH=stable

16
jenkins/env/concordia.staging.jenkins.env

@ -0,0 +1,16 @@
VIRTUAL_HOST=staging.concordia.ecentrics.net,www.staging.concordia.ecentrics.net
VIRTUAL_PORT=7000
LETSENCRYPT_HOST=staging.concordia.ecentrics.net,www.staging.concordia.ecentrics.net
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com
# Variables needed in runtime (in browser)
REACT_APP_CONCORDIA_HOST=https://staging.concordia.ecentrics.net
REACT_APP_CONCORDIA_PORT=443
REACT_APP_RENDEZVOUS_HOST=/dns4/rendezvous.ecentrics.net
REACT_APP_RENDEZVOUS_PORT=443
REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER=true
REACT_APP_CONTRACTS_PROVIDER_HOST=https://staging.contracts.concordia.ecentrics.net
REACT_APP_CONTRACTS_PROVIDER_PORT=443
REACT_APP_CONTRACTS_VERSION_HASH=latest

8
jenkins/env/contracts.production.jenkins.env

@ -0,0 +1,8 @@
# Variables needed in runtime
MIGRATE_NETWORK=env
WEB3_HOST=concordia-ganache-production
WEB3_PORT=8545
CONTRACTS_PROVIDER_HOST=https://contracts.concordia.ecentrics.net
CONTRACTS_PROVIDER_PORT=443
CONTRACTS_VERSION_TAG=stable

9
jenkins/env/contracts.provider.production.env

@ -0,0 +1,9 @@
VIRTUAL_HOST=contracts.concordia.ecentrics.net,www.contracts.concordia.ecentrics.net
VIRTUAL_PORT=8400
LETSENCRYPT_HOST=contracts.concordia.ecentrics.net,www.contracts.concordia.ecentrics.net
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com
CONTRACTS_PROVIDER_PORT=8400
UPLOAD_CONTRACTS_DIRECTORY=/mnt/concordia/contracts/
LOGS_PATH=/mnt/concordia/logs/
CORS_ALLOWED_ORIGINS="https://concordia.ecentrics.net:443;https://concordia.ecentrics.net;http://127.0.0.1:4444;127.0.0.1:4444"

9
jenkins/env/contracts.provider.staging.env

@ -0,0 +1,9 @@
VIRTUAL_HOST=staging.contracts.concordia.ecentrics.net,www.staging.contracts.concordia.ecentrics.net
VIRTUAL_PORT=8450
LETSENCRYPT_HOST=staging.contracts.concordia.ecentrics.net,www.staging.contracts.concordia.ecentrics.net
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com
CONTRACTS_PROVIDER_PORT=8450
UPLOAD_CONTRACTS_DIRECTORY=/mnt/concordia/contracts/
LOGS_PATH=/mnt/concordia/logs/
CORS_ALLOWED_ORIGINS="https://staging.concordia.ecentrics.net:443;https://staging.concordia.ecentrics.net;http://127.0.0.1:5555;127.0.0.1:5555;https://www.staging.concordia.ecentrics.net:443;https://www.staging.concordia.ecentrics.net;http://www.127.0.0.1:5555;www.127.0.0.1:5555"

8
jenkins/env/contracts.staging.jenkins.env

@ -0,0 +1,8 @@
# Variables needed in runtime
MIGRATE_NETWORK=env
WEB3_HOST=concordia-ganache-staging
WEB3_PORT=8555
CONTRACTS_PROVIDER_HOST=https://staging.contracts.concordia.ecentrics.net
CONTRACTS_PROVIDER_PORT=443
CONTRACTS_VERSION_TAG=latest

4
jenkins/env/contracts.test.jenkins.env

@ -0,0 +1,4 @@
# Variables needed in runtime
MIGRATE_NETWORK=env
WEB3_HOST=concordia-ganache-test
WEB3_PORT=8546

10
jenkins/env/ganache.production.jenkins.env

@ -0,0 +1,10 @@
VIRTUAL_HOST=ganache.ecentrics.net,www.ganache.ecentrics.net
VIRTUAL_PORT=8545
LETSENCRYPT_HOST=ganache.ecentrics.net,www.LETSENCRYPT_HOST
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com
ACCOUNTS_NUMBER=1000
ACCOUNTS_ETHER=100000
HOST=0.0.0.0
PORT=8545
NETWORK_ID=5778

10
jenkins/env/ganache.staging.jenkins.env

@ -0,0 +1,10 @@
VIRTUAL_HOST=staging.ganache.ecentrics.net,www.staging.ganache.ecentrics.net
VIRTUAL_PORT=8555
LETSENCRYPT_HOST=staging.ganache.ecentrics.net,www.staging.ganache.ecentrics.net
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com
ACCOUNTS_NUMBER=100
ACCOUNTS_ETHER=1000
HOST=0.0.0.0
PORT=8555
NETWORK_ID=5778

6
jenkins/env/ganache.test.jenkins.env

@ -0,0 +1,6 @@
ACCOUNTS_NUMBER=5
ACCOUNTS_ETHER=1
MNEMONIC="myth like bonus scare over problem client lizard pioneer submit female collect"
HOST=0.0.0.0
PORT=8546
NETWORK_ID=5778

22
jenkins/env/pinner.production.jenkins.env

@ -0,0 +1,22 @@
VIRTUAL_HOST=pinner.concordia.ecentrics.net,www.pinner.concordia.ecentrics.net
VIRTUAL_PORT=4444
LETSENCRYPT_HOST=pinner.concordia.ecentrics.net,www.pinner.concordia.ecentrics.net
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com
USE_EXTERNAL_CONTRACTS_PROVIDER=true
ORBIT_DIRECTORY=/mnt/concordia/orbitdb
IPFS_DIRECTORY=/mnt/concordia/ipfs
LOGS_PATH=/mnt/concordia/logs/
CONTRACTS_PROVIDER_HOST=https://contracts.concordia.ecentrics.net
CONTRACTS_PROVIDER_PORT=443
CONTRACTS_VERSION_HASH=stable
PINNER_API_HOST=https://pinner.concordia.ecentrics.net
PINNER_API_PORT=443
RENDEZVOUS_HOST=/dns4/rendezvous.ecentrics.net
RENDEZVOUS_PORT=443
WEB3_HOST=ganache.ecentrics.net
WEB3_PORT=8545

22
jenkins/env/pinner.staging.jenkins.env

@ -0,0 +1,22 @@
VIRTUAL_HOST=staging.pinner.concordia.ecentrics.net,www.staging.pinner.concordia.ecentrics.net
VIRTUAL_PORT=5555
LETSENCRYPT_HOST=staging.pinner.concordia.ecentrics.net,www.staging.pinner.concordia.ecentrics.net
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com
USE_EXTERNAL_CONTRACTS_PROVIDER=true
ORBIT_DIRECTORY=/mnt/concordia/orbitdb
IPFS_DIRECTORY=/mnt/concordia/ipfs
LOGS_PATH=/mnt/concordia/logs/
CONTRACTS_PROVIDER_HOST=https://staging.contracts.concordia.ecentrics.net
CONTRACTS_PROVIDER_PORT=443
CONTRACTS_VERSION_HASH=latest
PINNER_API_HOST=https://staging.pinner.concordia.ecentrics.net
PINNER_API_PORT=443
RENDEZVOUS_HOST=/dns4/rendezvous.ecentrics.net
RENDEZVOUS_PORT=443
WEB3_HOST=staging.ganache.ecentrics.net
WEB3_PORT=8555

5
jenkins/env/rendezvous.jenkins.env

@ -0,0 +1,5 @@
VIRTUAL_HOST=rendezvous.ecentrics.net,www.rendezvous.ecentrics.net
VIRTUAL_PORT=9090
LETSENCRYPT_HOST=rendezvous.ecentrics.net,www.rendezvous.ecentrics.net
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com

18
jenkins/hash_build_properties.sh

@ -0,0 +1,18 @@
#!/bin/bash
# Outputs to the stdout a deterministically generated integer in the range 0-4095. The integer is generated using the
# input strings and SHA1.
# Usage: hash_build_properties.sh <branch> <build_number>
# Inputs:
# - branch: the branch being build
# - build_number the incrementing number of the build
BRANCH=$1
BUILD_NUMBER=$2
STRING_TO_HASH="$BRANCH-$BUILD_NUMBER"
SHA1_SUM_HEX=$(sha1sum <<<"$STRING_TO_HASH")
SHA1_TRUNCATED_HEX=$(cut -c1-3 <<<"$SHA1_SUM_HEX")
HASHED_STRING=$((0x${SHA1_TRUNCATED_HEX}))
echo "$HASHED_STRING"

5
jenkins/map_to_thousand.sh

@ -0,0 +1,5 @@
#!/bin/bash
INTEGER_TO_MAP=$1
echo $(( INTEGER_TO_MAP * 999 / 4095 ))

5
migrations/1_initial_migration.js

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

5
migrations/2_deploy_contracts.js

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

34
package.json

@ -1,29 +1,13 @@
{
"name": "apella",
"version": "0.1.0",
"name": "concordia",
"private": true,
"repository": {
"type": "git",
"url": "https://gitlab.com/Ezerous/Apella.git"
},
"devDependencies": {
"truffle-contract": "^3.0.4"
},
"dependencies": {
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-scripts": "1.1.1",
"react-redux": "^5.0.7",
"react-router": "3.2.1",
"react-router-redux": "^4.0.8",
"redux": "^3.7.2",
"redux-auth-wrapper": "1.1.0",
"redux-thunk": "^2.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/web3",
"**/web3/**"
]
}
}

0
packages/concordia-app/.dockerignore

11
packages/concordia-app/.env.development.example

@ -0,0 +1,11 @@
# This is an example development configuration for the app
# To create your own configuration, copy this one and ommit the ".example" from the filename, then change the
# environment cariables to the prefered values.
# Node dev-server host & port
HOST=localhost
PORT=7000
# Variables needed in runtime (in browser)
REACT_APP_RENDEZVOUS_HOST=127.0.0.1
REACT_APP_RENDEZVOUS_PORT=9090

60
packages/concordia-app/.eslintrc.js

@ -0,0 +1,60 @@
module.exports = {
'env': {
'browser': true,
'es6': true,
'jest': true
},
'extends': [
'plugin:react/recommended',
'airbnb'
],
'globals': {
'Atomics': 'readonly',
'SharedArrayBuffer': 'readonly'
},
parser: 'babel-eslint',
'parserOptions': {
'ecmaFeatures': {
'jsx': true
},
'ecmaVersion': 2018,
'sourceType': 'module'
},
'plugins': [
'react',
'react-hooks',
],
'rules': {
'react/jsx-props-no-spreading': 'off',
'import/extensions': 'off',
"react/jsx-indent": [
'error',
4,
{
checkAttributes: true,
indentLogicalExpressions: true
}
],
'react/require-default-props': 'off',
'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'max-len': ['warn', {'code': 120, 'tabWidth': 4}],
'no-unused-vars': 'warn',
'no-console': ['warn', {allow: ['warn', 'error']}],
'no-shadow': 'warn',
"no-multi-str": "warn",
"jsx-a11y/label-has-associated-control": [2, {
"labelAttributes": ["label"],
"controlComponents": ["Input"],
"depth": 3,
}],
},
'settings': {
'import/resolver': {
'node': {
'extensions': ['.js', '.jsx']
}
}
},
};

22
packages/concordia-app/.gitignore

@ -0,0 +1,22 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
.env.local
.env.development
.env.development.local
.env.test
.env.test.local
.env.production
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

36
packages/concordia-app/CONCORDIA.md

@ -0,0 +1,36 @@
# Concordia
The Concordia application is a React app that handles interactions with the contracts and the distributed database used.
This document provides information about the Concordia React Application and how to get it running for development.
## Prerequisites
Concordia requires at the minimum two services to work, a blockchain and a rendezvous server.
Furthermore, the deployed contract artifacts must be made available. This can be done in two ways, migrate the contracts
and make sure the artifacts are present in the `concordia/packages/concordia-contracts/build` directory, or migrate the
contracts and upload the artifacts to a contracts-provider instance.
## Running Concordia
To start the application in development mode simply execute the `start` script:
```shell
yarn start
```
The application makes use of the environment variables described below.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| REACT_APP_CONCORDIA_HOST | `127.0.0.1` | Set the hostname of the concordia application |
| REACT_APP_CONCORDIA_PORT | `7000` | Set the port of the concordia application |
| REACT_APP_RENDEZVOUS_HOST | `/ip4/127.0.0.1` | Set the hostname of the rendezvous server |
| REACT_APP_RENDEZVOUS_PORT | `9090` | Set the port of the rendezvous server |
| REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER | `false` | Enable/Disable use of external contracts provider |
| REACT_APP_CONTRACTS_PROVIDER_HOST | `http://127.0.0.1` | Set the hostname of the contracts provider service |
| REACT_APP_CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider service |
| REACT_APP_CONTRACTS_VERSION_HASH | `latest` | Set the contracts tag that will be pulled |
**Attention**: if using a contracts provider, make sure that the provider is set to allow CORS from the host-port combo
defined by `REACT_APP_CONCORDIA_HOST` and `REACT_APP_CONCORDIA_PORT`.

71
packages/concordia-app/package.json

@ -0,0 +1,71 @@
{
"name": "concordia-app",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts start",
"build": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts build",
"test": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts test",
"eject": "react-scripts eject",
"postinstall": "patch-package",
"analyze": "react-scripts build && source-map-explorer 'build/static/js/*.js' --gzip",
"lint": "eslint --ext js,jsx . --format table"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@ecentrics/breeze": "~0.7.0",
"@ecentrics/drizzle": "~0.5.0",
"@ecentrics/eth-identity-provider": "~0.1.2",
"@reduxjs/toolkit": "~1.4.0",
"@welldone-software/why-did-you-render": "~6.0.5",
"apexcharts": "^3.26.0",
"concordia-contracts": "~0.1.0",
"concordia-shared": "~0.1.0",
"crypto-js": "~4.0.0",
"i18next": "^19.8.3",
"i18next-browser-languagedetector": "^6.0.1",
"i18next-http-backend": "^1.0.21",
"lodash": "^4.17.20",
"prop-types": "~15.7.2",
"react": "~16.13.1",
"react-apexcharts": "^1.3.7",
"react-avatar": "~3.9.7",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "~16.13.1",
"react-i18next": "^11.7.3",
"react-markdown": "^5.0.3",
"react-particles-js": "^3.4.0",
"react-redux": "~7.2.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "~3.4.3",
"react-timeago": "~5.2.0",
"redux-saga": "~1.1.3",
"semantic-ui-css": "~2.4.1",
"semantic-ui-react": "~2.0.3",
"web3": "~1.3.3"
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.1.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.2.0",
"patch-package": "~6.2.2",
"postinstall-postinstall": "~2.1.0",
"source-map-explorer": "~2.5.0"
}
}

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

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

0
packages/concordia-app/public/environment.js

BIN
packages/concordia-app/public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

@ -6,10 +6,10 @@
<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/
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">
<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.
@ -19,12 +19,14 @@
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>
<title>Concordia</title>
<!--
Allow access to runtime env vars after build.
-->
<script src="%PUBLIC_URL%/environment.js"></script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.

106
packages/concordia-app/public/locales/en/translation.json

@ -0,0 +1,106 @@
{
"board.header.no.topics.message": "There are no topics yet!",
"board.sub.header.no.topics.guest": "Sign up and be the first to post.",
"board.sub.header.no.topics.user": "Be the first to post.",
"clear.databases.modal.cancel.button": "Cancel, keep databases",
"clear.databases.modal.clear.button": "Yes, delete databases",
"clear.databases.modal.clearing.progress.message": "This might take a minute...",
"clear.databases.modal.clearing.progress.title": "Clearing all Concordia databases",
"clear.databases.modal.description.body.user": "Although this action is generally recoverable some of your topics and posts may be permanently lost.",
"clear.databases.modal.description.pre.guest": "You are about to clear the Concordia databases stored locally in your browser.",
"clear.databases.modal.description.pre.user": "Be careful, {{username}}! You are about to clear the Concordia databases stored locally in your browser.",
"clear.databases.modal.form.username.label.guest": "Please type Concordia to confirm.",
"clear.databases.modal.form.username.label.user": "Please type your username to confirm.",
"clear.databases.modal.title": "Clear all Concordia databases. Are you sure?",
"custom.loading.tab.pane.default.generic.message": "Magic in the background",
"edit.information.modal.form.cancel.button": "Cancel",
"edit.information.modal.form.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.",
"edit.information.modal.form.error.message.header": "Form contains errors",
"edit.information.modal.form.location.field.label": "Location",
"edit.information.modal.form.location.field.placeholder": "Location",
"edit.information.modal.form.profile.picture.field.label": "Profile picture URL",
"edit.information.modal.form.profile.picture.field.placeholder": "URL",
"edit.information.modal.form.submit.button": "Submit",
"edit.information.modal.title": "Edit profile information",
"poll.create.add.option.button": "Add Option",
"poll.create.allow.vote.changes.field.label": "Allow vote changes",
"poll.create.option.field.label": "Option #{{id}}",
"poll.create.option.field.placeholder": "Option #{{id}}",
"poll.create.question.field.label": "Poll Question",
"poll.create.question.field.placeholder": "Question",
"post.create.form.send.button": "Post",
"post.form.content.field.placeholder": "Message",
"post.form.subject.field.placeholder": "Subject",
"post.list.row.post.id": "#{{id}}",
"profile.general.tab.address.row.title": "Account address:",
"profile.general.tab.clear.databases.button.title": "Clear databases",
"profile.general.tab.edit.info.button.title": "Edit information",
"profile.general.tab.location.row.not.set": "Not set",
"profile.general.tab.location.row.title": "Location:",
"profile.general.tab.number.of.posts.row.title": "Number of posts:",
"profile.general.tab.number.of.topics.row.title": "Number of topics created:",
"profile.general.tab.polls.db.address.row.title": "PollsDB:",
"profile.general.tab.posts.db.address.row.title": "PostsDB:",
"profile.general.tab.registration.date.row.title": "Member since:",
"profile.general.tab.save.info.button.title": "Save information",
"profile.general.tab.title": "General",
"profile.general.tab.topics.db.address.row.title": "TopicsDB:",
"profile.general.tab.user.db.address.row.title": "UserDB:",
"profile.general.tab.username.row.title": "Username:",
"profile.posts.tab.title": "Posts",
"profile.topics.tab.title": "Topics",
"profile.user.has.no.posts.header.message": "{{user}} has not posted yet",
"profile.user.has.no.topics.header.message": "{{user}} has created no topics yet",
"register.card.header": "Sign Up",
"register.form.button.back": "Back",
"register.form.header.already.member.message": "There is already an account for this address.\nIf you want to create another account please change your address.",
"register.form.personal.information.step.button.skip": "Skip for now",
"register.form.personal.information.step.button.submit": "Submit",
"register.form.personal.information.step.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.",
"register.form.personal.information.step.error.message.header": "Form contains errors",
"register.form.personal.information.step.location.field.label": "Location",
"register.form.personal.information.step.location.field.placeholder": "Location",
"register.form.personal.information.step.profile.picture.field.label": "Profile picture URL",
"register.form.personal.information.step.profile.picture.field.placeholder": "URL",
"register.form.profile.information.step.description": "Give a hint about who you are",
"register.form.profile.information.step.title": "Profile Information",
"register.form.sign.up.step.button.guest": "Continue as guest",
"register.form.sign.up.step.button.submit": "Sign Up",
"register.form.sign.up.step.description": "Create a Concordia account",
"register.form.sign.up.step.error.message.header": "Form contains errors",
"register.form.sign.up.step.title": "Sign Up",
"register.p.account.address": "Account address:",
"topbar.button.about": "About",
"topbar.button.clear.databases": "Clear databases",
"topbar.button.create.topic": "Create topic",
"topbar.button.profile": "Profile",
"topbar.button.register": "Sign Up",
"topic.create.form.add.poll.button": "Add Poll",
"topic.create.form.content.field.label": "First post content",
"topic.create.form.content.field.placeholder": "Message",
"topic.create.form.post.button": "Create Topic",
"topic.create.form.remove.poll.button": "Remove Poll",
"topic.create.form.subject.field.label": "Topic subject",
"topic.create.form.subject.field.placeholder": "Subject",
"topic.list.row.topic.id": "#{{id}}",
"topic.poll.guest.header": "Only registered users are able to vote in polls.",
"topic.poll.guest.sub.header.link": "signup",
"topic.poll.guest.sub.header.post": " page.",
"topic.poll.guest.sub.header.pre": "You can register in the ",
"topic.poll.invalid.data.header": "This topic has a poll but the data are untrusted!",
"topic.poll.invalid.data.sub.header": "The poll data downloaded from the poster have been tampered with.",
"topic.poll.tab.graph.title": "Results",
"topic.poll.tab.results.vote": "Vote",
"topic.poll.tab.results.votes": "Votes",
"topic.poll.tab.results.no.votes": "No one has voted yet!",
"topic.poll.tab.results.user.vote": "You voted: ",
"topic.poll.tab.vote.no.changes": "This poll does not allow vote changes.",
"topic.poll.tab.vote.form.button.submit": "Submit",
"topic.poll.tab.vote.form.button.unvote": "Remove Vote",
"topic.poll.tab.vote.form.radio.label": "Select one of the options:",
"topic.poll.tab.vote.title": "Vote",
"username.selector.error.username.empty.message": "Username is required",
"username.selector.error.username.taken.message": "The username {{username}} is already taken.",
"username.selector.username.field.label": "Username",
"username.selector.username.field.placeholder": "Username"
}

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

@ -1,6 +1,6 @@
{
"short_name": "Apella",
"name": "Apella",
"short_name": "Concordia",
"name": "Concordia",
"icons": [
{
"src": "favicon.ico",
@ -8,7 +8,7 @@
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"

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

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

25
packages/concordia-app/src/App.jsx

@ -0,0 +1,25 @@
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import PropTypes from 'prop-types';
import InitializationScreen from './components/InitializationScreen';
import Routes from './Routes';
import './intl/index';
import 'semantic-ui-css/semantic.min.css';
const App = ({ store }) => (
<Provider store={store}>
<InitializationScreen>
<Router>
<Routes />
</Router>
</InitializationScreen>
</Provider>
);
App.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
store: PropTypes.object.isRequired,
};
export default App;

22
packages/concordia-app/src/ErrorBoundary.jsx

@ -0,0 +1,22 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
const { props: { children }, state: { hasError } } = this;
if (hasError) {
return <h1>Something went wrong.</h1>; // TODO: Make a better "Something went wrong" screen
}
return children;
}
}
export default ErrorBoundary;

95
packages/concordia-app/src/Routes.jsx

@ -0,0 +1,95 @@
import React, { Fragment, lazy, Suspense } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import LoadingScreen from './components/LoadingScreen';
import RegisterLayout from './layouts/RegisterLayout';
const routesConfig = [
{
exact: true,
path: '/',
component: () => <Redirect to="/home" />,
},
{
exact: true,
path: '/404',
layout: MainLayout,
component: lazy(() => import('./components/NotFound')),
},
{
path: '/auth',
layout: RegisterLayout,
routes: [
{
exact: true,
path: '/auth/register',
component: lazy(() => import('./views/Register')),
},
{
component: () => <Redirect to="/404" />,
},
],
},
{
path: '*',
layout: MainLayout,
routes: [
{
exact: true,
path: '/home',
component: lazy(() => import('./views/Home')),
},
{
exact: true,
path: '/about',
component: lazy(() => import('./views/About')),
},
{
exact: true,
path: '/topics/:id(\\bnew\\b|\\d+)',
component: lazy(() => import('./views/Topic')),
},
{
exact: true,
path: ['/users/:id', '/profiles/:id', '/profile'],
component: lazy(() => import('./views/Profile')),
},
{
component: () => <Redirect to="/404" />,
},
],
},
];
const renderRoutes = (routes) => (routes ? (
<Suspense fallback={<LoadingScreen />}>
<Switch>
{routes.map((route, i) => {
const Layout = route.layout || Fragment;
const Component = route.component;
const key = route.path ? route.path.concat(i) : ''.concat(i);
return (
<Route
key={key}
path={route.path}
exact={route.exact}
render={(props) => (
<Layout>
{route.routes
? renderRoutes(route.routes)
: <Component {...props} />}
</Layout>
)}
/>
);
})}
</Switch>
</Suspense>
) : null);
function Routes() {
return renderRoutes(routesConfig);
}
export default Routes;

35
packages/concordia-app/src/assets/About.md

@ -0,0 +1,35 @@
# About Concordia
## What
Concordia is a forum platform (remember forums? 🤩) that focuses on user privacy and direct democratic voting. It is a
FOSS distributed via its Gitlab [repository][concordia-repository] and Docker [repository][concordia-docker-hub] under
the [MIT][concordia-license] license.
## Why
The value of privacy, freedom of speech and democracy are diminishing in modern software. Even more so in social media
platforms. Users are called to select between being the product of companies that sell their personal information and
being shut out of the modern, digital society.
Concordia, much like other projects of this kind, provides an alternative to this predicament.
## How
Concordia uses decentralized technologies, namely the Ethereum blockchain and its smart contracts, as well as the
distributed database OrbitDB that's based on the decentralized network IPFS. These technologies make Concordia
impervious to censorship and guaranty the immutability of user information and anonymity while enabling user
authentication that makes trusted, direct voting possible.
You can read more about the technological stack in Concordia's [whitepaper][concordia-whitepaper].
---
Developed by [apostolof][devs-apostolof-profile], [ezerous][devs-ezerous-profile]
[concordia-repository]: https://gitlab.com/ecentrics/concordia
[concordia-docker-hub]: https://hub.docker.com/repository/docker/ecentrics/concordia-app
[concordia-license]: https://gitlab.com/ecentrics/concordia/-/blob/master/LICENSE.md
[devs-apostolof-profile]: https://gitlab.com/Apostolof
[devs-ezerous-profile]: https://gitlab.com/Ezerous
[concordia-whitepaper]: https://whitepaper.concordia.ecentrics.net

72
packages/concordia-app/src/assets/css/index.css

@ -0,0 +1,72 @@
:root {
--primary-color: #EA6954;
--primary-color-highlighted: #DB5844;
--secondary-color: #0B2540;
--secondary-color-highlighted: #061A30;
}
body {
overflow: auto;
margin: 0;
background: white;
}
div {
word-break: break-word;
}
#root {
height: 100%;
}
.i18next-newlines {
white-space: pre-line !important;
}
.text-secondary {
color: gray;
font-style: italic;
}
.primary-button{
color: white !important;
background-color: var(--primary-color) !important;
}
.primary-button:hover {
background-color: var(--primary-color-highlighted) !important;
}
.secondary-button{
color: white !important;
background-color: var(--secondary-color) !important;
}
.secondary-button:hover {
background-color: var(--secondary-color-highlighted) !important;
}
.skip-button {
color: var(--secondary-color) !important;
background-color: white !important;
box-shadow: 0 0 0 1px var(--secondary-color) inset !important;
}
.skip-button:hover {
color: var(--secondary-color-highlighted) !important;
box-shadow: 0 0 0 1px var(--secondary-color-highlighted) inset !important;
}
.unselectable {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
textarea, input, button, select {
font-family: inherit;
font-size: inherit;
}

BIN
packages/concordia-app/src/assets/images/PageNotFound.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

1
packages/concordia-app/src/assets/images/app_logo.svg

@ -0,0 +1 @@
<svg id="e12732d2-a5d8-4802-8cce-7bb947dee9fc" data-name="Main" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330.81 373.92"><defs><style>.ea73fbdf-f048-4e97-bc07-bb0b2969bee0,.f07aef9f-38fd-428f-bc73-47ca525dc184,.f4a4d906-3c6f-4179-bb65-49dff726c11f{fill:#ea6954;}.afa3e173-b557-4a70-97cf-07566c42bb6b,.ea73fbdf-f048-4e97-bc07-bb0b2969bee0,.eb772659-6642-46b7-9d47-de390253f8eb,.f07aef9f-38fd-428f-bc73-47ca525dc184,.f4a4d906-3c6f-4179-bb65-49dff726c11f{stroke:#ea6954;stroke-miterlimit:10;}.ea73fbdf-f048-4e97-bc07-bb0b2969bee0{stroke-width:4px;}.afa3e173-b557-4a70-97cf-07566c42bb6b,.eb772659-6642-46b7-9d47-de390253f8eb{fill:none;}.eb772659-6642-46b7-9d47-de390253f8eb{stroke-width:8px;}.afa3e173-b557-4a70-97cf-07566c42bb6b{stroke-width:5px;}.f4a4d906-3c6f-4179-bb65-49dff726c11f{stroke-width:6px;}.f07aef9f-38fd-428f-bc73-47ca525dc184{stroke-width:10px;}</style></defs><title>concordia_logo_clean_transp</title><g id="b83a01e3-c11e-43ae-b1d1-41016566a065" data-name="Thin lines"><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="6.54" y1="159.57" x2="214.18" y2="40.02"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="268.01" y1="67.02" x2="8.19" y2="217.01"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M147.33,310.48" transform="translate(-84.98 -63.19)"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M93.17,340.61" transform="translate(-84.98 -63.19)"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M194.38,339.38" transform="translate(-84.98 -63.19)"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M143.26,369.07" transform="translate(-84.98 -63.19)"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="164.08" y1="307.55" x2="218.73" y2="276.32"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="164.08" y1="307.55" x2="164.08" y2="307.55"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M250.4,70.47" transform="translate(-84.98 -63.19)"/><path class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" d="M250.4,131.07" transform="translate(-84.98 -63.19)"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="214.18" y1="40.02" x2="213.85" y2="98.18"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="268.07" y1="72.66" x2="268.07" y2="128.48"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="61.62" y1="248.08" x2="60.76" y2="251.27"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="109.4" y1="276.19" x2="109.4" y2="276.19"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="60.76" y1="185.89" x2="60.76" y2="246.48"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="164.08" y1="307.55" x2="8.13" y2="216.98"/><line class="ea73fbdf-f048-4e97-bc07-bb0b2969bee0" x1="164.39" y1="8.19" x2="164.08" y2="307.55"/></g><g id="fed0cce0-4af9-4b2d-b863-61c942ac6516" data-name="Thick lines"><polyline class="eb772659-6642-46b7-9d47-de390253f8eb" points="8.19 217.19 8.93 99.77 164.08 8.19"/><polyline class="eb772659-6642-46b7-9d47-de390253f8eb" points="322.52 98.08 268.38 130.07 164.08 69.47 61.56 128.48 61.56 186.08"/><polyline class="eb772659-6642-46b7-9d47-de390253f8eb" points="218.73 276.32 322.29 215.69 321.5 276.78 164.08 369.28 8.19 277.19"/></g><g id="fc525e57-c3b6-4537-a6f4-917e8af7caa9" data-name="Medium lines"><polyline class="afa3e173-b557-4a70-97cf-07566c42bb6b" points="164.08 8.19 164.08 68.78 214.18 40.02 214.18 98.18 268.27 67.88 268.28 130.28"/><polyline class="afa3e173-b557-4a70-97cf-07566c42bb6b" points="8.19 277.19 61.62 248.08 61.62 304.28 109.4 276.19 109.4 336.28 164.06 307.55 164.06 369.28"/></g><g id="b1c4230e-0310-46b2-a1fb-d6a53c5bc9b8" data-name="Small nodes"><circle class="f4a4d906-3c6f-4179-bb65-49dff726c11f" cx="268.28" cy="67.88" r="3.19"/><circle class="f4a4d906-3c6f-4179-bb65-49dff726c11f" cx="109.4" cy="276.19" r="3.19"/></g><g id="ef16cbf3-3f8b-4797-841e-58de76523415" data-name="Big nodes"><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="61.56" cy="186.08" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="164.08" cy="8.19" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="164.08" cy="307.55" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="8.19" cy="217.01" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="61.62" cy="248.08" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="322.62" cy="98.08" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="214.18" cy="40.02" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="218.73" cy="276.32" r="3.19"/><circle class="f07aef9f-38fd-428f-bc73-47ca525dc184" cx="8.19" cy="277.19" r="3.19"/></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

1
packages/concordia-app/src/assets/images/app_logo_circle.svg

@ -0,0 +1 @@
<svg id="b37e941a-4c43-4ef7-9a51-e1598f898259" data-name="Main" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 537.4 537.4"><defs><style>.ac1237be-2158-4fc0-ae9f-071b2ff57398{fill:#0b2540;}.b45f1c22-1b20-401d-9f53-3289cddfc3c4,.be317747-31f7-43a4-9fba-03772684f044,.be531e54-2bea-4919-990d-82f88a84fa29{fill:#ea6954;}.a4966b61-2d1b-4dfd-b016-51913d3feac9,.b45f1c22-1b20-401d-9f53-3289cddfc3c4,.be317747-31f7-43a4-9fba-03772684f044,.be531e54-2bea-4919-990d-82f88a84fa29,.e5dfb83f-a67f-4542-9762-d2465cc0a2c6{stroke:#ea6954;stroke-miterlimit:10;}.b45f1c22-1b20-401d-9f53-3289cddfc3c4{stroke-width:4px;}.a4966b61-2d1b-4dfd-b016-51913d3feac9,.e5dfb83f-a67f-4542-9762-d2465cc0a2c6{fill:none;}.a4966b61-2d1b-4dfd-b016-51913d3feac9{stroke-width:8px;}.e5dfb83f-a67f-4542-9762-d2465cc0a2c6{stroke-width:5px;}.be317747-31f7-43a4-9fba-03772684f044{stroke-width:6px;}.be531e54-2bea-4919-990d-82f88a84fa29{stroke-width:10px;}</style></defs><title>concordia_logo_clean</title><g id="aa7412e7-d10f-4a17-bb5d-8cce7f8be73c" data-name="Background"><circle class="ac1237be-2158-4fc0-ae9f-071b2ff57398" cx="268.7" cy="268.7" r="268.7"/></g><g id="b4a70f8e-cb9b-4d04-a6af-3bbf6423659c" data-name="Thin lines"><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="110.22" y1="241.47" x2="317.86" y2="121.92"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="371.69" y1="148.92" x2="111.87" y2="298.91"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M166,329.18"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M111.87,359.31"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M213.08,358.08"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M162,387.77"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="267.76" y1="389.44" x2="322.41" y2="358.21"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="267.76" y1="389.44" x2="267.76" y2="389.44"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M269.1,89.17"/><path class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" d="M269.1,149.77"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="317.86" y1="121.92" x2="317.53" y2="180.07"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="371.75" y1="154.56" x2="371.75" y2="210.37"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="165.3" y1="329.98" x2="164.44" y2="333.16"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="213.08" y1="358.08" x2="213.08" y2="358.08"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="164.44" y1="267.78" x2="164.44" y2="328.38"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="267.76" y1="389.44" x2="111.81" y2="298.88"/><line class="b45f1c22-1b20-401d-9f53-3289cddfc3c4" x1="268.07" y1="90.08" x2="267.76" y2="389.44"/></g><g id="ad2424b4-f499-4d57-ba69-57371a617f7c" data-name="Thick lines"><polyline class="a4966b61-2d1b-4dfd-b016-51913d3feac9" points="111.87 299.08 112.61 181.66 267.76 90.08"/><polyline class="a4966b61-2d1b-4dfd-b016-51913d3feac9" points="426.2 179.98 372.06 211.97 267.76 151.37 165.24 210.37 165.24 267.97"/><polyline class="a4966b61-2d1b-4dfd-b016-51913d3feac9" points="322.41 358.21 425.97 297.59 425.18 358.68 267.76 451.17 111.87 359.08"/></g><g id="b157ebcc-6b21-403c-8fdd-bc0b20aa202f" data-name="Medium lines"><polyline class="e5dfb83f-a67f-4542-9762-d2465cc0a2c6" points="267.76 90.08 267.76 150.67 317.86 121.92 317.86 180.07 371.96 149.77 371.96 212.17"/><polyline class="e5dfb83f-a67f-4542-9762-d2465cc0a2c6" points="111.87 359.08 165.3 329.98 165.3 386.17 213.08 358.08 213.08 418.17 267.74 389.44 267.74 451.17"/></g><g id="bab675c2-16d2-4e69-a23f-8e6406c1997e" data-name="Small nodes"><circle class="be317747-31f7-43a4-9fba-03772684f044" cx="371.96" cy="149.77" r="3.19"/><circle class="be317747-31f7-43a4-9fba-03772684f044" cx="213.08" cy="358.08" r="3.19"/></g><g id="b3217dd3-e6fd-4786-bb13-4cd0eac59017" data-name="Big nodes"><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="165.24" cy="267.97" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="267.76" cy="90.08" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="267.76" cy="389.44" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="111.87" cy="298.91" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="165.3" cy="329.98" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="426.3" cy="179.98" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="317.86" cy="121.92" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="322.41" cy="358.21" r="3.19"/><circle class="be531e54-2bea-4919-990d-82f88a84fa29" cx="111.87" cy="359.08" r="3.19"/></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

1
packages/concordia-app/src/assets/images/ethereum_logo.svg

@ -0,0 +1 @@
<svg height="64" viewBox="0 0 49.91744 49.931196" width="64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m720.6 306.4h508.7v266h-508.7z" height="100%" width="100%"/></clipPath><clipPath id="b"><path d="m720.6 0h254.4v572.4h-254.4z" height="100%" width="100%"/></clipPath><clipPath id="c"><path d="m975 0h254.4v572.4h-254.4z" height="100%" width="100%"/></clipPath><clipPath id="d"><path d="m720.6 470.3h254.4v358.4h-254.4z" height="100%" width="100%"/></clipPath><clipPath id="e"><path d="m975 470.3h254.5v358.4h-254.5z" height="100%" width="100%"/></clipPath><g fill="#010101"><path clip-path="url(#a)" d="m975 306.4-254.4 115.7 254.4 150.3 254.3-150.3z" opacity=".6" transform="matrix(.06025243658 0 0 .06025243658 -33.79041826347 .00000056684)"/><path clip-path="url(#b)" d="m720.6 422.1 254.4 150.3v-572.4z" opacity=".45" transform="matrix(.06025243658 0 0 .06025243658 -33.79041826347 .00000056684)"/><path clip-path="url(#c)" d="m975 0v572.4l254.3-150.3z" opacity=".8" transform="matrix(.06025243658 0 0 .06025243658 -33.79041826347 .00000056684)"/><path clip-path="url(#d)" d="m720.6 470.3 254.4 358.4v-208.1z" opacity=".45" transform="matrix(.06025243658 0 0 .06025243658 -33.79041826347 .00000056684)"/><path clip-path="url(#e)" d="m975 620.6v208.1l254.5-358.4z" opacity=".8" transform="matrix(.06025243658 0 0 .06025243658 -33.79041826347 .00000056684)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

20
packages/concordia-app/src/assets/images/ipfs_logo.svg

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" style="enable-background:new" xmlns="http://www.w3.org/2000/svg" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" height="512" width="512" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 511.99999 511.99998" xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs>
<linearGradient id="b" y2="771.51" gradientUnits="userSpaceOnUse" x2="527.72" y1="771.51" x1="84.315">
<stop stop-color="#4a9ea1" offset="0"/>
</linearGradient>
<linearGradient id="a" y2="771.48" gradientUnits="userSpaceOnUse" x2="512.36" y1="771.48" x1="99.675">
<stop stop-color="#63d3d7" offset="0"/>
</linearGradient>
</defs>
<g transform="translate(-50.017 -515.51)">
<path d="m84.315 899.51 221.7 128 221.7-128v-256l-221.7-127.99-221.7 128z" fill="url(#b)"/>
<path fill="url(#a)" d="m283.13 546.35-160.74 92.806c0.32126 2.8543 0.32125 5.7352 0 8.5894l160.75 92.806c13.554-10.001 32.043-10.001 45.597 0l160.75-92.807c-0.32126-2.8543-0.32293-5.7338-0.001-8.588l-160.74-92.806c-13.554 10.001-32.044 10.001-45.599 0zm221.79 127.03-160.92 93.84c1.884 16.739-7.3611 32.751-22.799 39.489l0.18062 184.58c2.6325 1.1489 5.1267 2.5886 7.438 4.294l160.75-92.805c-1.884-16.739 7.3611-32.752 22.799-39.49v-185.61c-2.6325-1.1489-5.1281-2.5886-7.4394-4.294zm-397.81 1.0315c-2.3112 1.7054-4.8054 3.1465-7.438 4.2954v185.61c15.438 6.7378 24.683 22.75 22.799 39.489l160.74 92.806c2.3112-1.7054 4.8069-3.1465 7.4394-4.2954v-185.61c-15.438-6.7378-24.683-22.75-22.799-39.489l-160.74-92.81z"/>
</g>
<g transform="translate(0 -196.66)">
<path d="m256 708.66 221.7-128v-256l-221.7 128v256z" fill-opacity=".25098"/>
<path d="m256 708.66v-256l-221.7-128v256l221.7 128z" fill-opacity=".039216"/>
<path d="m34.298 324.66 221.7 128 221.7-128-221.7-128-221.7 128z" fill-opacity=".13018"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

1
packages/concordia-app/src/assets/images/metamask_logo.svg

@ -0,0 +1 @@
<svg fill="none" height="33" viewBox="0 0 35 33" width="35" xmlns="http://www.w3.org/2000/svg"><g stroke-linecap="round" stroke-linejoin="round" stroke-width=".25"><path d="m32.9582 1-13.1341 9.7183 2.4424-5.72731z" fill="#e17726" stroke="#e17726"/><g fill="#e27625" stroke="#e27625"><path d="m2.66296 1 13.01714 9.809-2.3254-5.81802z"/><path d="m28.2295 23.5335-3.4947 5.3386 7.4829 2.0603 2.1436-7.2823z"/><path d="m1.27281 23.6501 2.13055 7.2823 7.46994-2.0603-3.48166-5.3386z"/><path d="m10.4706 14.5149-2.0786 3.1358 7.405.3369-.2469-7.969z"/><path d="m25.1505 14.5149-5.1575-4.58704-.1688 8.05974 7.4049-.3369z"/><path d="m10.8733 28.8721 4.4819-2.1639-3.8583-3.0062z"/><path d="m20.2659 26.7082 4.4689 2.1639-.6105-5.1701z"/></g><path d="m24.7348 28.8721-4.469-2.1639.3638 2.9025-.039 1.231z" fill="#d5bfb2" stroke="#d5bfb2"/><path d="m10.8732 28.8721 4.1572 1.9696-.026-1.231.3508-2.9025z" fill="#d5bfb2" stroke="#d5bfb2"/><path d="m15.1084 21.7842-3.7155-1.0884 2.6243-1.2051z" fill="#233447" stroke="#233447"/><path d="m20.5126 21.7842 1.0913-2.2935 2.6372 1.2051z" fill="#233447" stroke="#233447"/><path d="m10.8733 28.8721.6495-5.3386-4.13117.1167z" fill="#cc6228" stroke="#cc6228"/><path d="m24.0982 23.5335.6366 5.3386 3.4946-5.2219z" fill="#cc6228" stroke="#cc6228"/><path d="m27.2291 17.6507-7.405.3369.6885 3.7966 1.0913-2.2935 2.6372 1.2051z" fill="#cc6228" stroke="#cc6228"/><path d="m11.3929 20.6958 2.6242-1.2051 1.0913 2.2935.6885-3.7966-7.40495-.3369z" fill="#cc6228" stroke="#cc6228"/><path d="m8.392 17.6507 3.1049 6.0513-.1039-3.0062z" fill="#e27525" stroke="#e27525"/><path d="m24.2412 20.6958-.1169 3.0062 3.1049-6.0513z" fill="#e27525" stroke="#e27525"/><path d="m15.797 17.9876-.6886 3.7967.8704 4.4833.1949-5.9087z" fill="#e27525" stroke="#e27525"/><path d="m19.8242 17.9876-.3638 2.3584.1819 5.9216.8704-4.4833z" fill="#e27525" stroke="#e27525"/><path d="m20.5127 21.7842-.8704 4.4834.6236.4406 3.8584-3.0062.1169-3.0062z" fill="#f5841f" stroke="#f5841f"/><path d="m11.3929 20.6958.104 3.0062 3.8583 3.0062.6236-.4406-.8704-4.4834z" fill="#f5841f" stroke="#f5841f"/><path d="m20.5906 30.8417.039-1.231-.3378-.2851h-4.9626l-.3248.2851.026 1.231-4.1572-1.9696 1.4551 1.1921 2.9489 2.0344h5.0536l2.962-2.0344 1.442-1.1921z" fill="#c0ac9d" stroke="#c0ac9d"/><path d="m20.2659 26.7082-.6236-.4406h-3.6635l-.6236.4406-.3508 2.9025.3248-.2851h4.9626l.3378.2851z" fill="#161616" stroke="#161616"/><path d="m33.5168 11.3532 1.1043-5.36447-1.6629-4.98873-12.6923 9.3944 4.8846 4.1205 6.8983 2.0085 1.52-1.7752-.6626-.4795 1.0523-.9588-.8054-.622 1.0523-.8034z" fill="#763e1a" stroke="#763e1a"/><path d="m1 5.98873 1.11724 5.36447-.71451.5313 1.06527.8034-.80545.622 1.05228.9588-.66255.4795 1.51997 1.7752 6.89835-2.0085 4.8846-4.1205-12.69233-9.3944z" fill="#763e1a" stroke="#763e1a"/><path d="m32.0489 16.5234-6.8983-2.0085 2.0786 3.1358-3.1049 6.0513 4.1052-.0519h6.1318z" fill="#f5841f" stroke="#f5841f"/><path d="m10.4705 14.5149-6.89828 2.0085-2.29944 7.1267h6.11883l4.10519.0519-3.10487-6.0513z" fill="#f5841f" stroke="#f5841f"/><path d="m19.8241 17.9876.4417-7.5932 2.0007-5.4034h-8.9119l2.0006 5.4034.4417 7.5932.1689 2.3842.013 5.8958h3.6635l.013-5.8958z" fill="#f5841f" stroke="#f5841f"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

1
packages/concordia-app/src/assets/images/orbitdb_logo.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

32
packages/concordia-app/src/assets/particles.js

@ -0,0 +1,32 @@
const particlesOptions = {
particles: {
number: {
value: 120,
density: {
enable: true,
value_area: 1500,
},
},
line_linked: {
enable: true,
opacity: 0.06,
},
move: {
direction: 'none',
speed: 0.12,
},
size: {
value: 1.6,
},
opacity: {
anim: {
enable: true,
speed: 0.6,
opacity_min: 0.05,
},
},
},
retina_detect: true,
};
export default particlesOptions;

81
packages/concordia-app/src/components/AppContext.jsx

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

151
packages/concordia-app/src/components/ClearDatabasesModal/index.jsx

@ -0,0 +1,151 @@
import React, {
useCallback, useMemo, useState,
useEffect,
} from 'react';
import {
Button, Form, Input, Modal,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import purgeIndexedDBs from '../../utils/indexedDB/indexedDBUtils';
const ClearDatabasesModal = (props) => {
const {
open, onDatabasesCleared, onCancel,
} = props;
const [confirmationInput, setConfirmationInput] = useState('');
const [userConfirmed, setUserConfirmed] = useState(false);
const [isClearing, setIsClearing] = useState(false);
const user = useSelector((state) => state.user);
const { t } = useTranslation();
useEffect(() => {
if (user.hasSignedUp && confirmationInput === user.username) {
setUserConfirmed(true);
} else if (!user.hasSignedUp && confirmationInput.toLowerCase() === 'concordia') {
setUserConfirmed(true);
} else {
setUserConfirmed(false);
}
}, [confirmationInput, user.hasSignedUp, user.username]);
const handleSubmit = useCallback(() => {
setIsClearing(true);
purgeIndexedDBs()
.then(() => {
onDatabasesCleared();
}).catch((reason) => console.error(reason));
}, [onDatabasesCleared]);
const onCancelTry = useCallback(() => {
if (!isClearing) {
setConfirmationInput('');
onCancel();
}
}, [isClearing, onCancel]);
const handleInputChange = (event, { value }) => { setConfirmationInput(value); };
const modalContent = useMemo(() => {
if (isClearing) {
return (
<>
<p>
{t('clear.databases.modal.clearing.progress.message')}
</p>
</>
);
}
if (user.hasSignedUp) {
return (
<>
<p>
{t('clear.databases.modal.description.pre.user', { username: user.username })}
</p>
<p>
{t('clear.databases.modal.description.body.user')}
</p>
<Form>
<Form.Field>
<label htmlFor="form-clear-databases-field-confirm">
{t('clear.databases.modal.form.username.label.user')}
</label>
<Input
id="form-clear-databases-field-confirm"
name="confirmationInput"
value={confirmationInput}
onChange={handleInputChange}
/>
</Form.Field>
</Form>
</>
);
}
return (
<>
<p>
{t('clear.databases.modal.description.pre.guest')}
</p>
<Form>
<Form.Field>
<label htmlFor="form-clear-databases-field-confirm">
{t('clear.databases.modal.form.username.label.guest')}
</label>
<Input
id="form-clear-databases-field-confirm"
name="confirmationInput"
value={confirmationInput}
onChange={handleInputChange}
/>
</Form.Field>
</Form>
</>
);
}, [confirmationInput, isClearing, t, user.hasSignedUp, user.username]);
return useMemo(() => (
<Modal
onClose={onCancelTry}
open={open}
size="small"
>
<Modal.Header>
{isClearing
? t('clear.databases.modal.clearing.progress.title')
: t('clear.databases.modal.title')}
</Modal.Header>
<Modal.Content>
<Modal.Description>
{modalContent}
</Modal.Description>
</Modal.Content>
{!isClearing && (
<Modal.Actions>
<Button className="secondary-button" onClick={onCancelTry} disabled={isClearing}>
{t('clear.databases.modal.cancel.button')}
</Button>
<Button onClick={handleSubmit} className="primary-button" disabled={!userConfirmed}>
{t('clear.databases.modal.clear.button')}
</Button>
</Modal.Actions>
)}
</Modal>
), [handleSubmit, isClearing, modalContent, onCancelTry, open, t, userConfirmed]);
};
ClearDatabasesModal.defaultProps = {
open: false,
};
ClearDatabasesModal.propTypes = {
open: PropTypes.bool,
onDatabasesCleared: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
export default ClearDatabasesModal;

46
packages/concordia-app/src/components/CustomLoadingTabPane.jsx

@ -0,0 +1,46 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import {
Dimmer, Loader, Placeholder, Tab,
} from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
const CustomLoadingTabPane = (props) => {
const { loading, loadingMessage, children } = props;
const { t } = useTranslation();
return useMemo(() => {
if (loading) {
return (
<Tab.Pane>
<Dimmer active inverted>
<Loader inverted>
{loadingMessage !== undefined
? loadingMessage
: t('custom.loading.tab.pane.default.generic.message')}
</Loader>
</Dimmer>
<Placeholder fluid>
<Placeholder.Line length="very long" />
<Placeholder.Line length="medium" />
<Placeholder.Line length="long" />
</Placeholder>
</Tab.Pane>
);
}
return (
<Tab.Pane>
{children}
</Tab.Pane>
);
}, [children, loading, loadingMessage, t]);
};
CustomLoadingTabPane.propTypes = {
loading: PropTypes.bool,
loadingMessage: PropTypes.string,
children: PropTypes.element,
};
export default CustomLoadingTabPane;

76
packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx

@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Container, Progress } from 'semantic-ui-react';
// Images
import metamaskLogo from '../../../assets/images/metamask_logo.svg';
import ethereumLogo from '../../../assets/images/ethereum_logo.svg';
import ipfsLogo from '../../../assets/images/ipfs_logo.svg';
import orbitdbLogo from '../../../assets/images/orbitdb_logo.svg';
import appLogo from '../../../assets/images/app_logo_circle.svg';
import './style.css';
const LoadingComponent = (props) => {
const {
imageType, messageList, progressType, title, message, progress,
} = props;
let imageSrc; let imageAlt; let listItems; let indicating; let
error;
if (imageType === 'metamask') {
imageSrc = metamaskLogo;
imageAlt = 'metamask_logo';
} else if (imageType === 'ethereum') {
imageSrc = ethereumLogo;
imageAlt = 'ethereum_logo';
} else if (imageType === 'ipfs') {
imageSrc = ipfsLogo;
imageAlt = 'ipfs_logo';
} else if (imageType === 'orbit') {
imageSrc = orbitdbLogo;
imageAlt = 'orbitdb_logo';
} else if (imageType === 'app') {
imageSrc = appLogo;
imageAlt = 'app_logo';
}
if (progressType === 'indicating') indicating = true;
else if (progressType === 'error') error = true;
if (messageList) {
listItems = messageList.map((listItem) => <li>{listItem}</li>);
}
const list = messageList ? <ul>{listItems}</ul> : '';
return (
<main id="loading-screen">
<Container id="loading-screen-container">
<img src={imageSrc} alt={imageAlt} id="loading-img" />
<p><strong>{title}</strong></p>
<p>{message}</p>
{list}
</Container>
<Progress
id="loading-screen-progress"
percent={progress}
size="small"
indicating={indicating}
error={error}
/>
</main>
);
};
LoadingComponent.propTypes = {
title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
messageList: PropTypes.arrayOf(PropTypes.string),
imageType: PropTypes.string.isRequired,
progress: PropTypes.number.isRequired,
progressType: PropTypes.string.isRequired,
};
export default LoadingComponent;

25
packages/concordia-app/src/components/InitializationScreen/CustomLoader/style.css

@ -0,0 +1,25 @@
#loading-screen {
padding-top: 14em;
text-align: center;
font-size: large;
height: 100%;
}
#loading-screen ul {
list-style-position: inside;
}
#loading-img {
margin-bottom: 3em;
height: 12em;
}
#loading-screen-container {
height: 26em;
}
#loading-screen-progress {
width: 40vw;
margin-left: auto !important;
margin-right: auto !important;
}

160
packages/concordia-app/src/components/InitializationScreen/index.jsx

@ -0,0 +1,160 @@
import React, { Children } from 'react';
import { breezeConstants } from '@ecentrics/breeze';
import { useSelector } from 'react-redux';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import CustomLoader from './CustomLoader';
const InitializationLoader = ({ children }) => {
const initializing = useSelector((state) => state.drizzleStatus.initializing);
const failed = useSelector((state) => state.drizzleStatus.failed);
const ipfsStatus = useSelector((state) => state.ipfs.status);
const orbitStatus = useSelector((state) => state.orbit.status);
const web3Status = useSelector((state) => state.web3.status);
const web3NetworkId = useSelector((state) => state.web3.networkId);
const web3NetworkFailed = useSelector((state) => state.web3.networkFailed);
const web3AccountsFailed = useSelector((state) => state.web3.accountsFailed);
const contractInitialized = useSelector((state) => state.contracts[FORUM_CONTRACT].initialized);
const contractDeployed = useSelector((state) => state.contracts[FORUM_CONTRACT].deployed);
const userFetched = useSelector((state) => state.user.address);
if (!window.ethereum) {
return (
<CustomLoader
title="Couldn't detect MetaMask!"
message={['Please make sure to install ', <a href="https://metamask.io/">MetaMask</a>, ' first.']}
imageType="metamask"
progress={10}
progressType="error"
/>
);
}
if ((web3Status === 'initializing' || !web3NetworkId) && !web3NetworkFailed) {
return (
<CustomLoader
title="Connecting to the Ethereum network..."
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account."
imageType="ethereum"
progress={20}
progressType="indicating"
/>
);
}
if (web3Status === 'failed' || web3NetworkFailed) {
return (
<CustomLoader
title="No connection to the Ethereum network!"
message="Please make sure that:"
messageList={['MetaMask is unlocked and pointed to the correct, available network',
'The app has been granted the right to connect to your account']}
imageType="ethereum"
progress={20}
progressType="error"
/>
);
}
if (web3Status === 'initialized' && web3AccountsFailed) {
return (
<CustomLoader
title="We can't find any Ethereum accounts!"
message="Please make sure that MetaMask is unlocked."
imageType="ethereum"
progress={20}
progressType="error"
/>
);
}
if (initializing || (!failed && !contractInitialized && contractDeployed)) {
return (
<CustomLoader
title="Initializing contracts..."
message=""
imageType="ethereum"
progress={40}
progressType="indicating"
/>
);
}
if (!contractDeployed) {
return (
<CustomLoader
title="No contracts found on the current network!"
message="Please make sure that you are connected to the correct network and the contracts are deployed."
imageType="ethereum"
progress={40}
progressType="error"
/>
);
}
if (ipfsStatus === breezeConstants.STATUS_UNINITIALIZED || ipfsStatus === breezeConstants.STATUS_INITIALIZING) {
return (
<CustomLoader
title="Initializing IPFS..."
message=""
imageType="ipfs"
progress={60}
progressType="indicating"
/>
);
}
if (ipfsStatus === breezeConstants.STATUS_FAILED) {
return (
<CustomLoader
title="IPFS initialization failed!"
message=""
imageType="ipfs"
progress={60}
progressType="error"
/>
);
}
if (orbitStatus === breezeConstants.STATUS_UNINITIALIZED || orbitStatus === breezeConstants.STATUS_INITIALIZING) {
const message = process.env.NODE_ENV === 'development'
? 'If needed, please sign the transaction in MetaMask to create the databases.'
: 'Please sign the transaction in MetaMask to create the databases.';
return (
<CustomLoader
title="Preparing OrbitDB..."
message={message}
imageType="orbit"
progress={80}
progressType="indicating"
/>
);
}
if (orbitStatus === breezeConstants.STATUS_FAILED) {
return (
<CustomLoader
title="OrbitDB initialization failed!"
message=""
imageType="orbit"
progress={80}
progressType="error"
/>
);
}
if (!userFetched) {
return (
<CustomLoader
title="Loading dapp..."
message=""
imageType="app"
progress={90}
progressType="indicating"
/>
);
}
return Children.only(children);
};
export default InitializationLoader;

8
packages/concordia-app/src/components/LoadingScreen.jsx

@ -0,0 +1,8 @@
import React from 'react';
import { Loader } from 'semantic-ui-react';
const LoadingScreen = () => (
<Loader active />
);
export default LoadingScreen;

13
packages/concordia-app/src/components/NotFound.jsx

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

23
packages/concordia-app/src/components/PaginationComponent.jsx

@ -0,0 +1,23 @@
import React, { useMemo } from 'react';
import { Icon, Pagination } from 'semantic-ui-react';
export const ITEMS_PER_PAGE = 10;
const PaginationComponent = (props) => {
const { numberOfItems, onPageChange } = props;
return useMemo(() => (
<Pagination
defaultActivePage={1}
ellipsisItem={{ content: <Icon name="ellipsis horizontal" />, icon: true }}
firstItem={{ content: <Icon name="angle double left" />, icon: true }}
lastItem={{ content: <Icon name="angle double right" />, icon: true }}
prevItem={{ content: <Icon name="angle left" />, icon: true }}
nextItem={{ content: <Icon name="angle right" />, icon: true }}
totalPages={Math.ceil(numberOfItems / ITEMS_PER_PAGE)}
disabled={numberOfItems <= ITEMS_PER_PAGE}
onPageChange={onPageChange}
/>
), [numberOfItems, onPageChange]);
};
export default PaginationComponent;

50
packages/concordia-app/src/components/PollCreate/PollOption/index.jsx

@ -0,0 +1,50 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import {
Button, Form, Icon, Input,
} from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import './styles.css';
const PollOption = (props) => {
const {
id, removable, onChange, onRemove,
} = props;
const { t } = useTranslation();
return (
<Form.Field className="form-poll-option" required>
<label className="form-topic-create-header" htmlFor="form-poll-create-field-subject">
{t('poll.create.option.field.label', { id })}
</label>
<Input
id="form-poll-create-field-subject"
placeholder={t('poll.create.option.field.placeholder', { id })}
name="pollQuestionInput"
className="form-input"
onChange={(e) => onChange(e, id)}
/>
{removable
&& (
<Button
className="remove-option-button"
key={`form-remove-option-button-${id}`}
negative
icon
onClick={(e) => onRemove(e, id)}
>
<Icon name="x" />
</Button>
)}
</Form.Field>
);
};
PollOption.propTypes = {
id: PropTypes.number.isRequired,
onChange: PropTypes.func,
onRemove: PropTypes.func,
removable: PropTypes.bool,
};
export default memo(PollOption);

4
packages/concordia-app/src/components/PollCreate/PollOption/styles.css

@ -0,0 +1,4 @@
.form-poll-option > .form-input{
width: 50% !important;
margin-right: 0.25em;
}

190
packages/concordia-app/src/components/PollCreate/index.jsx

@ -0,0 +1,190 @@
import React, {
useMemo, useState, useCallback, useEffect, forwardRef, useImperativeHandle,
} from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import {
Button, Checkbox, Form, Icon, Input,
} from 'semantic-ui-react';
import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { POLL_CREATED_EVENT } from 'concordia-shared/src/constants/contracts/events/VotingContractEvents';
import { POLLS_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases';
import PollOption from './PollOption';
import { breeze, drizzle } from '../../redux/store';
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus';
import './styles.css';
import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys';
import { generatePollHash } from '../../utils/hashUtils';
const { contracts: { [VOTING_CONTRACT]: { methods: { createPoll } } } } = drizzle;
const { orbit: { stores } } = breeze;
const PollCreate = forwardRef((props, ref) => {
const { account, onChange, onCreated } = props;
const transactionStack = useSelector((state) => state.transactionStack);
const transactions = useSelector((state) => state.transactions);
const [createPollCacheSendStackId, setCreatePollCacheSendStackId] = useState('');
const [question, setQuestion] = useState('');
const [options, setOptions] = useState(
[{ id: 1 }, { id: 2 }],
);
const [optionValues, setOptionValues] = useState(
['', ''],
);
const [optionsNextId, setOptionsNextId] = useState(3);
const [allowVoteChanges, setAllowVoteChanges] = useState(false);
const [creating, setCreating] = useState(false);
const [errored, setErrored] = useState(false);
const { t } = useTranslation();
const handlePollQuestionChange = useCallback((event) => {
const newQuestion = event.target.value.trim();
if (newQuestion !== question) setQuestion(newQuestion);
}, [question]);
const addOption = useCallback((e) => {
e.currentTarget.blur();
const newOptions = [...options, { id: optionsNextId, removable: true }];
const newOptionValues = [...optionValues, ''];
setOptionsNextId(optionsNextId + 1);
setOptions(newOptions);
setOptionValues(newOptionValues);
}, [optionValues, options, optionsNextId]);
const removeOption = useCallback((e, id) => {
e.currentTarget.blur();
const newOptions = [...options];
newOptions.splice(id - 1, 1);
const newOptionValues = [...optionValues];
newOptionValues.splice(id - 1, 1);
setOptions(newOptions);
setOptionValues(newOptionValues);
}, [optionValues, options]);
const handlePollOptionChange = useCallback((event, id) => {
const newValue = event.target.value.trim();
if (newValue !== optionValues[id - 1]) {
const newOptionValues = [...optionValues];
newOptionValues[id - 1] = newValue;
setOptionValues(newOptionValues);
}
}, [optionValues]);
const pollOptions = useMemo(() => options
.map((option, index) => {
const { id, removable } = option;
return (
<PollOption
id={index + 1}
key={id}
removable={removable}
onRemove={removeOption}
onChange={handlePollOptionChange}
/>
);
}), [handlePollOptionChange, options, removeOption]);
useEffect(() => {
onChange({ question, optionValues });
}, [onChange, optionValues, question]);
const handleCheckboxChange = useCallback((event, data) => {
setAllowVoteChanges(data.checked);
}, []);
useImperativeHandle(ref, () => ({
createPoll(topicId) {
setCreating(true);
const dataHash = generatePollHash(question, optionValues);
setCreatePollCacheSendStackId(createPoll.cacheSend(
...[topicId, options.length, dataHash, allowVoteChanges], { from: account },
));
},
pollCreating() {
return creating;
},
pollErrored() {
return errored;
},
}));
useEffect(() => {
if (creating && transactionStack && transactionStack[createPollCacheSendStackId]
&& transactions[transactionStack[createPollCacheSendStackId]]) {
if (transactions[transactionStack[createPollCacheSendStackId]].status === TRANSACTION_ERROR) {
setErrored(true);
setCreating(false);
onCreated(false);
} else if (transactions[transactionStack[createPollCacheSendStackId]].status === TRANSACTION_SUCCESS) {
const {
receipt: {
events: {
[POLL_CREATED_EVENT]: {
returnValues: {
topicID: topicId,
},
},
},
},
} = transactions[transactionStack[createPollCacheSendStackId]];
const pollsDb = Object.values(stores).find((store) => store.dbname === POLLS_DATABASE);
pollsDb
.put(topicId, { [POLL_QUESTION]: question, [POLL_OPTIONS]: optionValues })
.then(() => {
onCreated(topicId);
})
.catch((reason) => {
console.error(reason);
setErrored(true);
setCreating(false);
onCreated(false);
});
}
}
}, [createPollCacheSendStackId, creating, onCreated, optionValues, question, transactionStack, transactions]);
return useMemo(() => (
<div className="poll-create">
<Form.Field required>
<label className="form-topic-create-header" htmlFor="form-poll-create-field-subject">
{t('poll.create.question.field.label')}
</label>
<Input
id="form-poll-create-field-subject"
placeholder={t('poll.create.question.field.placeholder')}
name="pollQuestionInput"
className="form-input"
onChange={handlePollQuestionChange}
/>
</Form.Field>
<Form.Field>
<Checkbox
label={t('poll.create.allow.vote.changes.field.label')}
onClick={handleCheckboxChange}
/>
</Form.Field>
{pollOptions}
<Button
id="add-option-button"
key="form-add-option-button"
positive
icon
labelPosition="left"
onClick={addOption}
>
<Icon name="plus" />
{t('poll.create.add.option.button')}
</Button>
</div>
), [addOption, handleCheckboxChange, handlePollQuestionChange, pollOptions, t]);
});
PollCreate.propTypes = {
onChange: PropTypes.func,
onCreated: PropTypes.func,
};
export default PollCreate;

12
packages/concordia-app/src/components/PollCreate/styles.css

@ -0,0 +1,12 @@
.poll-create {
padding-top: 1em;
padding-bottom: 1em;
}
.poll-create .checkbox > label,
.poll-create .checkbox > label:focus,
.poll-create .checkbox > label:hover{
color: white !important;
font-weight: 700;
}

23
packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx

@ -0,0 +1,23 @@
import React from 'react';
import { Container, Header, Icon } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
const PollDataInvalid = () => {
const { t } = useTranslation();
return (
<Container id="topic-poll-data-invalid-container" textAlign="center">
<Header as="h3" icon textAlign="center">
<Icon name="warning sign" color="red" />
<Header.Content>
{t('topic.poll.invalid.data.header')}
<Header.Subheader>
{t('topic.poll.invalid.data.sub.header')}
</Header.Subheader>
</Header.Content>
</Header>
</Container>
);
};
export default PollDataInvalid;

67
packages/concordia-app/src/components/PollView/PollGraph/PollChartBar/index.jsx

@ -0,0 +1,67 @@
import React, { useMemo } from 'react';
import Chart from 'react-apexcharts';
import PropTypes from 'prop-types';
import { CHART_TYPE_BAR } from '../../../../constants/polls/PollGraph';
const PollChartBar = (props) => {
const { pollOptions, voteCounts, voterNames } = props;
const chartOptions = useMemo(() => ({
chart: {
id: 'chart-bar',
},
theme: {
palette: 'palette8',
},
plotOptions: {
bar: {
horizontal: true,
distributed: true,
},
},
xaxis: {
categories: pollOptions,
tickAmount: 1,
labels: {
formatter(val) {
if (val.toFixed) return val.toFixed(0);
return val;
},
},
},
tooltip: {
enabled: true,
x: {
show: false,
},
y: {
formatter(value, { dataPointIndex }) {
return `<div>${voterNames[dataPointIndex].join('</div><div>')}</div>`;
},
title: {
formatter: () => null,
},
},
},
}), [pollOptions, voterNames]);
const chartSeries = useMemo(() => [{
name: 'Votes',
data: voteCounts,
}], [voteCounts]);
return (
<Chart
options={chartOptions}
series={chartSeries}
type={CHART_TYPE_BAR}
/>
);
};
PollChartBar.propTypes = {
pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired,
voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired,
};
export default PollChartBar;

63
packages/concordia-app/src/components/PollView/PollGraph/PollChartDonut/index.jsx

@ -0,0 +1,63 @@
import React, { useMemo } from 'react';
import Chart from 'react-apexcharts';
import PropTypes from 'prop-types';
import { CHART_TYPE_DONUT } from '../../../../constants/polls/PollGraph';
const PollChartDonut = (props) => {
const { pollOptions, voteCounts, voterNames } = props;
const chartOptions = useMemo(() => ({
chart: {
id: 'chart-donut',
toolbar: {
show: true,
tools: {
download: true,
},
},
},
theme: {
palette: 'palette8',
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
},
},
},
},
labels: pollOptions,
tooltip: {
enabled: true,
fillSeriesColor: false,
y: {
formatter(value, { seriesIndex }) {
return `<div>${voterNames[seriesIndex].join('</div><div>')}</div>`;
},
title: {
formatter: () => null,
},
},
},
legend: {
position: 'bottom',
},
}), [pollOptions, voterNames]);
return (
<Chart
options={chartOptions}
series={voteCounts}
type={CHART_TYPE_DONUT}
/>
);
};
PollChartDonut.propTypes = {
pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired,
voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired,
};
export default PollChartDonut;

140
packages/concordia-app/src/components/PollView/PollGraph/index.jsx

@ -0,0 +1,140 @@
import React, {
useCallback, useEffect, useMemo, useState,
} from 'react';
import {
Grid,
Header, Statistic, Tab,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import PollChartBar from './PollChartBar';
import PollChartDonut from './PollChartDonut';
import './styles.css';
import { CHART_TYPE_BAR, CHART_TYPE_DONUT } from '../../../constants/polls/PollGraph';
const PollGraph = (props) => {
const {
pollOptions, userVoteIndex, voteCounts, voterNames,
} = props;
const [totalVotes, setTotalVotes] = useState(
voteCounts.reduce((accumulator, voteCount) => accumulator + voteCount, 0),
);
const { t } = useTranslation();
useEffect(() => {
setTotalVotes(voteCounts.reduce((accumulator, voteCount) => accumulator + voteCount, 0));
}, [voteCounts]);
const footer = useMemo(() => (
<div>
<Statistic size="mini">
<Statistic.Value>
{ totalVotes }
</Statistic.Value>
<Statistic.Label>
{ totalVotes !== 1 ? t('topic.poll.tab.results.votes') : t('topic.poll.tab.results.vote') }
</Statistic.Label>
</Statistic>
{userVoteIndex !== -1
&& (
<div>
{t('topic.poll.tab.results.user.vote')}
<span className="poll-voted-option">{pollOptions[userVoteIndex]}</span>
</div>
)}
</div>
), [pollOptions, t, totalVotes, userVoteIndex]);
function calculateChartBarWidth(nOptions) {
if (nOptions < 6) return 8;
if (nOptions < 10) return 10;
if (nOptions < 14) return 12;
if (nOptions < 18) return 14;
return 16;
}
function calculateChartDonutWidth(nOptions) {
if (nOptions < 10) return 8;
if (nOptions < 16) return 10;
return 12;
}
const generatePane = useCallback(
(type) => (
<Tab.Pane attached={false}>
<Grid columns="equal">
<Grid.Row>
<Grid.Column />
<Grid.Column
width={type === CHART_TYPE_BAR
? calculateChartBarWidth(pollOptions.length)
: calculateChartDonutWidth(pollOptions.length)}
textAlign="center"
>
{type === CHART_TYPE_BAR
? (
<PollChartBar
pollOptions={pollOptions}
voteCounts={voteCounts}
voterNames={voterNames}
/>
) : (
<PollChartDonut
pollOptions={pollOptions}
voteCounts={voteCounts}
voterNames={voterNames}
/>
)}
</Grid.Column>
<Grid.Column />
</Grid.Row>
<Grid.Row>
<Grid.Column textAlign="center">
{footer}
</Grid.Column>
</Grid.Row>
</Grid>
</Tab.Pane>
),
[footer, pollOptions, voteCounts, voterNames],
);
const panes = useMemo(() => {
const chartBarPane = generatePane(CHART_TYPE_BAR);
const chartDonutPane = generatePane(CHART_TYPE_DONUT);
return ([
{ menuItem: { key: 'chart-bar', icon: 'chart bar' }, render: () => chartBarPane },
{ menuItem: { key: 'chart-donut', icon: 'chart pie' }, render: () => chartDonutPane },
]);
}, [generatePane]);
return useMemo(() => (
<>
{totalVotes > 0
? (
<Tab
menu={{ secondary: true }}
panes={panes}
/>
)
: (
<Header as="h4" textAlign="center">
{t('topic.poll.tab.results.no.votes')}
</Header>
)}
</>
), [panes, t, totalVotes]);
};
PollGraph.defaultProps = {
userVoteIndex: -1,
};
PollGraph.propTypes = {
pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired,
voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired,
voterNames: PropTypes.arrayOf(PropTypes.array).isRequired,
userVoteIndex: PropTypes.number,
};
export default PollGraph;

10
packages/concordia-app/src/components/PollView/PollGraph/styles.css

@ -0,0 +1,10 @@
#topic-poll-container .ui.segment.tab {
box-shadow: none;
border: none;
}
.apexcharts-tooltip {
border: 1px solid #ddd !important;
background-color: white !important;
color: black !important;
}

26
packages/concordia-app/src/components/PollView/PollGuestView/index.jsx

@ -0,0 +1,26 @@
import React from 'react';
import { Container, Header, Icon } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const PollGuestView = () => {
const { t } = useTranslation();
return (
<Container id="topic-poll-guest-view-container" textAlign="center">
<Header as="h3" icon textAlign="center">
<Icon name="signup" />
<Header.Content>
{t('topic.poll.guest.header')}
<Header.Subheader>
{t('topic.poll.guest.sub.header.pre')}
<Link to="/auth/register">{t('topic.poll.guest.sub.header.link')}</Link>
{t('topic.poll.guest.sub.header.post')}
</Header.Subheader>
</Header.Content>
</Header>
</Container>
);
};
export default PollGuestView;

116
packages/concordia-app/src/components/PollView/PollVote/index.jsx

@ -0,0 +1,116 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Button, Form } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { drizzle } from '../../../redux/store';
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../../constants/TransactionStatus';
import './styles.css';
const { contracts: { [VOTING_CONTRACT]: { methods: { vote } } } } = drizzle;
const PollVote = (props) => {
const {
topicId, account, pollOptions, enableVoteChanges, hasUserVoted, userVoteIndex,
} = props;
const transactionStack = useSelector((state) => state.transactionStack);
const transactions = useSelector((state) => state.transactions);
const [voteCacheSendStackId, setVoteCacheSendStackId] = useState('');
const [selectedOptionIndex, setSelectedOptionIndex] = useState(userVoteIndex);
const [voting, setVoting] = useState(false);
const { t } = useTranslation();
const onOptionSelected = (e, { value }) => {
setSelectedOptionIndex(value);
};
useEffect(() => {
setSelectedOptionIndex(userVoteIndex);
}, [userVoteIndex]);
const onCastVote = () => {
setVoting(true);
setVoteCacheSendStackId(vote.cacheSend(...[topicId, selectedOptionIndex + 1], { from: account }));
};
const onUnvote = (e) => {
e.preventDefault();
setVoting(true);
setVoteCacheSendStackId(vote.cacheSend(...[topicId, 0], { from: account }));
};
useEffect(() => {
if (voting && transactionStack && transactionStack[voteCacheSendStackId]
&& transactions[transactionStack[voteCacheSendStackId]]) {
if (transactions[transactionStack[voteCacheSendStackId]].status === TRANSACTION_ERROR
|| transactions[transactionStack[voteCacheSendStackId]].status === TRANSACTION_SUCCESS) {
setVoting(false);
}
}
}, [transactionStack, transactions, voteCacheSendStackId, voting]);
if (hasUserVoted && !enableVoteChanges) {
return (
<>
<div>
{t('topic.poll.tab.results.user.vote')}
<span className="poll-voted-option">{pollOptions[userVoteIndex]}</span>
</div>
<div>
{t('topic.poll.tab.vote.no.changes')}
</div>
</>
);
}
return (
<Form onSubmit={onCastVote}>
<Form.Group grouped>
<label htmlFor="poll">{t('topic.poll.tab.vote.form.radio.label')}</label>
{pollOptions.map((pollOption, index) => (
<Form.Radio
key={pollOption}
label={pollOption}
value={index}
className={index === userVoteIndex ? 'poll-voted-option' : null}
checked={index === selectedOptionIndex}
disabled={voting}
onChange={onOptionSelected}
/>
))}
</Form.Group>
<Button
type="submit"
className="primary-button"
disabled={voting || (selectedOptionIndex === userVoteIndex)}
>
{t('topic.poll.tab.vote.form.button.submit')}
</Button>
{hasUserVoted && enableVoteChanges
&& (
<Button
type="submit"
disabled={voting}
onClick={onUnvote}
>
{t('topic.poll.tab.vote.form.button.unvote')}
</Button>
)}
</Form>
);
};
PollVote.defaultProps = {
userVoteIndex: -1,
};
PollVote.propTypes = {
topicId: PropTypes.number.isRequired,
pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired,
enableVoteChanges: PropTypes.bool.isRequired,
hasUserVoted: PropTypes.bool.isRequired,
userVoteIndex: PropTypes.number,
};
export default PollVote;

3
packages/concordia-app/src/components/PollView/PollVote/styles.css

@ -0,0 +1,3 @@
.poll-voted-option{
font-weight: bold;
}

211
packages/concordia-app/src/components/PollView/index.jsx

@ -0,0 +1,211 @@
import React, {
useEffect, useMemo, useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import {
Container, Header, Icon, Loader, Tab,
} from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { POLLS_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases';
import { breeze, drizzle } from '../../redux/store';
import PollGraph from './PollGraph';
import CustomLoadingTabPane from '../CustomLoadingTabPane';
import { GRAPH_TAB, VOTE_TAB } from '../../constants/polls/PollTabs';
import PollVote from './PollVote';
import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions';
import { generatePollHash } from '../../utils/hashUtils';
import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys';
import PollDataInvalid from './PollDataInvalid';
import PollGuestView from './PollGuestView';
const {
contracts: {
[VOTING_CONTRACT]: {
methods: {
getPoll:
{ cacheCall: getPollChainData, clearCacheCall: clearGetPollChainData },
},
},
},
} = drizzle;
const { orbit } = breeze;
const PollView = (props) => {
const { topicId, topicAuthorAddress } = props;
const userAddress = useSelector((state) => state.user.address);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const getPollResults = useSelector((state) => state.contracts[VOTING_CONTRACT].getPoll);
const polls = useSelector((state) => state.orbitData.polls);
const [getPollCallHash, setGetPollCallHash] = useState(null);
const [pollHash, setPollHash] = useState('');
const [pollChangeVoteEnabled, setPollChangeVoteEnabled] = useState(false);
const [pollOptions, setPollOptions] = useState([]);
const [voteCounts, setVoteCounts] = useState([]);
const [voters, setVoters] = useState([]);
const [voterNames, setVoterNames] = useState([]);
const [pollHashValid, setPollHashValid] = useState(true);
const [pollQuestion, setPollQuestion] = useState('');
const [chainDataLoading, setChainDataLoading] = useState(true);
const [orbitDataLoading, setOrbitDataLoading] = useState(true);
const dispatch = useDispatch();
const { t } = useTranslation();
useEffect(() => {
if (!getPollCallHash) {
setGetPollCallHash(getPollChainData(topicId));
}
}, [getPollCallHash, topicId]);
useEffect(() => {
dispatch({
type: FETCH_USER_DATABASE,
orbit,
dbName: POLLS_DATABASE,
userAddress: topicAuthorAddress,
});
}, [dispatch, topicAuthorAddress]);
useEffect(() => {
if (getPollCallHash && getPollResults && getPollResults[getPollCallHash]) {
const pollResults = getPollResults[getPollCallHash];
setPollHash(pollResults.value[1]);
setPollChangeVoteEnabled(pollResults.value[2]);
setVoteCounts(pollResults.value[4].map((voteCount) => parseInt(voteCount, 10)));
const cumulativeSum = pollResults.value[4]
.map((voteCount) => parseInt(voteCount, 10))
.reduce((accumulator, voteCount) => (accumulator.length === 0
? [voteCount]
: [...accumulator, accumulator[accumulator.length - 1] + voteCount]), []);
setVoters(cumulativeSum
.map((subArrayEnd, index) => pollResults.value[5]
.slice(index > 0 ? cumulativeSum[index - 1] : 0,
subArrayEnd)));
setVoterNames(cumulativeSum
.map((subArrayEnd, index) => pollResults.value[6]
.slice(index > 0 ? cumulativeSum[index - 1] : 0,
subArrayEnd)));
setChainDataLoading(false);
}
}, [getPollCallHash, getPollResults]);
useEffect(() => {
const pollFound = polls
.find((poll) => poll.id === topicId);
if (pollHash && pollFound) {
if (generatePollHash(pollFound[POLL_QUESTION], pollFound[POLL_OPTIONS]) === pollHash) {
setPollHashValid(true);
setPollQuestion(pollFound[POLL_QUESTION]);
setPollOptions([...pollFound[POLL_OPTIONS]]);
} else {
setPollHashValid(false);
}
setOrbitDataLoading(false);
}
}, [pollHash, polls, topicId]);
const userHasVoted = useMemo(() => hasSignedUp && voters
.some((optionVoters) => optionVoters.includes(userAddress)),
[hasSignedUp, userAddress, voters]);
const userVoteIndex = useMemo(() => {
if (!chainDataLoading && !orbitDataLoading && userHasVoted) {
return voters
.findIndex((optionVoters) => optionVoters.includes(userAddress));
}
return -1;
}, [chainDataLoading, orbitDataLoading, userAddress, userHasVoted, voters]);
const pollVoteTab = useMemo(() => {
if (!hasSignedUp) {
return <PollGuestView />;
}
if (chainDataLoading || orbitDataLoading) {
return null;
}
return (
<PollVote
topicId={topicId}
pollOptions={pollOptions}
enableVoteChanges={pollChangeVoteEnabled}
hasUserVoted={userHasVoted}
userVoteIndex={userVoteIndex}
/>
);
}, [
chainDataLoading, hasSignedUp, orbitDataLoading, pollChangeVoteEnabled, pollOptions, topicId, userHasVoted,
userVoteIndex,
]);
const pollGraphTab = useMemo(() => (
!chainDataLoading || orbitDataLoading
? (
<PollGraph
pollOptions={pollOptions}
voteCounts={voteCounts}
userVoteIndex={userVoteIndex}
voterNames={voterNames}
/>
)
: null
), [chainDataLoading, orbitDataLoading, pollOptions, userVoteIndex, voteCounts, voterNames]);
const panes = useMemo(() => {
const pollVotePane = (
<CustomLoadingTabPane loading={chainDataLoading || orbitDataLoading}>
{pollVoteTab}
</CustomLoadingTabPane>
);
const pollGraphPane = (
<CustomLoadingTabPane loading={chainDataLoading || orbitDataLoading}>
{pollGraphTab}
</CustomLoadingTabPane>
);
return ([
{ menuItem: t(GRAPH_TAB.intl_display_name_id), render: () => pollGraphPane },
{ menuItem: t(VOTE_TAB.intl_display_name_id), render: () => pollVotePane },
]);
}, [chainDataLoading, orbitDataLoading, pollGraphTab, pollVoteTab, t]);
useEffect(() => () => {
clearGetPollChainData();
}, []);
return (
<Container id="topic-poll-container" textAlign="left">
{pollHashValid
? (
<>
{!chainDataLoading && !orbitDataLoading ? (
<>
<Header as="h3">
<Icon name="chart area" size="large" />
{pollQuestion}
</Header>
<Tab panes={panes} />
</>
) : <Loader active inline="centered" />}
</>
)
: <PollDataInvalid />}
</Container>
);
};
PollView.propTypes = {
topicId: PropTypes.number.isRequired,
topicAuthorAddress: PropTypes.string,
};
export default PollView;

162
packages/concordia-app/src/components/PostCreate/index.jsx

@ -0,0 +1,162 @@
import React, {
memo, useCallback, useEffect, useState,
} from 'react';
import {
Button, Feed, Form, Icon, TextArea,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { POSTS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases';
import { POST_CREATED_EVENT } from 'concordia-shared/src/constants/contracts/events/ForumContractEvents';
import determineKVAddress from '../../utils/orbitUtils';
import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions';
import { breeze, drizzle } from '../../redux/store';
import './styles.css';
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus';
import { POST_CONTENT } from '../../constants/orbit/PostsDatabaseKeys';
const { contracts: { [FORUM_CONTRACT]: { methods: { createPost } } } } = drizzle;
const { orbit } = breeze;
const PostCreate = (props) => {
const {
topicId, initialPostSubject, account,
} = props;
const transactionStack = useSelector((state) => state.transactionStack);
const transactions = useSelector((state) => state.transactions);
const [postContent, setPostContent] = useState('');
const [createPostCacheSendStackId, setCreatePostCacheSendStackId] = useState('');
const [posting, setPosting] = useState(false);
const [storingPost, setStoringPost] = useState(false);
const userAddress = useSelector((state) => state.user.address);
const users = useSelector((state) => state.orbitData.users);
const dispatch = useDispatch();
const { t } = useTranslation();
useEffect(() => {
if (userAddress) {
determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress })
.then((userOrbitAddress) => {
const userFound = users
.find((user) => user.id === userOrbitAddress);
if (!userFound) {
dispatch({
type: FETCH_USER_DATABASE,
orbit,
dbName: USER_DATABASE,
userAddress,
});
}
})
.catch((error) => {
console.error('Error during determination of key-value DB address:', error);
});
}
}, [dispatch, userAddress, users]);
const handleInputChange = useCallback((event) => {
if (posting) {
return;
}
switch (event.target.name) {
case 'postContent':
setPostContent(event.target.value);
break;
default:
break;
}
}, [posting]);
useEffect(() => {
if (posting && !storingPost && transactionStack && transactionStack[createPostCacheSendStackId]
&& transactions[transactionStack[createPostCacheSendStackId]]) {
if (transactions[transactionStack[createPostCacheSendStackId]].status === TRANSACTION_ERROR) {
setPosting(false);
} else if (transactions[transactionStack[createPostCacheSendStackId]].status === TRANSACTION_SUCCESS) {
const {
receipt: { events: { [POST_CREATED_EVENT]: { returnValues: { postID: contractPostId } } } },
} = transactions[transactionStack[createPostCacheSendStackId]];
const { stores } = orbit;
const postsDb = Object.values(stores).find((store) => store.dbname === POSTS_DATABASE);
postsDb
.put(contractPostId, {
[POST_CONTENT]: postContent,
})
.then(() => {
setPostContent('');
setPosting(false);
setStoringPost(false);
setCreatePostCacheSendStackId('');
})
.catch((reason) => {
console.error(reason);
});
setStoringPost(true);
}
}
}, [
createPostCacheSendStackId, initialPostSubject, postContent, posting, storingPost, transactionStack,
transactions,
]);
const savePost = useCallback(() => {
if (postContent === '') {
return;
}
setPosting(true);
setCreatePostCacheSendStackId(createPost.cacheSend(...[topicId], { from: account }));
}, [account, postContent, topicId]);
return (
<Feed>
<Feed.Event>
<Feed.Content>
<Feed.Summary>
<Form>
<TextArea
placeholder={t('post.form.content.field.placeholder')}
name="postContent"
size="mini"
rows={4}
value={postContent}
onChange={handleInputChange}
/>
</Form>
</Feed.Summary>
<Feed.Meta id="post-button-div">
<Feed.Like>
<Button
animated
type="button"
className="primary-button"
disabled={posting || postContent === ''}
onClick={savePost}
>
<Button.Content visible>
{t('post.create.form.send.button')}
</Button.Content>
<Button.Content hidden>
<Icon name="send" />
</Button.Content>
</Button>
</Feed.Like>
</Feed.Meta>
</Feed.Content>
</Feed.Event>
</Feed>
);
};
PostCreate.propTypes = {
topicId: PropTypes.number.isRequired,
};
export default memo(PostCreate);

18
packages/concordia-app/src/components/PostCreate/styles.css

@ -0,0 +1,18 @@
.post-summary-meta-index {
float: right;
font-size: 12px;
opacity: 0.4;
}
.like:hover .icon {
color: #fff !important;
}
#post-button-div {
float: right;
margin: 1rem 0 4rem 0;
}
#post-button-div button {
margin: 0;
}

260
packages/concordia-app/src/components/PostList/PostListRow/index.jsx

@ -0,0 +1,260 @@
import React, {
memo, useEffect, useMemo, useState, useCallback,
} from 'react';
import {
Dimmer, Feed, Form, Icon, Placeholder, Ref, TextArea,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import TimeAgo from 'react-timeago';
import ReactMarkdown from 'react-markdown';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { POSTS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases';
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions';
import { breeze, drizzle } from '../../../redux/store';
import determineKVAddress from '../../../utils/orbitUtils';
import { POST_CONTENT } from '../../../constants/orbit/PostsDatabaseKeys';
import ProfileImage from '../../ProfileImage';
import PostVoting from '../PostVoting';
import targetBlank from '../../../utils/markdownUtils';
import './styles.css';
const { orbit } = breeze;
const {
contracts: { [FORUM_CONTRACT]: { methods: { getPost: { clearCacheCall: clearGetPostChainData } } } },
} = drizzle;
const PostListRow = (props) => {
const {
id: postId, postIndex, postCallHash, loading, focus,
} = props;
const getPostResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getPost);
const [postAuthorAddress, setPostAuthorAddress] = useState(null);
const [postAuthor, setPostAuthor] = useState(null);
const [timeAgo, setTimeAgo] = useState(null);
const [topicId, setTopicId] = useState(null);
const [postContent, setPostContent] = useState(null);
const [postAuthorMeta, setPostAuthorMeta] = useState(null);
const [editing, setEditing] = useState(false);
const [editedPostContent, setEditedPostContent] = useState(null);
const userAddress = useSelector((state) => state.user.address);
const posts = useSelector((state) => state.orbitData.posts);
const users = useSelector((state) => state.orbitData.users);
const dispatch = useDispatch();
const { t } = useTranslation();
useEffect(() => {
if (!loading && postCallHash && getPostResults[postCallHash] !== undefined) {
setPostAuthorAddress(getPostResults[postCallHash].value[0]);
setPostAuthor(getPostResults[postCallHash].value[1]);
setTimeAgo(getPostResults[postCallHash].value[2] * 1000);
setTopicId(getPostResults[postCallHash].value[3]);
}
}, [getPostResults, loading, postCallHash]);
useEffect(() => {
if (postAuthorAddress && userAddress !== postAuthorAddress) {
dispatch({
type: FETCH_USER_DATABASE,
orbit,
dbName: POSTS_DATABASE,
userAddress: postAuthorAddress,
});
dispatch({
type: FETCH_USER_DATABASE,
orbit,
dbName: USER_DATABASE,
userAddress: postAuthorAddress,
});
}
}, [dispatch, postAuthorAddress, userAddress]);
useEffect(() => {
const postFound = posts
.find((post) => post.id === postId);
if (postFound) {
setPostContent(postFound[POST_CONTENT]);
}
}, [postId, posts]);
useEffect(() => {
if (postAuthorAddress !== null) {
determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress: postAuthorAddress })
.then((userOrbitAddress) => {
const userFound = users
.find((user) => user.id === userOrbitAddress);
if (userFound) {
setPostAuthorMeta(userFound);
}
})
.catch((error) => {
console.error('Error during determination of key-value DB address:', error);
});
}
}, [postAuthorAddress, users]);
const focusRef = useCallback((node) => {
if (focus && node !== null) {
node.scrollIntoView({ behavior: 'smooth' });
}
}, [focus]);
// ---------- Post Editing -----------------
const editPost = useCallback(() => {
setEditedPostContent(postContent);
setEditing(true);
}, [postContent]);
const discardChanges = useCallback(() => {
setEditing(false);
}, []);
const saveChanges = useCallback(() => {
setEditing(false);
if (editedPostContent !== postContent) {
const { stores } = orbit;
const postsDb = Object.values(stores).find((store) => store.dbname === POSTS_DATABASE);
postsDb
.put(postId.toString(), {
[POST_CONTENT]: editedPostContent,
})
.catch((reason) => {
console.error(reason);
});
}
}, [editedPostContent, postContent, postId]);
const editPostButtons = useMemo(() => {
if (postContent !== null && userAddress === postAuthorAddress) {
if (editing) {
return (
<>
<Icon
className="post-list-edit-button"
name="check"
color="green"
fitted
onClick={saveChanges}
/>
<Icon
className="post-list-edit-button"
name="x"
color="red"
fitted
onClick={discardChanges}
/>
</>
);
}
return (
<Icon
className="post-list-edit-button"
name="pencil"
fitted
onClick={editPost}
/>
);
}
return null;
}, [discardChanges, editPost, editing, postAuthorAddress, postContent, saveChanges, userAddress]);
const postContentArea = useMemo(() => {
const handleInputChange = (event, { value }) => {
setEditedPostContent(value);
};
if (postContent !== null) {
if (!editing) {
return (
<ReactMarkdown
source={postContent}
renderers={{
link: targetBlank(),
linkReference: targetBlank(),
}}
/>
);
}
return (
<Form>
<TextArea
value={editedPostContent}
onChange={handleInputChange}
/>
</Form>
);
}
return (<Placeholder><Placeholder.Line length="long" /></Placeholder>);
}, [editedPostContent, editing, postContent]);
useEffect(() => () => clearGetPostChainData(postId), [postId]);
return useMemo(() => (
<Dimmer.Dimmable
as={Feed.Event}
blurring
dimmed={loading}
id={`post-${postId}`}
className="post-list-row"
>
<Ref innerRef={focusRef}>
<Feed.Label className="post-profile-picture">
<ProfileImage
profileAddress={postAuthorAddress}
profileUsername={postAuthor}
profileUserMeta={postAuthorMeta}
size="42"
link
/>
</Feed.Label>
</Ref>
<Feed.Content className="post-content">
<Feed.Summary>
<Link to={`/topics/${topicId}/#post-${postId}`}>
<span className="post-summary-meta-index">
{t('post.list.row.post.id', { id: postIndex })}
</span>
</Link>
{postAuthor !== null && setPostAuthorAddress !== null && timeAgo !== null
? (
<>
<Feed.User as={Link} to={`/users/${postAuthorAddress}`}>{postAuthor}</Feed.User>
<Feed.Date>
<TimeAgo date={timeAgo} />
</Feed.Date>
{editPostButtons}
</>
)
: <Placeholder><Placeholder.Line length="medium" /></Placeholder>}
</Feed.Summary>
<Feed.Extra>
{postContentArea}
</Feed.Extra>
<PostVoting postId={postId} postAuthorAddress={postAuthorAddress} />
</Feed.Content>
</Dimmer.Dimmable>
), [editPostButtons, focusRef, loading, postAuthor, postAuthorAddress, postAuthorMeta,
postContentArea, postId, postIndex, t, timeAgo, topicId]);
};
PostListRow.defaultProps = {
loading: false,
focus: false,
};
PostListRow.propTypes = {
id: PropTypes.number.isRequired,
postIndex: PropTypes.number.isRequired,
postCallHash: PropTypes.string,
loading: PropTypes.bool,
focus: PropTypes.bool,
};
export default memo(PostListRow);

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

Loading…
Cancel
Save