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/LICENSE b/LICENSE new file mode 100644 index 0000000..6e4a383 --- /dev/null +++ b/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. 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/.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 f2bcbf9..6645ee2 100644 --- a/packages/concordia-app/package.json +++ b/packages/concordia-app/package.json @@ -25,8 +25,8 @@ }, "dependencies": { "@ezerous/breeze": "~0.4.0", - "@ezerous/drizzle": "~0.4.0", - "@ezerous/eth-identity-provider": "^0.1.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", @@ -44,6 +44,7 @@ "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" diff --git a/packages/concordia-app/public/locales/en/translation.json b/packages/concordia-app/public/locales/en/translation.json index c55754f..74f1e18 100644 --- a/packages/concordia-app/public/locales/en/translation.json +++ b/packages/concordia-app/public/locales/en/translation.json @@ -2,22 +2,80 @@ "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.button.guest": "Continue as guest", - "register.form.button.submit": "Sign Up", - "register.form.error.message.header": "Form contains errors", - "register.form.error.username.taken.message": "The username {{username}} is already taken.", "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.username.field.label": "Username", - "register.form.username.field.placeholder": "Username", + "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.create.form.message.field.label": "First post message", - "topic.create.form.message.field.placeholder": "Message", - "topic.create.form.post.button": "Post" + "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 index 7ed70c0..4e97d13 100644 --- a/packages/concordia-app/src/App.jsx +++ b/packages/concordia-app/src/App.jsx @@ -2,23 +2,23 @@ import React from 'react'; import { Provider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; import PropTypes from 'prop-types'; -import LoadingContainer from './components/LoadingContainer'; +import InitializationScreen from './components/InitializationScreen'; import Routes from './Routes'; import './intl/index'; import 'semantic-ui-css/semantic.min.css'; -import './assets/css/app.css'; const App = ({ store }) => ( - + - + ); App.propTypes = { + // eslint-disable-next-line react/forbid-prop-types store: PropTypes.object.isRequired, }; diff --git a/packages/concordia-app/src/Routes.jsx b/packages/concordia-app/src/Routes.jsx index 9931cfa..15a11b2 100644 --- a/packages/concordia-app/src/Routes.jsx +++ b/packages/concordia-app/src/Routes.jsx @@ -44,6 +44,11 @@ const routesConfig = [ 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: () => , }, 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 4e01f90..0000000 --- a/packages/concordia-app/src/assets/css/app.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - margin: 1em !important; -} - -.i18next-newlines { - white-space: pre-line !important; -} \ No newline at end of file diff --git a/packages/concordia-app/src/assets/css/index.css b/packages/concordia-app/src/assets/css/index.css new file mode 100644 index 0000000..2a2d849 --- /dev/null +++ b/packages/concordia-app/src/assets/css/index.css @@ -0,0 +1,17 @@ +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/particles.js b/packages/concordia-app/src/assets/particles.js index 20cfa33..1862f6f 100644 --- a/packages/concordia-app/src/assets/particles.js +++ b/packages/concordia-app/src/assets/particles.js @@ -1,7 +1,7 @@ const particlesOptions = { particles: { number: { - value: 60, + value: 90, density: { enable: true, value_area: 1500, @@ -9,11 +9,11 @@ const particlesOptions = { }, line_linked: { enable: true, - opacity: 0.02, + opacity: 0.04, }, move: { - direction: 'right', - speed: 0.05, + direction: 'none', + speed: 0.12, }, size: { value: 1, @@ -21,24 +21,11 @@ const particlesOptions = { opacity: { anim: { enable: true, - speed: 1, + speed: 1.3, opacity_min: 0.05, }, }, }, - interactivity: { - events: { - onclick: { - enable: true, - mode: 'push', - }, - }, - modes: { - push: { - particles_nb: 1, - }, - }, - }, retina_detect: true, }; 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/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/LoadingComponent.jsx b/packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx similarity index 73% rename from packages/concordia-app/src/components/LoadingComponent.jsx rename to packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx index 4875800..32ea822 100644 --- a/packages/concordia-app/src/components/LoadingComponent.jsx +++ b/packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx @@ -1,25 +1,30 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { Container, Progress } from 'semantic-ui-react'; -// CSS -import '../assets/css/loading-component.css'; - // Images -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'; +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 === 'ethereum') { + if (imageType === 'metamask') { + imageSrc = metamaskLogo; + imageAlt = 'metamask_logo'; + } else if (imageType === 'ethereum') { imageSrc = ethereumLogo; imageAlt = 'ethereum_logo'; } else if (imageType === 'ipfs') { 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/LoadingContainer.jsx b/packages/concordia-app/src/components/LoadingContainer.jsx deleted file mode 100644 index 0260132..0000000 --- a/packages/concordia-app/src/components/LoadingContainer.jsx +++ /dev/null @@ -1,167 +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() { - const { - web3: { - status, networkId, networkFailed, accountsFailed, - }, - drizzleStatus: { - initializing, - failed, - }, - contractInitialized, contractDeployed, ipfsStatus, orbitStatus, userFetched, children, - } = this.props; - - if ((status === 'initializing' || !networkId) - && !networkFailed) { - return ( - - ); - } - - if (status === 'failed' || networkFailed) { - return ( - - ); - } - - if (status === 'initialized' && accountsFailed) { - 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); - } -} - -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/Placeholder/index.jsx b/packages/concordia-app/src/components/Placeholder/index.jsx deleted file mode 100644 index 5c9394e..0000000 --- a/packages/concordia-app/src/components/Placeholder/index.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { List } from 'semantic-ui-react'; -import { PLACEHOLDER_TYPE_POST, PLACEHOLDER_TYPE_TOPIC } from '../../constants/PlaceholderTypes'; - -const Placeholder = (props) => { - const { placeholderType, extra } = props; - - switch (placeholderType) { - case PLACEHOLDER_TYPE_TOPIC: - return ( - <> - - - topicSubject - - - username - Number of Replies - timestamp - - - ); - case PLACEHOLDER_TYPE_POST: - return ( -
LOADING POST
- ); - default: - return
; - } -}; - -const TopicPlaceholderExtra = PropTypes.PropTypes.shape({ - topicId: PropTypes.number.isRequired, -}); - -const PostPlaceholderExtra = PropTypes.PropTypes.shape({ - postIndex: PropTypes.number.isRequired, -}); - -Placeholder.propTypes = { - placeholderType: PropTypes.string.isRequired, - extra: PropTypes.oneOfType([ - TopicPlaceholderExtra.isRequired, - PostPlaceholderExtra.isRequired, - ]), -}; - -export default Placeholder; 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 + ? ( + + ) + : ( + + )} + + + +
+