diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9735aca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +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/ganache/start-blockchain.sh + +packages/*/node_modules +packages/*/dist +packages/*/coverage +# 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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index f37fc48..31c619d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,12 @@ yarn-error.log* # Docker volumes docker/volumes +docker/ganache/volumes docker/reports + +# Env var files docker/env/concordia.env +docker/env/contracts.env # Misc .env.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..97b0b0c --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Concordia +> A distributed forum using Blockchain, supporting direct democratic voting + +## Setup + +```shell script +cd apella +yarn +``` + +## Compile contracts + +```shell script +cd packages/apella-contracts +yarn compile +``` + +## Run app + +```shell script +cd packages/apella-app +yarn start +``` + +## Build app + +```shell script +cd packages/apella-app +yarn build +``` + +## Using Docker images + +This project provides docker images for a number of services required to setup Concordia, as well as for Concordia +itself. + +Check out the README.md in the `./docker` directory diff --git a/docker/Makefile b/docker/Makefile new file mode 100644 index 0000000..0b47f47 --- /dev/null +++ b/docker/Makefile @@ -0,0 +1,67 @@ +PACKAGES := $(abspath ${CURDIR}/../packages) +REPORTS := $(abspath ${CURDIR}/reports) +GANACHE_VOLUMES := $(abspath ${CURDIR}/ganache/volumes) + +run: compose-run build-contracts-migrate run-contracts-migrate build-app run-app + @echo "Concordia is up and running, head over to http://localhost:7777." + +# 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 concordia-ganache +run-ganache: + @docker network create --driver bridge concordia_ganache_network || true &&\ + docker run -d -v ${GANACHE_VOLUMES}/ganache_keys:/home/ganache_keys -p 8545:8545 --env-file=./env/ganache.docker.env --name concordia-ganache --net=concordia_ganache_network concordia-ganache:latest +run-ganache-test: + @docker network create --driver bridge concordia_ganache_test_network || true &&\ + docker run --rm -d -p 8546:8546 --env-file=./env/ganache.test.docker.env --name concordia-ganache-test --net=concordia_ganache_test_network concordia-ganache:latest + +# Rendezvous targets +run-rendezvous: + @docker network create --driver bridge concordia_rendezvous_network || true &&\ + docker run -d -p 9090:9090 --name concordia-rendezvous libp2p/js-libp2p-webrtc-star:version-0.20.5 + +# Contracts targets +build-contracts: + @docker build ../ -f ./concordia-contracts/Dockerfile --target compile -t concordia-contracts --build-arg TZ=Europe/Athens +build-contracts-migrate: + @docker build ../ -f ./concordia-contracts/Dockerfile -t concordia-contracts-migrate --build-arg TZ=Europe/Athens +build-contracts-tests: + @docker build ../ -f ./concordia-contracts/Dockerfile --target test -t concordia-contracts-tests --build-arg TZ=Europe/Athens +run-contracts-tests: + @docker run --rm -v ${REPORTS}/contracts/:/usr/test-reports/ --env-file=./env/contracts.docker.env --net=concordia_ganache_test_network concordia-contracts-tests:latest +run-contracts-tests-host-chain: + @docker run --rm -v ${REPORTS}/contracts/:/usr/test-reports/ --env-file=./env/contracts.env --net=host 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.docker.env --net=concordia_ganache_network 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 concordia-contracts-migrate:latest +get-contracts: + @docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/build --entrypoint=sh concordia-contracts:latest -c 'cp /usr/src/concordia/packages/concordia-contracts/build/* /build' + +# App targets +build-app: + @docker build ../ -f ./concordia-app/Dockerfile -t concordia-app --build-arg TZ=Europe/Athens +build-app-tests: + @docker build ../ -f ./concordia-app/Dockerfile --target test -t concordia-app-tests --build-arg TZ=Europe/Athens +run-app-tests: + @docker run --rm -v ${REPORTS}/app/:/usr/test-reports/ --env-file=./env/concordia.docker.env concordia-app-tests:latest +run-app: + @docker create --env-file=./env/concordia.docker.env -p 7777:80 --name concordia-app --net=concordia_ganache_network concordia-app:latest &&\ + docker network connect concordia_rendezvous_network concordia-app &&\ + docker start concordia-app +run-app-host-chain: + @docker run -d --env-file=./env/concordia.env --name concordia-app --net=host concordia-app:latest + +# Other +clean-images: + @docker rmi `docker images -q -f "dangling=true"` diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..8f0d350 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,204 @@ +# Concordia Dockerized + +This page provides 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. + +### 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 +bellow. + +| 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 +`/home/ganache_keys/keys.json`. If you need to access the keys (eg 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 `/home/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:/home/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 + +This is a provision system that compiles and deploys the contracts to any Ethereum blockchain. + +A Dockerfile is provided in the path `./concordia-contracts` that will build the contracts used by Concordia and +handle their deployment to any Ethereum network defined using env-vars upon container run. Dockerfile contains three +useful stages, described in the table bellow. + +| 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 bellow. + +| 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) | +| DEPLOY_CHAIN_HOST | NaN | Set the hostname of the blockchain network that will be used for deployment (requires network to be "env") | +| DEPLOY_CHAIN_PORT | NaN | Set the port of the blockchain network that will be used for deployment (requires network to be "env") | +| TEST_CHAIN_HOST | NaN | Set the hostname of the blockchain network that will be used for testing (requires network to be "env") | +| TEST_CHAIN_PORT | NaN | Set the port of the blockchain network that will be used for testing (requires network to be "env") | + +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. + +### Application + +The Dockerfile provided in the path `./concordia-application` builds the application for production and serves +the resulting build using an nginx server. Dockerfile contains two useful stages, described in the table bellow. + +| Stage name | Entrypoint | Usage | +| --- | --- | --- | +| test | Runs tests | Fetches npm packages and runs tests | +| runtime | Serves application | Builds for production and serves it through nginx | + + +The image makes use of the environment variables described bellow. + +| Environment variable | Default value | Usage | +| --- | --- | --- | +| REACT_APP_RENDEZVOUS_HOST | 127.0.0.1 | Set the hostname of the rendezvous server | +| REACT_APP_RENDEZVOUS_PORT | 9090 | Set the port of the rendezvous server | + +**Attention**: this image will copy the contract artifacts from the directory `/packages/concordia-contracts/build`. +The image is bound the these artifacts after build. If the contracts change or get re-deployed the image must be +re-built to use the new artifacts. + +**Attention**: 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. + +### 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 suffixed by `.docker` located in the directory `./env`. Using this +environment variables, you can change various configuration options of the testing/production deploys. + +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 and get the required environment variables from different env files, +`./env/contracts.env` and `./env/concordia.env` (notice these env files don't include the `.docker`). These env files do +not exist by default. The values set will largely depend on how you choose to run services in your system (which ports +you use etc.), so be sure to create them before running any `host-chain` target. Luckily example files are provided. diff --git a/docker/concordia-app/Dockerfile b/docker/concordia-app/Dockerfile new file mode 100644 index 0000000..564f0be --- /dev/null +++ b/docker/concordia-app/Dockerfile @@ -0,0 +1,72 @@ +# -------------------------------------------------- +# Stage 1 (Init application build base) +# -------------------------------------------------- +FROM node:14-buster as base +LABEL maintainers.1="Apostolos Fanakis " +LABEL maintainers.2="Panagiotis Nikolaidis " +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 and app, then install base modules +COPY ./packages/concordia-contracts/package.json ./packages/concordia-contracts/package.json +COPY ./packages/concordia-app/package.json ./packages/concordia-app/ + +RUN yarn install --frozen-lockfile + +# Gets the rest of the source code +COPY ./packages/concordia-contracts ./packages/concordia-contracts +COPY ./packages/concordia-app ./packages/concordia-app + +# -------------------------------------------------- +# Stage 2 (Test) +# -------------------------------------------------- +FROM base as test + +# 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 /opt/concordia-app + +COPY ./docker/concordia-app/test-app.sh . + +WORKDIR /usr/src/concordia/packages/concordia-app + +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 (Runtime) +# -------------------------------------------------- +FROM nginx:1.17-alpine as runtime +LABEL maintainers.1="Apostolos Fanakis " +LABEL maintainers.2="Panagiotis Nikolaidis /etc/timezone \ + && apk del tzdata \ + && rm -rf /var/cache/apk/* + +WORKDIR "/var/www/concordia-app" + +COPY ./docker/concordia-app/nginx.conf /etc/nginx/conf.d/default.conf +COPY --chown=nginx:nginx --from=build /usr/src/concordia/packages/concordia-app/build . diff --git a/docker/concordia-app/nginx.conf b/docker/concordia-app/nginx.conf new file mode 100644 index 0000000..cb598c0 --- /dev/null +++ b/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; + } +} diff --git a/docker/concordia-app/test-app.sh b/docker/concordia-app/test-app.sh new file mode 100644 index 0000000..70c4d9d --- /dev/null +++ b/docker/concordia-app/test-app.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +yarn lint -f html -o /usr/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 diff --git a/docker/concordia-contracts/Dockerfile b/docker/concordia-contracts/Dockerfile new file mode 100644 index 0000000..414c2b3 --- /dev/null +++ b/docker/concordia-contracts/Dockerfile @@ -0,0 +1,66 @@ +# -------------------------------------------------- +# Stage 1 (Init contracts build base) +# -------------------------------------------------- +FROM node:14-alpine as base +LABEL maintainers.1="Apostolos Fanakis " +LABEL maintainers.2="Panagiotis Nikolaidis " +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 the contracts package.json, then install modules +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-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 + +ENTRYPOINT ["/opt/concordia-contracts/test-contracts.sh"] + +# -------------------------------------------------- +# Stage 4 (Runtime) +# -------------------------------------------------- +FROM compile as runtime +LABEL maintainers.1="Apostolos Fanakis " +LABEL maintainers.2="Panagiotis Nikolaidis " +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"] diff --git a/docker/concordia-contracts/migrate.sh b/docker/concordia-contracts/migrate.sh new file mode 100644 index 0000000..29d7449 --- /dev/null +++ b/docker/concordia-contracts/migrate.sh @@ -0,0 +1,6 @@ +#!/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 diff --git a/docker/concordia-contracts/test-contracts.sh b/docker/concordia-contracts/test-contracts.sh new file mode 100644 index 0000000..cbae85d --- /dev/null +++ b/docker/concordia-contracts/test-contracts.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +export CHAIN_HOST="$TEST_CHAIN_HOST" +export CHAIN_PORT="$TEST_CHAIN_PORT" + +yarn _eslint -f html -o /usr/test-reports/concordia-contracts-eslint.html --no-color && + (yarn _solhint >/usr/test-reports/concordia-contracts-solhint.report) && + (yarn test --network env >/usr/test-reports/concordia-contracts-truffle-tests.report) + +if [ $? -eq 0 ]; then + echo "TESTS RAN SUCCESSFULLY!" + exit 0 +else + echo "SOME TESTS FAILED!" + exit 1 +fi diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..3fa4386 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + ganache: + build: + context: ../ + dockerfile: ./docker/ganache/Dockerfile + image: concordia-ganache + container_name: concordia-ganache + env_file: + - env/ganache.docker.env + expose: + - 8545 + ports: + - 8545:8545 + user: root + volumes: + - ./ganache/volumes/ganache_keys:/home/ganache_keys + networks: + ganache_network: + restart: always + + rendezvous: + image: libp2p/js-libp2p-webrtc-star:version-0.20.5 + container_name: concordia-rendezvous + networks: + rendezvous_network: + ports: + - 9090:9090 + restart: always + +networks: + ganache_network: + rendezvous_network: diff --git a/docker/env/concordia.docker.env b/docker/env/concordia.docker.env new file mode 100644 index 0000000..1810795 --- /dev/null +++ b/docker/env/concordia.docker.env @@ -0,0 +1,7 @@ +# Variables needed in runtime (in browser) +REACT_APP_RENDEZVOUS_HOST=rendezvous +REACT_APP_RENDEZVOUS_PORT=9090 + +# If the rendezvous server is running on host use these instead +#REACT_APP_RENDEZVOUS_HOST=127.0.0.1 +#REACT_APP_RENDEZVOUS_PORT=9090 diff --git a/docker/env/concordia.example.env b/docker/env/concordia.example.env new file mode 100644 index 0000000..c593d66 --- /dev/null +++ b/docker/env/concordia.example.env @@ -0,0 +1,20 @@ +# Set to "CI" if in CI environment, anything else (including unset) will be ignored +BUILD_ENV={CI} + +# Docker compose variables +VIRTUAL_HOST=example.com +VIRTUAL_PORT=3000 + +# If you uncomment the lines below, Concordia will become available through https BUT the rendezvous +# server will stop working and IPFS initialization won't complete +#LETSENCRYPT_HOST=example.com +#LETSENCRYPT_EMAIL=someemail.email.com + +# Variables needed in runtime +# TO-NEVER-DO: change CONCORDIA_HOST to localhost +CONCORDIA_HOST=0.0.0.0 +CONCORDIA_PORT=3000 + +# Variables needed in runtime (in browser) +REACT_APP_RENDEZVOUS_HOST=xx.xxx.xxx.xxx +REACT_APP_RENDEZVOUS_PORT=9090 diff --git a/docker/env/contracts.docker.env b/docker/env/contracts.docker.env new file mode 100644 index 0000000..1b6d49c --- /dev/null +++ b/docker/env/contracts.docker.env @@ -0,0 +1,14 @@ +# Variables needed in runtime +MIGRATE_NETWORK=env +DEPLOY_CHAIN_HOST=concordia-ganache +DEPLOY_CHAIN_PORT=8545 + +TEST_CHAIN_HOST=concordia-ganache-test +TEST_CHAIN_PORT=8546 + +# If the blockchain is running on host use these instead +#DEPLOY_CHAIN_HOST=127.0.0.1 +#DEPLOY_CHAIN_PORT=8545 + +#TEST_CHAIN_HOST=127.0.0.1 +#TEST_CHAIN_PORT=8546 diff --git a/docker/env/contracts.example.env b/docker/env/contracts.example.env new file mode 100644 index 0000000..3194b9c --- /dev/null +++ b/docker/env/contracts.example.env @@ -0,0 +1,7 @@ +# Variables needed in runtime +MIGRATE_NETWORK=env +DEPLOY_CHAIN_HOST=xx.xxx.xxx.xxx +DEPLOY_CHAIN_PORT=8545 + +TEST_CHAIN_HOST=xx.xxx.xxx.xxx +TEST_CHAIN_PORT=8545 diff --git a/docker/env/ganache.docker.env b/docker/env/ganache.docker.env new file mode 100644 index 0000000..0187b06 --- /dev/null +++ b/docker/env/ganache.docker.env @@ -0,0 +1,5 @@ +ACCOUNTS_NUMBER=10 +ACCOUNTS_ETHER=100 +HOST=0.0.0.0 +PORT=8545 +NETWORK_ID=5778 diff --git a/docker/env/ganache.test.docker.env b/docker/env/ganache.test.docker.env new file mode 100644 index 0000000..479da1f --- /dev/null +++ b/docker/env/ganache.test.docker.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 diff --git a/docker/ganache/Dockerfile b/docker/ganache/Dockerfile new file mode 100644 index 0000000..80eba88 --- /dev/null +++ b/docker/ganache/Dockerfile @@ -0,0 +1,10 @@ +FROM trufflesuite/ganache-cli:latest + +RUN mkdir /home/ganache_db /home/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"] diff --git a/docker/ganache/start-blockchain.sh b/docker/ganache/start-blockchain.sh new file mode 100644 index 0000000..36aceea --- /dev/null +++ b/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 "/home/ganache_keys/keys.json" \ + --db "/home/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 "/home/ganache_keys/keys.json" \ + --db "/home/ganache_db/" \ + --allowUnlimitedContractSize \ + --noVMErrorsOnRPCResponse \ + --deterministic \ + --verbose +fi diff --git a/package.json b/package.json index fafeadd..5c9bdd6 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { - "name": "apella", + "name": "concordia", "private": true, "workspaces": { - "packages": ["packages/*"], - "nohoist": ["**/web3", "**/web3/**"] + "packages": [ + "packages/*" + ], + "nohoist": [ + "**/web3", + "**/web3/**" + ] } } diff --git a/packages/concordia-app/.dockerignore b/packages/concordia-app/.dockerignore new file mode 100644 index 0000000..e69de29 diff --git a/packages/concordia-app/.env.development.example b/packages/concordia-app/.env.development.example new file mode 100644 index 0000000..eeeb884 --- /dev/null +++ b/packages/concordia-app/.env.development.example @@ -0,0 +1,12 @@ +# 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) +# Carefull, IPFS won't accept localhost as a valid hostname +REACT_APP_RENDEZVOUS_HOST=127.0.0.1 +REACT_APP_RENDEZVOUS_PORT=9090 diff --git a/packages/concordia-app/.eslintrc.js b/packages/concordia-app/.eslintrc.js new file mode 100644 index 0000000..96481bd --- /dev/null +++ b/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', + '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'] + } + } + }, +}; diff --git a/packages/concordia-app/.gitignore b/packages/concordia-app/.gitignore new file mode 100644 index 0000000..74ef14b --- /dev/null +++ b/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* diff --git a/packages/concordia-app/package.json b/packages/concordia-app/package.json index 53a2c0b..6645ee2 100644 --- a/packages/concordia-app/package.json +++ b/packages/concordia-app/package.json @@ -8,10 +8,8 @@ "test": "react-scripts test", "eject": "react-scripts eject", "postinstall": "patch-package", - "analyze": "source-map-explorer 'build/static/js/*.js'" - }, - "eslintConfig": { - "extends": "react-app" + "analyze": "react-scripts build && source-map-explorer 'build/static/js/*.js' --gzip", + "lint": "eslint --ext js,jsx . --format table" }, "browserslist": { "production": [ @@ -26,25 +24,38 @@ ] }, "dependencies": { - "@ezerous/breeze": "~0.2.0", - "@ezerous/drizzle": "~0.4.0", + "@ezerous/breeze": "~0.4.0", + "@ezerous/drizzle": "~0.4.1", + "@ezerous/eth-identity-provider": "~0.1.2", "@reduxjs/toolkit": "~1.4.0", + "@welldone-software/why-did-you-render": "^6.0.0-rc.1", "concordia-contracts": "~0.1.0", - "level": "~6.0.1", - "orbit-db-identity-provider": "~0.3.1", + "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-dom": "~16.13.1", + "react-i18next": "^11.7.3", + "react-particles-js": "^3.4.0", "react-redux": "~7.2.1", - "react-router": "~5.2.0", - "react-router-dom": "~5.2.0", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", "react-scripts": "~3.4.3", "redux-saga": "~1.1.3", + "react-timeago": "~5.2.0", "semantic-ui-css": "~2.4.1", "semantic-ui-react": "~1.2.1", "web3": "1.3.0" }, "devDependencies": { + "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" diff --git a/packages/concordia-app/public/index.html b/packages/concordia-app/public/index.html index 9426dd5..99d6395 100644 --- a/packages/concordia-app/public/index.html +++ b/packages/concordia-app/public/index.html @@ -19,7 +19,6 @@ 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`. --> - Concordia diff --git a/packages/concordia-app/public/locales/en/translation.json b/packages/concordia-app/public/locales/en/translation.json new file mode 100644 index 0000000..74f1e18 --- /dev/null +++ b/packages/concordia-app/public/locales/en/translation.json @@ -0,0 +1,81 @@ +{ + "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": "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", + "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.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.clear.databases": "Clear databases", + "topbar.button.create.topic": "Create topic", + "topbar.button.profile": "Profile", + "topbar.button.register": "Sign Up", + "topic.create.form.content.field.label": "First post content", + "topic.create.form.content.field.placeholder": "Message", + "topic.create.form.post.button": "Post", + "topic.create.form.subject.field.label": "Topic subject", + "topic.create.form.subject.field.placeholder": "Subject", + "topic.list.row.author": "by {{author}}", + "topic.list.row.number.of.replies": "{{numberOfReplies}} replies", + "topic.list.row.topic.id": "#{{id}}", + "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" +} \ No newline at end of file diff --git a/packages/concordia-app/public/manifest.json b/packages/concordia-app/public/manifest.json index ce536e4..60b7e11 100644 --- a/packages/concordia-app/public/manifest.json +++ b/packages/concordia-app/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "Apella", - "name": "Apella", + "short_name": "Concordia", + "name": "Concordia", "icons": [ { "src": "favicon.ico", diff --git a/packages/concordia-app/src/App.jsx b/packages/concordia-app/src/App.jsx new file mode 100644 index 0000000..4e97d13 --- /dev/null +++ b/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 }) => ( + + + + + + + +); + +App.propTypes = { + // eslint-disable-next-line react/forbid-prop-types + store: PropTypes.object.isRequired, +}; + +export default App; diff --git a/packages/concordia-app/src/Routes.jsx b/packages/concordia-app/src/Routes.jsx new file mode 100644 index 0000000..15a11b2 --- /dev/null +++ b/packages/concordia-app/src/Routes.jsx @@ -0,0 +1,90 @@ +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: () => , + }, + { + 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: () => , + }, + ], + }, + { + path: '*', + layout: MainLayout, + routes: [ + { + exact: true, + path: '/home', + component: lazy(() => import('./views/Home')), + }, + { + 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: () => , + }, + ], + }, +]; + +const renderRoutes = (routes) => (routes ? ( + }> + + {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.routes + ? renderRoutes(route.routes) + : } + + )} + /> + ); + })} + + +) : null); + +function Routes() { + return renderRoutes(routesConfig); +} + +export default Routes; diff --git a/packages/concordia-app/src/assets/css/app.css b/packages/concordia-app/src/assets/css/app.css deleted file mode 100644 index 07fe37e..0000000 --- a/packages/concordia-app/src/assets/css/app.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - margin: 1em !important; -} diff --git a/packages/concordia-app/src/assets/css/index.css b/packages/concordia-app/src/assets/css/index.css index 76a3f26..2a2d849 100644 --- a/packages/concordia-app/src/assets/css/index.css +++ b/packages/concordia-app/src/assets/css/index.css @@ -1,4 +1,17 @@ -body { - margin: 10em; - padding: 0; +body.app { + overflow: auto; + margin: 1em !important; +} + +#root { + height: 100%; +} + +.i18next-newlines { + white-space: pre-line !important; +} + +.text-secondary { + color: gray; + font-style: italic; } diff --git a/packages/concordia-app/src/assets/css/loading-component.css b/packages/concordia-app/src/assets/css/loading-component.css index e4635a2..37a8f96 100644 --- a/packages/concordia-app/src/assets/css/loading-component.css +++ b/packages/concordia-app/src/assets/css/loading-component.css @@ -2,16 +2,16 @@ body { overflow: hidden; } -ul { - list-style-position: inside; -} - .loading-screen { margin-top: 10em; text-align: center; font-size: large; } +.loading-screen ul { + list-style-position: inside; +} + .loading-img { margin-bottom: 3em; height: 12em; diff --git a/packages/concordia-app/src/assets/images/metamask_logo.svg b/packages/concordia-app/src/assets/images/metamask_logo.svg new file mode 100644 index 0000000..a6cffef --- /dev/null +++ b/packages/concordia-app/src/assets/images/metamask_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/concordia-app/src/assets/images/orbitdb_logo.png b/packages/concordia-app/src/assets/images/orbitdb_logo.png deleted file mode 100644 index 39dc99c..0000000 Binary files a/packages/concordia-app/src/assets/images/orbitdb_logo.png and /dev/null differ diff --git a/packages/concordia-app/src/assets/images/orbitdb_logo.svg b/packages/concordia-app/src/assets/images/orbitdb_logo.svg new file mode 100644 index 0000000..01c0d9b --- /dev/null +++ b/packages/concordia-app/src/assets/images/orbitdb_logo.svg @@ -0,0 +1 @@ +orbit_db_logo_color \ No newline at end of file diff --git a/packages/concordia-app/src/assets/particles.js b/packages/concordia-app/src/assets/particles.js new file mode 100644 index 0000000..1862f6f --- /dev/null +++ b/packages/concordia-app/src/assets/particles.js @@ -0,0 +1,32 @@ +const particlesOptions = { + particles: { + number: { + value: 90, + density: { + enable: true, + value_area: 1500, + }, + }, + line_linked: { + enable: true, + opacity: 0.04, + }, + move: { + direction: 'none', + speed: 0.12, + }, + size: { + value: 1, + }, + opacity: { + anim: { + enable: true, + speed: 1.3, + opacity_min: 0.05, + }, + }, + }, + retina_detect: true, +}; + +export default particlesOptions; diff --git a/packages/concordia-app/src/components/App.jsx b/packages/concordia-app/src/components/App.jsx deleted file mode 100644 index 4086e07..0000000 --- a/packages/concordia-app/src/components/App.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { Provider } from 'react-redux' -import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' -import LoadingContainer from './LoadingContainer' -import PropTypes from 'prop-types' - -// CSS -import '../assets/css/app.css'; - -import CoreLayoutContainer from './CoreLayoutContainer'; -import HomeContainer from './HomeContainer'; -import NotFound from '../components/NotFound'; - - -const App = ({ store }) => ( - - - - - - - - - - - - -) - -App.propTypes = { - store: PropTypes.object.isRequired -} - -export default App diff --git a/packages/concordia-app/src/components/AppContext.js b/packages/concordia-app/src/components/AppContext.js deleted file mode 100644 index 5534716..0000000 --- a/packages/concordia-app/src/components/AppContext.js +++ /dev/null @@ -1,72 +0,0 @@ -// Modified version of https://github.com/trufflesuite/drizzle/blob/develop/packages/react-plugin/src/DrizzleContext.js -import React from "react"; - -const Context = React.createContext(); - -class Provider extends React.Component { - state = { - drizzleState: null, - drizzleInitialized: false, - breezeState: null, - breezeInitialized: false - }; - - componentDidMount() { - const { drizzle, breeze } = this.props; - // subscribe to changes in the store, keep state up-to-date - this.unsubscribe = drizzle.store.subscribe(() => { - const drizzleState = drizzle.store.getState(); - const breezeState = breeze.store.getState(); - - if (drizzleState.drizzleStatus.initialized) { - this.setState({ - drizzleState, - drizzleInitialized: true - }); - } - if (breezeState.breezeStatus.initialized) { - this.setState({ - breezeState: breezeState, - breezeInitialized: true - }); - } - }); - - this.unsubscribe = breeze.store.subscribe(() => { - const breezeState = breeze.store.getState(); - if (breezeState.breezeStatus.initialized) { - this.setState({ - breezeState: breezeState, - breezeInitialized: true - }); - } - }); - } - - componentWillUnmount() { - this.unsubscribe(); - } - - render() { - return ( - - {this.props.children} - - ); - } -} - -export default { - Context: Context, - Consumer: Context.Consumer, - Provider -}; diff --git a/packages/concordia-app/src/components/AppContext.jsx b/packages/concordia-app/src/components/AppContext.jsx new file mode 100644 index 0000000..6b8b739 --- /dev/null +++ b/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 ( + + {children} + + ); + } +} + +export default { + Context, + Consumer: Context.Consumer, + Provider, +}; diff --git a/packages/concordia-app/src/components/ClearDatabasesModal/index.jsx b/packages/concordia-app/src/components/ClearDatabasesModal/index.jsx new file mode 100644 index 0000000..26e47fe --- /dev/null +++ b/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 === 'concordia') { + setUserConfirmed(true); + } else { + setUserConfirmed(false); + } + }, [confirmationInput, user.hasSignedUp, user.username]); + + const handleSubmit = useCallback(() => { + setIsClearing(true); + + purgeIndexedDBs() + .then(() => { + onDatabasesCleared(); + }).catch((reason) => console.log(reason)); + }, [onDatabasesCleared]); + + const onCancelTry = useCallback(() => { + if (!isClearing) { + setConfirmationInput(''); + onCancel(); + } + }, [isClearing, onCancel]); + + const handleInputChange = (event, { value }) => { setConfirmationInput(value); }; + + const modalContent = useMemo(() => { + if (isClearing) { + return ( + <> +

+ {t('clear.databases.modal.clearing.progress.message')} +

+ + ); + } + + if (user.hasSignedUp) { + return ( + <> +

+ {t('clear.databases.modal.description.pre')} +

+

+ {t('clear.databases.modal.description.body.user')} +

+
+ + + + +
+ + ); + } + + return ( + <> +

+ {t('clear.databases.modal.description.pre')} +

+
+ + + + +
+ + ); + }, [confirmationInput, isClearing, t, user.hasSignedUp]); + + return useMemo(() => ( + + + {isClearing + ? t('clear.databases.modal.clearing.progress.title') + : t('clear.databases.modal.title')} + + + + {modalContent} + + + + {!isClearing && ( + + + + + )} + + ), [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; diff --git a/packages/concordia-app/src/components/CoreLayoutContainer.jsx b/packages/concordia-app/src/components/CoreLayoutContainer.jsx deleted file mode 100644 index 4a72130..0000000 --- a/packages/concordia-app/src/components/CoreLayoutContainer.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import MenuComponent from './MenuComponent'; - -export default class CoreLayout extends Component { - render() { - return ( -
- - {this.props.children} -
- ) - } -} - -CoreLayout.propTypes = { - children: PropTypes.element.isRequired -}; diff --git a/packages/concordia-app/src/components/CustomLoadingTabPane.jsx b/packages/concordia-app/src/components/CustomLoadingTabPane.jsx new file mode 100644 index 0000000..1b363a8 --- /dev/null +++ b/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 ( + + + + {loadingMessage !== undefined + ? loadingMessage + : t('custom.loading.tab.pane.default.generic.message')} + + + + + + + + + ); + } + + return ( + + {children} + + ); + }, [children, loading, loadingMessage, t]); +}; + +CustomLoadingTabPane.propTypes = { + loading: PropTypes.bool, + loadingMessage: PropTypes.string, + children: PropTypes.element.isRequired, +}; + +export default CustomLoadingTabPane; diff --git a/packages/concordia-app/src/components/HomeContainer.jsx b/packages/concordia-app/src/components/HomeContainer.jsx deleted file mode 100644 index 6fdafbb..0000000 --- a/packages/concordia-app/src/components/HomeContainer.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { Component } from 'react'; - -class HomeContainer extends Component { - render() { - return(

TODO: Home Container

); - } -} - -export default HomeContainer; diff --git a/packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx b/packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx new file mode 100644 index 0000000..32ea822 --- /dev/null +++ b/packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx @@ -0,0 +1,72 @@ +import React, { useEffect } 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.png'; + +const LoadingComponent = (props) => { + useEffect(() => function cleanup() { + document.body.classList.add('app'); + }, []); + + 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) =>
  • {listItem}
  • ); + } + + const list = messageList ?
      {listItems}
    : ''; + + return ( +
    + + {imageAlt} +

    {title}

    +

    {message}

    + {list} +
    + +
    + ); +}; + +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; diff --git a/packages/concordia-app/src/components/InitializationScreen/index.jsx b/packages/concordia-app/src/components/InitializationScreen/index.jsx new file mode 100644 index 0000000..7a2dc5a --- /dev/null +++ b/packages/concordia-app/src/components/InitializationScreen/index.jsx @@ -0,0 +1,163 @@ +import React, { Children } from 'react'; +import { breezeConstants } from '@ezerous/breeze'; +import { useSelector } from 'react-redux'; +import CustomLoader from './CustomLoader'; + +// CSS +import '../../assets/css/loading-component.css'; +import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames'; + +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 ( + MetaMask, ' first.']} + imageType="metamask" + progress={10} + progressType="error" + /> + ); + } + + if ((web3Status === 'initializing' || !web3NetworkId) && !web3NetworkFailed) { + return ( + + ); + } + + if (web3Status === 'failed' || web3NetworkFailed) { + return ( + + ); + } + + if (web3Status === 'initialized' && web3AccountsFailed) { + return ( + + ); + } + + if (initializing || (!failed && !contractInitialized && contractDeployed)) { + return ( + + ); + } + + if (!contractDeployed) { + return ( + + ); + } + + if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) { + return ( + + ); + } + + if (ipfsStatus === breezeConstants.STATUS_FAILED) { + return ( + + ); + } + + if (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 ( + + ); + } + + if (orbitStatus === breezeConstants.STATUS_FAILED) { + return ( + + ); + } + + if (!userFetched) { + return ( + + ); + } + + return Children.only(children); +}; + +export default InitializationLoader; diff --git a/packages/concordia-app/src/components/LoadingComponent.jsx b/packages/concordia-app/src/components/LoadingComponent.jsx deleted file mode 100644 index 6f4ab70..0000000 --- a/packages/concordia-app/src/components/LoadingComponent.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { Container, Progress } from 'semantic-ui-react'; - -// CSS -import '../assets/css/loading-component.css'; - -// Images -import ethereum_logo from '../assets/images/ethereum_logo.svg'; -import ipfs_logo from '../assets/images/ipfs_logo.svg'; -import orbitdb_logo from '../assets/images/orbitdb_logo.png'; -import app_logo from '../assets/images/app_logo.png'; - -class LoadingComponent extends Component { - render(){ - const { image_type, message_list, progress_type } = this.props ; - let imageSrc, imageAlt, listItems, indicating, error; - - if (image_type === "ethereum"){ - imageSrc = ethereum_logo; - imageAlt = "ethereum_logo"; - } - else if (image_type === "ipfs"){ - imageSrc = ipfs_logo; - imageAlt = "ipfs_logo"; - } - else if (image_type === "orbit"){ - imageSrc = orbitdb_logo; - imageAlt = "orbitdb_logo"; - } - else if (image_type === "app"){ - imageSrc = app_logo; - imageAlt = "app_logo"; - } - - if(progress_type === "indicating") - indicating = true; - else if(progress_type === "error") - error = true; - - if(message_list){ - listItems = message_list.map((listItem) => -
  • {listItem}
  • - ); - } - - const list = message_list ?
      {listItems}
    : ''; - - return( -
    - - {imageAlt} -

    {this.props.title}

    -

    {this.props.message}

    - {list} -
    - -
    - ); - } -} - -LoadingComponent.propTypes = { - title: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, - message_list: PropTypes.arrayOf(PropTypes.string), - image_type: PropTypes.string.isRequired, - progress: PropTypes.number.isRequired, - progress_type: PropTypes.string.isRequired, -}; - -export default LoadingComponent; diff --git a/packages/concordia-app/src/components/LoadingContainer.jsx b/packages/concordia-app/src/components/LoadingContainer.jsx deleted file mode 100644 index c8ed6e0..0000000 --- a/packages/concordia-app/src/components/LoadingContainer.jsx +++ /dev/null @@ -1,136 +0,0 @@ -import React, { Children, Component } from 'react'; -import { connect } from 'react-redux'; - -import { breezeConstants } from '@ezerous/breeze' - -import LoadingComponent from './LoadingComponent'; - -// CSS -import '../assets/css/loading-component.css'; - -class LoadingContainer extends Component { - render() { - if ((this.props.web3.status === 'initializing' || !this.props.web3.networkId) - && !this.props.web3.networkFailed) { - return - } - - if (this.props.web3.status === 'failed' || this.props.web3.networkFailed) { - return - } - - if (this.props.web3.status === 'initialized' && this.props.web3.accountsFailed) { - return - } - - if (this.props.drizzleStatus.initializing - || (!this.props.drizzleStatus.failed && !this.props.contractInitialized && this.props.contractDeployed )){ - return - } - - if (!this.props.contractDeployed) { - return - } - - if (this.props.ipfsStatus === breezeConstants.STATUS_INITIALIZING) { - return - } - - if (this.props.ipfsStatus === breezeConstants.STATUS_FAILED) { - return - } - - if (this.props.orbitStatus === breezeConstants.STATUS_INITIALIZING) { - const message = process.env.NODE_ENV === 'development' - ? 'If needed, please sign the transaction in MetaMask to create the databases.' - : 'Please sign the transaction in MetaMask to create the databases.'; - return - } - - if (this.props.orbitStatus === breezeConstants.STATUS_FAILED) { - return - } - - if (!this.props.userFetched){ - return - } - - return Children.only(this.props.children); - } -} - -const mapStateToProps = (state) => ({ - drizzleStatus: state.drizzleStatus, - breezeStatus: state.breezeStatus, - ipfsStatus: state.ipfs.status, - orbitStatus: state.orbit.status, - web3: state.web3, - accounts: state.accounts, - contractInitialized: state.contracts.Forum.initialized, - contractDeployed: state.contracts.Forum.deployed, - userFetched: state.user.address -}); - -export default connect(mapStateToProps)(LoadingContainer); diff --git a/packages/concordia-app/src/components/LoadingScreen.jsx b/packages/concordia-app/src/components/LoadingScreen.jsx new file mode 100644 index 0000000..da719f1 --- /dev/null +++ b/packages/concordia-app/src/components/LoadingScreen.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const LoadingScreen = () => ( +
    + Loading +
    +); + +export default LoadingScreen; diff --git a/packages/concordia-app/src/components/MenuComponent.jsx b/packages/concordia-app/src/components/MenuComponent.jsx deleted file mode 100644 index c71b040..0000000 --- a/packages/concordia-app/src/components/MenuComponent.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { Component } from 'react'; -import { withRouter } from "react-router"; -import { Menu } from 'semantic-ui-react'; - -import AppContext from "./AppContext"; - -import app_logo from '../assets/images/app_logo.png'; -import SignUpForm from './SignUpForm'; - -class MenuComponent extends Component { - render() { - return ( - - {context => { - return( -
    - - { this.props.history.push("/"); }} - > - app_logo - - - - - -
    - ) - } - } -
    - ) - } -} - -export default withRouter(MenuComponent); diff --git a/packages/concordia-app/src/components/NotFound.jsx b/packages/concordia-app/src/components/NotFound.jsx index c73a7e5..9b5be95 100644 --- a/packages/concordia-app/src/components/NotFound.jsx +++ b/packages/concordia-app/src/components/NotFound.jsx @@ -3,7 +3,7 @@ import pageNotFound from '../assets/images/PageNotFound.jpg'; const NotFound = () => (
    Page not found! diff --git a/packages/concordia-app/src/components/PostCreate/index.jsx b/packages/concordia-app/src/components/PostCreate/index.jsx new file mode 100644 index 0000000..27d966a --- /dev/null +++ b/packages/concordia-app/src/components/PostCreate/index.jsx @@ -0,0 +1,183 @@ +import React, { + memo, useCallback, useEffect, useState, +} from 'react'; +import { + Button, Feed, Form, Icon, Image, TextArea, +} from 'semantic-ui-react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import determineKVAddress from '../../utils/orbitUtils'; +import { POSTS_DATABASE, USER_DATABASE } from '../../constants/orbit/OrbitDatabases'; +import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions'; +import { USER_PROFILE_PICTURE } from '../../constants/orbit/UserDatabaseKeys'; +import { breeze, drizzle } from '../../redux/store'; +import './styles.css'; +import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus'; +import { POST_CONTENT } from '../../constants/orbit/PostsDatabaseKeys'; +import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames'; +import { POST_CREATED_EVENT } from '../../constants/contracts/events/ForumContractEvents'; + +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 [userProfilePictureUrl, setUserProfilePictureUrl] = 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) { + setUserProfilePictureUrl(userFound[USER_PROFILE_PICTURE]); + } else { + 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, + }, { pin: true }) + .then(() => { + setPostContent(''); + setPosting(false); + setStoringPost(false); + setCreatePostCacheSendStackId(''); + }) + .catch((reason) => { + console.log(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 ( + + + + {userProfilePictureUrl + ? ( + + ) + : ( + + )} + + + +
    +