mirror of https://gitlab.com/ecentrics/concordia
Apostolos Fanakis
4 years ago
137 changed files with 7292 additions and 2961 deletions
@ -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 |
@ -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 |
@ -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"` |
@ -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. |
||||
|
|
||||
|
### <a name="piat-mkfile-targets"></a> Makefile targets |
||||
|
|
||||
|
Concordia uses blockchain and other distributed technologies. There are a number of ways to set up a running instance of |
||||
|
this application. |
||||
|
|
||||
|
This chapter will guide you through simple setups for testing and production that depend on local blockchain (ganache) |
||||
|
instances which do not require real ETH to work or have any other charges. |
||||
|
|
||||
|
#### Testing the contracts |
||||
|
|
||||
|
Build the ganache image and spin up a blockchain for testing: |
||||
|
|
||||
|
```shell |
||||
|
make build-ganache run-ganache-test |
||||
|
``` |
||||
|
|
||||
|
Build the testing stage of the contracts image: |
||||
|
|
||||
|
```shell |
||||
|
make build-contracts-tests |
||||
|
``` |
||||
|
|
||||
|
Run the tests: |
||||
|
|
||||
|
```shell |
||||
|
make run-contracts-tests |
||||
|
``` |
||||
|
|
||||
|
The results should be printed in the terminal, but are also available in the directory `./reports/contracts`. |
||||
|
|
||||
|
#### Testing the application |
||||
|
|
||||
|
Build the testing stage of the application image: |
||||
|
|
||||
|
```shell |
||||
|
make build-app-tests |
||||
|
``` |
||||
|
|
||||
|
Run the test: |
||||
|
|
||||
|
```shell |
||||
|
make run-app-tests |
||||
|
``` |
||||
|
|
||||
|
The results should be printed in the terminal, but are also available in the directory `./reports/app`. |
||||
|
|
||||
|
#### Production |
||||
|
|
||||
|
Just run the target: |
||||
|
|
||||
|
```shell |
||||
|
make run |
||||
|
``` |
||||
|
|
||||
|
And you' re done! Head to [localhost:7777](localhost:7777) and voilà, a working Concordia instance appears! The |
||||
|
blockchain is exposed in the address `localhost:8545`. |
||||
|
|
||||
|
**Tip**: the accounts (private keys) generated by Ganache are available in the file `./volumes/ganache_keys/keys.json`. |
||||
|
|
||||
|
Note that the `make run` command might take several minutes to execute (depending on your system). What happens under |
||||
|
the hood is that: |
||||
|
|
||||
|
- the ganache image is built |
||||
|
- blockchain and rendezvous server containers are started |
||||
|
- migration stage of the contracts image is built |
||||
|
- the contracts are deployed to the blockchain: |
||||
|
- the application image is built and then deployed |
||||
|
|
||||
|
### Env Files |
||||
|
|
||||
|
Targets in the Makefile make use of env files 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. |
@ -0,0 +1,72 @@ |
|||||
|
# -------------------------------------------------- |
||||
|
# Stage 1 (Init application build base) |
||||
|
# -------------------------------------------------- |
||||
|
FROM node:14-buster as base |
||||
|
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>" |
||||
|
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>" |
||||
|
LABEL gr.thmmy.ecentrics.concordia-image.name="app" |
||||
|
|
||||
|
WORKDIR /usr/src/concordia |
||||
|
|
||||
|
# Copy the root package.json and yarn.lock |
||||
|
COPY ./package.json . |
||||
|
COPY ./yarn.lock . |
||||
|
|
||||
|
# Copy package.json files from contracts 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 <apostolof@auth.gr>" |
||||
|
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com" |
||||
|
LABEL gr.thmmy.ecentrics.concordia-image.name="app" |
||||
|
|
||||
|
# Fix timezome |
||||
|
ARG TZ |
||||
|
|
||||
|
RUN apk add -U tzdata \ |
||||
|
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \ |
||||
|
&& echo $TZ > /etc/timezone \ |
||||
|
&& apk del tzdata \ |
||||
|
&& rm -rf /var/cache/apk/* |
||||
|
|
||||
|
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 . |
@ -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; |
||||
|
} |
||||
|
} |
@ -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 |
@ -0,0 +1,66 @@ |
|||||
|
# -------------------------------------------------- |
||||
|
# Stage 1 (Init contracts build base) |
||||
|
# -------------------------------------------------- |
||||
|
FROM node:14-alpine as base |
||||
|
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>" |
||||
|
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>" |
||||
|
LABEL gr.thmmy.ecentrics.concordia-image.name="contracts" |
||||
|
|
||||
|
# Fix timezome (needed for timestamps on report files) |
||||
|
ARG TZ |
||||
|
|
||||
|
RUN apk add -U tzdata \ |
||||
|
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \ |
||||
|
&& echo $TZ > /etc/timezone \ |
||||
|
&& apk del tzdata \ |
||||
|
&& rm -rf /var/cache/apk/* |
||||
|
|
||||
|
WORKDIR /usr/src/concordia |
||||
|
|
||||
|
# Copy the root package.json and yarn.lock |
||||
|
COPY ./package.json . |
||||
|
COPY ./yarn.lock . |
||||
|
|
||||
|
# Copy 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 <apostolof@auth.gr>" |
||||
|
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>" |
||||
|
LABEL gr.thmmy.ecentrics.concordia-image.name="contracts" |
||||
|
|
||||
|
WORKDIR /opt/concordia-contracts |
||||
|
|
||||
|
COPY ./docker/concordia-contracts/migrate.sh . |
||||
|
RUN ["chmod", "+x", "/opt/concordia-contracts/migrate.sh"] |
||||
|
|
||||
|
ENTRYPOINT ["/opt/concordia-contracts/migrate.sh"] |
@ -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 |
@ -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 |
@ -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: |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -0,0 +1,5 @@ |
|||||
|
ACCOUNTS_NUMBER=10 |
||||
|
ACCOUNTS_ETHER=100 |
||||
|
HOST=0.0.0.0 |
||||
|
PORT=8545 |
||||
|
NETWORK_ID=5778 |
@ -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 |
@ -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"] |
@ -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 |
@ -1,8 +1,13 @@ |
|||||
{ |
{ |
||||
"name": "apella", |
"name": "concordia", |
||||
"private": true, |
"private": true, |
||||
"workspaces": { |
"workspaces": { |
||||
"packages": ["packages/*"], |
"packages": [ |
||||
"nohoist": ["**/web3", "**/web3/**"] |
"packages/*" |
||||
|
], |
||||
|
"nohoist": [ |
||||
|
"**/web3", |
||||
|
"**/web3/**" |
||||
|
] |
||||
} |
} |
||||
} |
} |
||||
|
@ -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 |
@ -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'] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
}; |
@ -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* |
@ -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" |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Provider } from 'react-redux'; |
||||
|
import { BrowserRouter as Router } from 'react-router-dom'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import InitializationScreen from './components/InitializationScreen'; |
||||
|
import Routes from './Routes'; |
||||
|
import './intl/index'; |
||||
|
import 'semantic-ui-css/semantic.min.css'; |
||||
|
|
||||
|
const App = ({ store }) => ( |
||||
|
<Provider store={store}> |
||||
|
<InitializationScreen> |
||||
|
<Router> |
||||
|
<Routes /> |
||||
|
</Router> |
||||
|
</InitializationScreen> |
||||
|
</Provider> |
||||
|
); |
||||
|
|
||||
|
App.propTypes = { |
||||
|
// eslint-disable-next-line react/forbid-prop-types |
||||
|
store: PropTypes.object.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default App; |
@ -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: () => <Redirect to="/home" />, |
||||
|
}, |
||||
|
{ |
||||
|
exact: true, |
||||
|
path: '/404', |
||||
|
layout: MainLayout, |
||||
|
component: lazy(() => import('./components/NotFound')), |
||||
|
}, |
||||
|
{ |
||||
|
path: '/auth', |
||||
|
layout: RegisterLayout, |
||||
|
routes: [ |
||||
|
{ |
||||
|
exact: true, |
||||
|
path: '/auth/register', |
||||
|
component: lazy(() => import('./views/Register')), |
||||
|
}, |
||||
|
{ |
||||
|
component: () => <Redirect to="/404" />, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
path: '*', |
||||
|
layout: MainLayout, |
||||
|
routes: [ |
||||
|
{ |
||||
|
exact: true, |
||||
|
path: '/home', |
||||
|
component: lazy(() => import('./views/Home')), |
||||
|
}, |
||||
|
{ |
||||
|
exact: true, |
||||
|
path: '/topics/:id(\\bnew\\b|\\d+)', |
||||
|
component: lazy(() => import('./views/Topic')), |
||||
|
}, |
||||
|
{ |
||||
|
exact: true, |
||||
|
path: ['/users/:id', '/profiles/:id', '/profile'], |
||||
|
component: lazy(() => import('./views/Profile')), |
||||
|
}, |
||||
|
{ |
||||
|
component: () => <Redirect to="/404" />, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const renderRoutes = (routes) => (routes ? ( |
||||
|
<Suspense fallback={<LoadingScreen />}> |
||||
|
<Switch> |
||||
|
{routes.map((route, i) => { |
||||
|
const Layout = route.layout || Fragment; |
||||
|
const Component = route.component; |
||||
|
|
||||
|
const key = route.path ? route.path.concat(i) : ''.concat(i); |
||||
|
return ( |
||||
|
<Route |
||||
|
key={key} |
||||
|
path={route.path} |
||||
|
exact={route.exact} |
||||
|
render={(props) => ( |
||||
|
<Layout> |
||||
|
{route.routes |
||||
|
? renderRoutes(route.routes) |
||||
|
: <Component {...props} />} |
||||
|
</Layout> |
||||
|
)} |
||||
|
/> |
||||
|
); |
||||
|
})} |
||||
|
</Switch> |
||||
|
</Suspense> |
||||
|
) : null); |
||||
|
|
||||
|
function Routes() { |
||||
|
return renderRoutes(routesConfig); |
||||
|
} |
||||
|
|
||||
|
export default Routes; |
@ -1,3 +0,0 @@ |
|||||
body { |
|
||||
margin: 1em !important; |
|
||||
} |
|
@ -1,4 +1,17 @@ |
|||||
body { |
body.app { |
||||
margin: 10em; |
overflow: auto; |
||||
padding: 0; |
margin: 1em !important; |
||||
|
} |
||||
|
|
||||
|
#root { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.i18next-newlines { |
||||
|
white-space: pre-line !important; |
||||
|
} |
||||
|
|
||||
|
.text-secondary { |
||||
|
color: gray; |
||||
|
font-style: italic; |
||||
} |
} |
||||
|
After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 112 KiB |
After Width: | Height: | Size: 10 KiB |
@ -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; |
@ -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 }) => ( |
|
||||
<Provider store={store}> |
|
||||
<LoadingContainer> |
|
||||
<Router> |
|
||||
<CoreLayoutContainer> |
|
||||
<Switch> |
|
||||
<Route exact path="/" component={HomeContainer} /> |
|
||||
<Route component={NotFound} /> |
|
||||
</Switch> |
|
||||
</CoreLayoutContainer> |
|
||||
</Router> |
|
||||
</LoadingContainer> |
|
||||
</Provider> |
|
||||
) |
|
||||
|
|
||||
App.propTypes = { |
|
||||
store: PropTypes.object.isRequired |
|
||||
} |
|
||||
|
|
||||
export default App |
|
@ -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 ( |
|
||||
<Context.Provider |
|
||||
value={{ |
|
||||
drizzle: this.props.drizzle, |
|
||||
drizzleState: this.state.drizzleState, |
|
||||
drizzleInitialized: this.state.drizzleInitialized, |
|
||||
breeze: this.props.breeze, |
|
||||
breezeState: this.state.breezeState, |
|
||||
breezeInitialized: this.state.breezeInitialized |
|
||||
}} |
|
||||
> |
|
||||
{this.props.children} |
|
||||
</Context.Provider> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default { |
|
||||
Context: Context, |
|
||||
Consumer: Context.Consumer, |
|
||||
Provider |
|
||||
}; |
|
@ -0,0 +1,81 @@ |
|||||
|
// Modified version of https://github.com/trufflesuite/drizzle/blob/develop/packages/react-plugin/src/DrizzleContext.js |
||||
|
import React from 'react'; |
||||
|
|
||||
|
const Context = React.createContext(); |
||||
|
|
||||
|
class Provider extends React.Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.state = { |
||||
|
drizzleState: null, |
||||
|
drizzleInitialized: false, |
||||
|
breezeState: null, |
||||
|
breezeInitialized: false, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
const { drizzle, breeze } = this.props; |
||||
|
// subscribe to changes in the store, keep state up-to-date |
||||
|
this.unsubscribe = drizzle.store.subscribe(() => { |
||||
|
const drizzleState = drizzle.store.getState(); |
||||
|
const breezeState = breeze.store.getState(); |
||||
|
|
||||
|
if (drizzleState.drizzleStatus.initialized) { |
||||
|
this.setState({ |
||||
|
drizzleState, |
||||
|
drizzleInitialized: true, |
||||
|
}); |
||||
|
} |
||||
|
if (breezeState.breezeStatus.initialized) { |
||||
|
this.setState({ |
||||
|
breezeState, |
||||
|
breezeInitialized: true, |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.unsubscribe = breeze.store.subscribe(() => { |
||||
|
const breezeState = breeze.store.getState(); |
||||
|
if (breezeState.breezeStatus.initialized) { |
||||
|
this.setState({ |
||||
|
breezeState, |
||||
|
breezeInitialized: true, |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
this.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
drizzleState, drizzleInitialized, breezeState, breezeInitialized, |
||||
|
} = this.state; |
||||
|
const { drizzle, breeze, children } = this.props; |
||||
|
|
||||
|
return ( |
||||
|
<Context.Provider |
||||
|
value={{ |
||||
|
drizzle, |
||||
|
drizzleState, |
||||
|
drizzleInitialized, |
||||
|
breeze, |
||||
|
breezeState, |
||||
|
breezeInitialized, |
||||
|
}} |
||||
|
> |
||||
|
{children} |
||||
|
</Context.Provider> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
Context, |
||||
|
Consumer: Context.Consumer, |
||||
|
Provider, |
||||
|
}; |
@ -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 ( |
||||
|
<> |
||||
|
<p> |
||||
|
{t('clear.databases.modal.clearing.progress.message')} |
||||
|
</p> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (user.hasSignedUp) { |
||||
|
return ( |
||||
|
<> |
||||
|
<p> |
||||
|
{t('clear.databases.modal.description.pre')} |
||||
|
</p> |
||||
|
<p> |
||||
|
{t('clear.databases.modal.description.body.user')} |
||||
|
</p> |
||||
|
<Form> |
||||
|
<Form.Field> |
||||
|
<label htmlFor="form-clear-databases-field-confirm"> |
||||
|
{t('clear.databases.modal.form.username.label.user')} |
||||
|
</label> |
||||
|
<Input |
||||
|
id="form-clear-databases-field-confirm" |
||||
|
name="confirmationInput" |
||||
|
value={confirmationInput} |
||||
|
onChange={handleInputChange} |
||||
|
/> |
||||
|
</Form.Field> |
||||
|
</Form> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<p> |
||||
|
{t('clear.databases.modal.description.pre')} |
||||
|
</p> |
||||
|
<Form> |
||||
|
<Form.Field> |
||||
|
<label htmlFor="form-clear-databases-field-confirm"> |
||||
|
{t('clear.databases.modal.form.username.label.guest')} |
||||
|
</label> |
||||
|
<Input |
||||
|
id="form-clear-databases-field-confirm" |
||||
|
name="confirmationInput" |
||||
|
value={confirmationInput} |
||||
|
onChange={handleInputChange} |
||||
|
/> |
||||
|
</Form.Field> |
||||
|
</Form> |
||||
|
</> |
||||
|
); |
||||
|
}, [confirmationInput, isClearing, t, user.hasSignedUp]); |
||||
|
|
||||
|
return useMemo(() => ( |
||||
|
<Modal |
||||
|
onClose={onCancelTry} |
||||
|
open={open} |
||||
|
size="small" |
||||
|
> |
||||
|
<Modal.Header> |
||||
|
{isClearing |
||||
|
? t('clear.databases.modal.clearing.progress.title') |
||||
|
: t('clear.databases.modal.title')} |
||||
|
</Modal.Header> |
||||
|
<Modal.Content> |
||||
|
<Modal.Description> |
||||
|
{modalContent} |
||||
|
</Modal.Description> |
||||
|
</Modal.Content> |
||||
|
|
||||
|
{!isClearing && ( |
||||
|
<Modal.Actions> |
||||
|
<Button color="black" basic onClick={onCancelTry} disabled={isClearing}> |
||||
|
{t('clear.databases.modal.cancel.button')} |
||||
|
</Button> |
||||
|
<Button onClick={handleSubmit} negative disabled={!userConfirmed}> |
||||
|
{t('clear.databases.modal.clear.button')} |
||||
|
</Button> |
||||
|
</Modal.Actions> |
||||
|
)} |
||||
|
</Modal> |
||||
|
), [handleSubmit, isClearing, modalContent, onCancelTry, open, t, userConfirmed]); |
||||
|
}; |
||||
|
|
||||
|
ClearDatabasesModal.defaultProps = { |
||||
|
open: false, |
||||
|
}; |
||||
|
|
||||
|
ClearDatabasesModal.propTypes = { |
||||
|
open: PropTypes.bool, |
||||
|
onDatabasesCleared: PropTypes.func.isRequired, |
||||
|
onCancel: PropTypes.func.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default ClearDatabasesModal; |
@ -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 ( |
|
||||
<div> |
|
||||
<MenuComponent/> |
|
||||
{this.props.children} |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
CoreLayout.propTypes = { |
|
||||
children: PropTypes.element.isRequired |
|
||||
}; |
|
@ -0,0 +1,46 @@ |
|||||
|
import React, { useMemo } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { |
||||
|
Dimmer, Loader, Placeholder, Tab, |
||||
|
} from 'semantic-ui-react'; |
||||
|
import { useTranslation } from 'react-i18next'; |
||||
|
|
||||
|
const CustomLoadingTabPane = (props) => { |
||||
|
const { loading, loadingMessage, children } = props; |
||||
|
const { t } = useTranslation(); |
||||
|
|
||||
|
return useMemo(() => { |
||||
|
if (loading) { |
||||
|
return ( |
||||
|
<Tab.Pane> |
||||
|
<Dimmer active inverted> |
||||
|
<Loader inverted> |
||||
|
{loadingMessage !== undefined |
||||
|
? loadingMessage |
||||
|
: t('custom.loading.tab.pane.default.generic.message')} |
||||
|
</Loader> |
||||
|
</Dimmer> |
||||
|
<Placeholder fluid> |
||||
|
<Placeholder.Line length="very long" /> |
||||
|
<Placeholder.Line length="medium" /> |
||||
|
<Placeholder.Line length="long" /> |
||||
|
</Placeholder> |
||||
|
</Tab.Pane> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Tab.Pane> |
||||
|
{children} |
||||
|
</Tab.Pane> |
||||
|
); |
||||
|
}, [children, loading, loadingMessage, t]); |
||||
|
}; |
||||
|
|
||||
|
CustomLoadingTabPane.propTypes = { |
||||
|
loading: PropTypes.bool, |
||||
|
loadingMessage: PropTypes.string, |
||||
|
children: PropTypes.element.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default CustomLoadingTabPane; |
@ -1,9 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
|
|
||||
class HomeContainer extends Component { |
|
||||
render() { |
|
||||
return(<p>TODO: Home Container</p>); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default HomeContainer; |
|
@ -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) => <li>{listItem}</li>); |
||||
|
} |
||||
|
|
||||
|
const list = messageList ? <ul>{listItems}</ul> : ''; |
||||
|
|
||||
|
return ( |
||||
|
<main className="loading-screen"> |
||||
|
<Container> |
||||
|
<img src={imageSrc} alt={imageAlt} className="loading-img" /> |
||||
|
<p><strong>{title}</strong></p> |
||||
|
<p>{message}</p> |
||||
|
{list} |
||||
|
</Container> |
||||
|
<Progress percent={progress} size="small" indicating={indicating} error={error} /> |
||||
|
</main> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
LoadingComponent.propTypes = { |
||||
|
title: PropTypes.string.isRequired, |
||||
|
message: PropTypes.string.isRequired, |
||||
|
messageList: PropTypes.arrayOf(PropTypes.string), |
||||
|
imageType: PropTypes.string.isRequired, |
||||
|
progress: PropTypes.number.isRequired, |
||||
|
progressType: PropTypes.string.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default LoadingComponent; |
@ -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 ( |
||||
|
<CustomLoader |
||||
|
title="Couldn't detect MetaMask!" |
||||
|
message={['Please make sure to install ', <a href="https://metamask.io/">MetaMask</a>, ' first.']} |
||||
|
imageType="metamask" |
||||
|
progress={10} |
||||
|
progressType="error" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if ((web3Status === 'initializing' || !web3NetworkId) && !web3NetworkFailed) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="Connecting to the Ethereum network..." |
||||
|
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account." |
||||
|
imageType="ethereum" |
||||
|
progress={20} |
||||
|
progressType="indicating" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (web3Status === 'failed' || web3NetworkFailed) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="No connection to the Ethereum network!" |
||||
|
message="Please make sure that:" |
||||
|
messageList={['MetaMask is unlocked and pointed to the correct, available network', |
||||
|
'The app has been granted the right to connect to your account']} |
||||
|
imageType="ethereum" |
||||
|
progress={20} |
||||
|
progressType="error" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (web3Status === 'initialized' && web3AccountsFailed) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="We can't find any Ethereum accounts!" |
||||
|
message="Please make sure that MetaMask is unlocked." |
||||
|
imageType="ethereum" |
||||
|
progress={20} |
||||
|
progressType="error" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (initializing || (!failed && !contractInitialized && contractDeployed)) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="Initializing contracts..." |
||||
|
message="" |
||||
|
imageType="ethereum" |
||||
|
progress={40} |
||||
|
progressType="indicating" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (!contractDeployed) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="No contracts found on the current network!" |
||||
|
message="Please make sure that you are connected to the correct network and the contracts are deployed." |
||||
|
imageType="ethereum" |
||||
|
progress={40} |
||||
|
progressType="error" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="Initializing IPFS..." |
||||
|
message="" |
||||
|
imageType="ipfs" |
||||
|
progress={60} |
||||
|
progressType="indicating" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (ipfsStatus === breezeConstants.STATUS_FAILED) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="IPFS initialization failed!" |
||||
|
message="" |
||||
|
imageType="ipfs" |
||||
|
progress={60} |
||||
|
progressType="error" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (orbitStatus === breezeConstants.STATUS_INITIALIZING) { |
||||
|
const message = process.env.NODE_ENV === 'development' |
||||
|
? 'If needed, please sign the transaction in MetaMask to create the databases.' |
||||
|
: 'Please sign the transaction in MetaMask to create the databases.'; |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="Preparing OrbitDB..." |
||||
|
message={message} |
||||
|
imageType="orbit" |
||||
|
progress={80} |
||||
|
progressType="indicating" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (orbitStatus === breezeConstants.STATUS_FAILED) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="OrbitDB initialization failed!" |
||||
|
message="" |
||||
|
imageType="orbit" |
||||
|
progress={80} |
||||
|
progressType="error" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (!userFetched) { |
||||
|
return ( |
||||
|
<CustomLoader |
||||
|
title="Loading dapp..." |
||||
|
message="" |
||||
|
imageType="app" |
||||
|
progress={90} |
||||
|
progressType="indicating" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return Children.only(children); |
||||
|
}; |
||||
|
|
||||
|
export default InitializationLoader; |
@ -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) => |
|
||||
<li>{listItem}</li> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
const list = message_list ? <ul>{listItems}</ul> : ''; |
|
||||
|
|
||||
return( |
|
||||
<main className="loading-screen"> |
|
||||
<Container> |
|
||||
<img src={imageSrc} alt={imageAlt} className="loading-img" /> |
|
||||
<p><strong>{this.props.title}</strong></p> |
|
||||
<p>{this.props.message}</p> |
|
||||
{list} |
|
||||
</Container> |
|
||||
<Progress percent={this.props.progress} size='small' indicating={indicating} error={error}/> |
|
||||
</main> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
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; |
|
@ -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 <LoadingComponent |
|
||||
title="Connecting to the Ethereum network..." |
|
||||
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account." |
|
||||
image_type="ethereum" |
|
||||
progress={20} |
|
||||
progress_type="indicating" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
if (this.props.web3.status === 'failed' || this.props.web3.networkFailed) { |
|
||||
return <LoadingComponent |
|
||||
title="No connection to the Ethereum network!" |
|
||||
message="Please make sure that:" |
|
||||
message_list={['MetaMask is unlocked and pointed to the correct, available network', |
|
||||
'The app has been granted the right to connect to your account']} |
|
||||
image_type="ethereum" |
|
||||
progress={20} |
|
||||
progress_type="error" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
if (this.props.web3.status === 'initialized' && this.props.web3.accountsFailed) { |
|
||||
return <LoadingComponent |
|
||||
title="We can't find any Ethereum accounts!" |
|
||||
message="Please make sure that MetaMask is unlocked." |
|
||||
image_type="ethereum" |
|
||||
progress={20} |
|
||||
progress_type="error" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
if (this.props.drizzleStatus.initializing |
|
||||
|| (!this.props.drizzleStatus.failed && !this.props.contractInitialized && this.props.contractDeployed )){ |
|
||||
return <LoadingComponent |
|
||||
title="Initializing contracts..." |
|
||||
message="" |
|
||||
image_type="ethereum" |
|
||||
progress={40} |
|
||||
progress_type="indicating" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
if (!this.props.contractDeployed) { |
|
||||
return <LoadingComponent |
|
||||
title="No contracts found on the current network!" |
|
||||
message="Please make sure that you are connected to the correct network and the contracts are deployed." |
|
||||
image_type="ethereum" |
|
||||
progress={40} |
|
||||
progress_type="error" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
if (this.props.ipfsStatus === breezeConstants.STATUS_INITIALIZING) { |
|
||||
return <LoadingComponent |
|
||||
title="Initializing IPFS..." |
|
||||
message="" |
|
||||
image_type="ipfs" |
|
||||
progress={60} |
|
||||
progress_type="indicating" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
if (this.props.ipfsStatus === breezeConstants.STATUS_FAILED) { |
|
||||
return <LoadingComponent |
|
||||
title="IPFS initialization failed!" |
|
||||
message="" |
|
||||
image_type="ipfs" |
|
||||
progress={60} |
|
||||
progress_type="error" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
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 <LoadingComponent |
|
||||
title="Preparing OrbitDB..." |
|
||||
message={message} |
|
||||
image_type="orbit" |
|
||||
progress={80} |
|
||||
progress_type="indicating" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
if (this.props.orbitStatus === breezeConstants.STATUS_FAILED) { |
|
||||
return <LoadingComponent |
|
||||
title="OrbitDB initialization failed!" |
|
||||
message="" |
|
||||
image_type="orbit" |
|
||||
progress={80} |
|
||||
progress_type="error" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
if (!this.props.userFetched){ |
|
||||
return <LoadingComponent |
|
||||
title="Loading dapp..." |
|
||||
message="" |
|
||||
image_type="app" |
|
||||
progress={90} |
|
||||
progress_type="indicating" |
|
||||
/> |
|
||||
} |
|
||||
|
|
||||
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); |
|
@ -0,0 +1,9 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
const LoadingScreen = () => ( |
||||
|
<div> |
||||
|
Loading |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
export default LoadingScreen; |
@ -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 ( |
|
||||
<AppContext.Consumer> |
|
||||
{context => { |
|
||||
return( |
|
||||
<div> |
|
||||
<Menu color='black' inverted> |
|
||||
<Menu.Item |
|
||||
link |
|
||||
name='home' |
|
||||
onClick={() => { this.props.history.push("/"); }} |
|
||||
> |
|
||||
<img src={app_logo} alt="app_logo"/> |
|
||||
</Menu.Item> |
|
||||
|
|
||||
<SignUpForm/> |
|
||||
|
|
||||
</Menu> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
</AppContext.Consumer> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default withRouter(MenuComponent); |
|
@ -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 ( |
||||
|
<Feed> |
||||
|
<Feed.Event> |
||||
|
<Feed.Label className="post-profile-picture"> |
||||
|
{userProfilePictureUrl |
||||
|
? ( |
||||
|
<Image |
||||
|
avatar |
||||
|
src={userProfilePictureUrl} |
||||
|
/> |
||||
|
) |
||||
|
: ( |
||||
|
<Icon |
||||
|
name="user circle" |
||||
|
size="big" |
||||
|
inverted |
||||
|
color="black" |
||||
|
/> |
||||
|
)} |
||||
|
</Feed.Label> |
||||
|
<Feed.Content> |
||||
|
<Feed.Summary> |
||||
|
<Form> |
||||
|
<TextArea |
||||
|
placeholder={t('post.form.content.field.placeholder')} |
||||
|
name="postContent" |
||||
|
size="mini" |
||||
|
rows={4} |
||||
|
value={postContent} |
||||
|
onChange={handleInputChange} |
||||
|
/> |
||||
|
</Form> |
||||
|
</Feed.Summary> |
||||
|
<Feed.Meta> |
||||
|
<Feed.Like> |
||||
|
<Form.Button |
||||
|
animated |
||||
|
type="button" |
||||
|
color="green" |
||||
|
disabled={posting || postContent === ''} |
||||
|
onClick={savePost} |
||||
|
> |
||||
|
<Button.Content visible> |
||||
|
{t('post.create.form.send.button')} |
||||
|
</Button.Content> |
||||
|
<Button.Content hidden> |
||||
|
<Icon name="send" /> |
||||
|
</Button.Content> |
||||
|
</Form.Button> |
||||
|
</Feed.Like> |
||||
|
</Feed.Meta> |
||||
|
</Feed.Content> |
||||
|
</Feed.Event> |
||||
|
</Feed> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
PostCreate.propTypes = { |
||||
|
topicId: PropTypes.number.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default memo(PostCreate); |
@ -0,0 +1,13 @@ |
|||||
|
.post-profile-picture { |
||||
|
margin: 5px 0 0 0; |
||||
|
} |
||||
|
|
||||
|
.post-summary-meta-index { |
||||
|
float: right; |
||||
|
font-size: 12px; |
||||
|
opacity: 0.4; |
||||
|
} |
||||
|
|
||||
|
.like:hover .icon { |
||||
|
color: #fff !important; |
||||
|
} |
@ -0,0 +1,165 @@ |
|||||
|
import React, { |
||||
|
memo, useEffect, useMemo, useState, |
||||
|
} from 'react'; |
||||
|
import { |
||||
|
Dimmer, Icon, Image, Feed, Placeholder, |
||||
|
} from 'semantic-ui-react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { useTranslation } from 'react-i18next'; |
||||
|
import TimeAgo from 'react-timeago'; |
||||
|
import { useDispatch, useSelector } from 'react-redux'; |
||||
|
import { Link } from 'react-router-dom'; |
||||
|
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; |
||||
|
import { breeze } from '../../../redux/store'; |
||||
|
import './styles.css'; |
||||
|
import { POSTS_DATABASE, USER_DATABASE } from '../../../constants/orbit/OrbitDatabases'; |
||||
|
import determineKVAddress from '../../../utils/orbitUtils'; |
||||
|
import { USER_PROFILE_PICTURE } from '../../../constants/orbit/UserDatabaseKeys'; |
||||
|
import { POST_CONTENT } from '../../../constants/orbit/PostsDatabaseKeys'; |
||||
|
import { FORUM_CONTRACT } from '../../../constants/contracts/ContractNames'; |
||||
|
|
||||
|
const { orbit } = breeze; |
||||
|
|
||||
|
const PostListRow = (props) => { |
||||
|
const { |
||||
|
id: postId, postIndexInTopic, postCallHash, loading, |
||||
|
} = props; |
||||
|
const getPostResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getPost); |
||||
|
const [postAuthorAddress, setPostAuthorAddress] = useState(null); |
||||
|
const [postAuthor, setPostAuthor] = useState(null); |
||||
|
const [timeAgo, setTimeAgo] = useState(null); |
||||
|
const [postContent, setPostContent] = useState(null); |
||||
|
const [postAuthorMeta, setPostAuthorMeta] = useState(null); |
||||
|
const userAddress = useSelector((state) => state.user.address); |
||||
|
const posts = useSelector((state) => state.orbitData.posts); |
||||
|
const users = useSelector((state) => state.orbitData.users); |
||||
|
const dispatch = useDispatch(); |
||||
|
const { t } = useTranslation(); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!loading && postCallHash && getPostResults[postCallHash] !== undefined) { |
||||
|
setPostAuthorAddress(getPostResults[postCallHash].value[0]); |
||||
|
setPostAuthor(getPostResults[postCallHash].value[1]); |
||||
|
setTimeAgo(getPostResults[postCallHash].value[2] * 1000); |
||||
|
} |
||||
|
}, [getPostResults, loading, postCallHash]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (postAuthorAddress && userAddress !== postAuthorAddress) { |
||||
|
dispatch({ |
||||
|
type: FETCH_USER_DATABASE, |
||||
|
orbit, |
||||
|
dbName: POSTS_DATABASE, |
||||
|
userAddress: postAuthorAddress, |
||||
|
}); |
||||
|
|
||||
|
dispatch({ |
||||
|
type: FETCH_USER_DATABASE, |
||||
|
orbit, |
||||
|
dbName: USER_DATABASE, |
||||
|
userAddress: postAuthorAddress, |
||||
|
}); |
||||
|
} |
||||
|
}, [dispatch, postAuthorAddress, userAddress]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const postFound = posts |
||||
|
.find((post) => post.id === postId); |
||||
|
|
||||
|
if (postFound) { |
||||
|
setPostContent(postFound[POST_CONTENT]); |
||||
|
} |
||||
|
}, [postId, posts]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (postAuthorAddress !== null) { |
||||
|
determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress: postAuthorAddress }) |
||||
|
.then((userOrbitAddress) => { |
||||
|
const userFound = users |
||||
|
.find((user) => user.id === userOrbitAddress); |
||||
|
|
||||
|
if (userFound) { |
||||
|
setPostAuthorMeta(userFound); |
||||
|
} |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.error('Error during determination of key-value DB address:', error); |
||||
|
}); |
||||
|
} |
||||
|
}, [postAuthorAddress, users]); |
||||
|
|
||||
|
const authorAvatar = useMemo(() => (postAuthorMeta !== null && postAuthorMeta[USER_PROFILE_PICTURE] |
||||
|
? ( |
||||
|
<Image |
||||
|
avatar |
||||
|
src={postAuthorMeta[USER_PROFILE_PICTURE]} |
||||
|
/> |
||||
|
) |
||||
|
: ( |
||||
|
<Icon |
||||
|
name="user circle" |
||||
|
size="big" |
||||
|
inverted |
||||
|
color="black" |
||||
|
/> |
||||
|
)), [postAuthorMeta]); |
||||
|
|
||||
|
const authorAvatarLink = useMemo(() => { |
||||
|
if (postAuthorAddress) { |
||||
|
return ( |
||||
|
<Link to={`/users/${postAuthorAddress}`}> |
||||
|
{authorAvatar} |
||||
|
</Link> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return authorAvatar; |
||||
|
}, [authorAvatar, postAuthorAddress]); |
||||
|
|
||||
|
return useMemo(() => ( |
||||
|
<Dimmer.Dimmable as={Feed.Event} blurring dimmed={loading}> |
||||
|
<Feed.Label className="post-profile-picture"> |
||||
|
{authorAvatarLink} |
||||
|
</Feed.Label> |
||||
|
<Feed.Content> |
||||
|
<Feed.Summary> |
||||
|
<div> |
||||
|
<span className="post-summary-meta-index"> |
||||
|
{t('post.list.row.post.id', { id: postIndexInTopic })} |
||||
|
</span> |
||||
|
</div> |
||||
|
{postAuthor !== null && setPostAuthorAddress !== null && timeAgo !== null |
||||
|
? ( |
||||
|
<> |
||||
|
<Feed.User as={Link} to={`/users/${postAuthorAddress}`}>{postAuthor}</Feed.User> |
||||
|
<Feed.Date className="post-summary-meta-date"> |
||||
|
<TimeAgo date={timeAgo} /> |
||||
|
</Feed.Date> |
||||
|
</> |
||||
|
) |
||||
|
: <Placeholder><Placeholder.Line length="medium" /></Placeholder>} |
||||
|
</Feed.Summary> |
||||
|
<Feed.Extra> |
||||
|
{postContent !== null |
||||
|
? postContent |
||||
|
: <Placeholder><Placeholder.Line length="long" /></Placeholder>} |
||||
|
</Feed.Extra> |
||||
|
</Feed.Content> |
||||
|
</Dimmer.Dimmable> |
||||
|
), [ |
||||
|
authorAvatarLink, loading, postAuthor, postAuthorAddress, postContent, postIndexInTopic, t, timeAgo, |
||||
|
]); |
||||
|
}; |
||||
|
|
||||
|
PostListRow.defaultProps = { |
||||
|
loading: false, |
||||
|
}; |
||||
|
|
||||
|
PostListRow.propTypes = { |
||||
|
id: PropTypes.number.isRequired, |
||||
|
postIndexInTopic: PropTypes.number.isRequired, |
||||
|
postCallHash: PropTypes.string, |
||||
|
loading: PropTypes.bool, |
||||
|
}; |
||||
|
|
||||
|
export default memo(PostListRow); |
@ -0,0 +1,9 @@ |
|||||
|
.post-profile-picture { |
||||
|
margin: 5px 0 0 0; |
||||
|
} |
||||
|
|
||||
|
.post-summary-meta-index { |
||||
|
float: right; |
||||
|
font-size: 12px; |
||||
|
opacity: 0.4; |
||||
|
} |
@ -0,0 +1,72 @@ |
|||||
|
import React, { |
||||
|
useEffect, useMemo, useState, |
||||
|
} from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { useSelector } from 'react-redux'; |
||||
|
import { Dimmer, Feed, Loader } from 'semantic-ui-react'; |
||||
|
import PostListRow from './PostListRow'; |
||||
|
import { drizzle } from '../../redux/store'; |
||||
|
import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames'; |
||||
|
|
||||
|
const { contracts: { [FORUM_CONTRACT]: { methods: { getPost: { cacheCall: getPostChainData } } } } } = drizzle; |
||||
|
|
||||
|
const PostList = (props) => { |
||||
|
const { postIds, loading } = props; |
||||
|
const [getPostCallHashes, setGetPostCallHashes] = useState([]); |
||||
|
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized); |
||||
|
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (drizzleInitialized && !drizzleInitializationFailed && !loading) { |
||||
|
const newPostsFound = postIds |
||||
|
.filter((postId) => !getPostCallHashes |
||||
|
.map((getPostCallHash) => getPostCallHash.id) |
||||
|
.includes(postId)); |
||||
|
|
||||
|
if (newPostsFound.length > 0) { |
||||
|
setGetPostCallHashes([ |
||||
|
...getPostCallHashes, |
||||
|
...newPostsFound |
||||
|
.map((postId) => ({ |
||||
|
id: postId, |
||||
|
hash: getPostChainData(postId), |
||||
|
})), |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
|
}, [drizzleInitializationFailed, drizzleInitialized, getPostCallHashes, loading, postIds]); |
||||
|
|
||||
|
const posts = useMemo(() => { |
||||
|
if (loading) { |
||||
|
return null; |
||||
|
} |
||||
|
return postIds |
||||
|
.map((postId, index) => { |
||||
|
const postHash = getPostCallHashes.find((getPostCallHash) => getPostCallHash.id === postId); |
||||
|
|
||||
|
return ( |
||||
|
<PostListRow |
||||
|
id={postId} |
||||
|
postIndexInTopic={index + 1} |
||||
|
key={postId} |
||||
|
postCallHash={postHash && postHash.hash} |
||||
|
loading={postHash === undefined} |
||||
|
/> |
||||
|
); |
||||
|
}); |
||||
|
}, [getPostCallHashes, loading, postIds]); |
||||
|
|
||||
|
return ( |
||||
|
<Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large"> |
||||
|
<Loader active={loading} /> |
||||
|
{posts} |
||||
|
</Dimmer.Dimmable> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
PostList.propTypes = { |
||||
|
postIds: PropTypes.arrayOf(PropTypes.number).isRequired, |
||||
|
loading: PropTypes.bool, |
||||
|
}; |
||||
|
|
||||
|
export default PostList; |
@ -0,0 +1,3 @@ |
|||||
|
#post-list{ |
||||
|
height: 100%; |
||||
|
} |
@ -1,139 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { Button, Form, Menu, Message, Modal } from 'semantic-ui-react'; |
|
||||
|
|
||||
import AppContext from "./AppContext"; |
|
||||
import { connect } from 'react-redux'; |
|
||||
|
|
||||
const contractName = 'Forum'; |
|
||||
const checkUsernameTakenMethod = 'isUserNameTaken'; |
|
||||
const signUpMethod = 'signUp'; |
|
||||
|
|
||||
class SignUpForm extends Component { |
|
||||
constructor(props, context) { |
|
||||
super(props, context); |
|
||||
|
|
||||
// For quick access |
|
||||
this.contract = this.context.drizzle.contracts[contractName]; |
|
||||
|
|
||||
this.handleInputChange = this.handleInputChange.bind(this); |
|
||||
this.handleSubmit = this.handleSubmit.bind(this); |
|
||||
this.completeAction = this.completeAction.bind(this); |
|
||||
|
|
||||
this.checkedUsernames = []; |
|
||||
|
|
||||
this.state = { |
|
||||
usernameInput: '', |
|
||||
error: false, |
|
||||
errorHeader: '', |
|
||||
errorMessage: '', |
|
||||
signingUp: false, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
handleInputChange(e, { name, value }) { |
|
||||
this.setState({ |
|
||||
[name]: value, |
|
||||
error: false, |
|
||||
}); |
|
||||
if (value !== '') { |
|
||||
if (this.checkedUsernames.length > 0) { |
|
||||
if (this.checkedUsernames.some((e) => e.usernameChecked === value)) { |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
this.contract.methods[checkUsernameTakenMethod].cacheCall( |
|
||||
value, |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
handleSubmit() { |
|
||||
const { usernameInput, error } = this.state; |
|
||||
|
|
||||
if (usernameInput === '') { |
|
||||
this.setState({ |
|
||||
error: true, |
|
||||
errorHeader: 'Data Incomplete', |
|
||||
errorMessage: 'You need to provide a username', |
|
||||
}); |
|
||||
} else if (!error) { |
|
||||
// TODO |
|
||||
// // Makes sure current input username has been checked for availability |
|
||||
// if (this.checkedUsernames.some((e) => e.usernameChecked === usernameInput)) { |
|
||||
// this.completeAction(); |
|
||||
// } |
|
||||
this.completeAction(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate() { |
|
||||
// TODO |
|
||||
} |
|
||||
|
|
||||
completeAction() { |
|
||||
const { usernameInput } = this.state; |
|
||||
const { user, account } = this.props; |
|
||||
|
|
||||
if (user.hasSignedUp) { |
|
||||
console.log('Signing up..') |
|
||||
this.contract.methods['signUp'].cacheSend(usernameInput); |
|
||||
} else { |
|
||||
this.setState({ |
|
||||
signingUp: true, |
|
||||
}); |
|
||||
this.contract.methods[signUpMethod].cacheSend( |
|
||||
...[usernameInput], { from: account }, |
|
||||
); |
|
||||
} |
|
||||
this.setState({ |
|
||||
usernameInput: '', |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { |
|
||||
error, usernameInput, errorHeader, errorMessage, signingUp, |
|
||||
} = this.state; |
|
||||
|
|
||||
return( |
|
||||
<Modal as={Form} onSubmit={e => this.handleSubmit(e)} trigger={ |
|
||||
<Menu.Item |
|
||||
name='signup' |
|
||||
position='right' |
|
||||
content='Sign Up' |
|
||||
/> |
|
||||
}> |
|
||||
<Modal.Header>Sign Up</Modal.Header> |
|
||||
<Modal.Content> |
|
||||
|
|
||||
<Form.Field required> |
|
||||
<label>Username</label> |
|
||||
<Form.Input |
|
||||
placeholder='Username' |
|
||||
name="usernameInput" |
|
||||
value={usernameInput} |
|
||||
onChange={this.handleInputChange} |
|
||||
/> |
|
||||
</Form.Field> |
|
||||
<Message |
|
||||
error |
|
||||
header={errorHeader} |
|
||||
content={errorMessage} |
|
||||
/> |
|
||||
<Button type="submit" color="black" content="Sign Up" /> |
|
||||
|
|
||||
</Modal.Content> |
|
||||
</Modal> |
|
||||
) |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
SignUpForm.contextType = AppContext.Context; |
|
||||
|
|
||||
const mapStateToProps = (state) => ({ |
|
||||
user: state.user |
|
||||
}); |
|
||||
|
|
||||
export default connect(mapStateToProps)(SignUpForm); |
|
@ -0,0 +1,190 @@ |
|||||
|
import React, { |
||||
|
memo, useEffect, useMemo, useState, |
||||
|
} from 'react'; |
||||
|
import { |
||||
|
Dimmer, Grid, Image, List, Placeholder, |
||||
|
} from 'semantic-ui-react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { useTranslation } from 'react-i18next'; |
||||
|
import TimeAgo from 'react-timeago'; |
||||
|
import { useHistory } from 'react-router'; |
||||
|
import { useDispatch, useSelector } from 'react-redux'; |
||||
|
import { Link } from 'react-router-dom'; |
||||
|
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; |
||||
|
import { breeze } from '../../../redux/store'; |
||||
|
import './styles.css'; |
||||
|
import { TOPICS_DATABASE, USER_DATABASE } from '../../../constants/orbit/OrbitDatabases'; |
||||
|
import determineKVAddress from '../../../utils/orbitUtils'; |
||||
|
import { USER_PROFILE_PICTURE } from '../../../constants/orbit/UserDatabaseKeys'; |
||||
|
import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys'; |
||||
|
import { FORUM_CONTRACT } from '../../../constants/contracts/ContractNames'; |
||||
|
|
||||
|
const { orbit } = breeze; |
||||
|
|
||||
|
const TopicListRow = (props) => { |
||||
|
const { id: topicId, topicCallHash, loading } = props; |
||||
|
const getTopicResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getTopic); |
||||
|
const [numberOfReplies, setNumberOfReplies] = useState(null); |
||||
|
const [topicAuthorAddress, setTopicAuthorAddress] = useState(null); |
||||
|
const [topicAuthor, setTopicAuthor] = useState(null); |
||||
|
const [timeAgo, setTimeAgo] = useState(null); |
||||
|
const [topicSubject, setTopicSubject] = useState(null); |
||||
|
const [topicAuthorMeta, setTopicAuthorMeta] = useState(null); |
||||
|
const userAddress = useSelector((state) => state.user.address); |
||||
|
const topics = useSelector((state) => state.orbitData.topics); |
||||
|
const users = useSelector((state) => state.orbitData.users); |
||||
|
const dispatch = useDispatch(); |
||||
|
const history = useHistory(); |
||||
|
const { t } = useTranslation(); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!loading && topicCallHash && getTopicResults[topicCallHash] !== undefined) { |
||||
|
setTopicAuthorAddress(getTopicResults[topicCallHash].value[0]); |
||||
|
setTopicAuthor(getTopicResults[topicCallHash].value[1]); |
||||
|
setTimeAgo(getTopicResults[topicCallHash].value[2] * 1000); |
||||
|
setNumberOfReplies(getTopicResults[topicCallHash].value[3].length); |
||||
|
} |
||||
|
}, [getTopicResults, loading, topicCallHash]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (topicAuthorAddress && userAddress !== topicAuthorAddress) { |
||||
|
dispatch({ |
||||
|
type: FETCH_USER_DATABASE, |
||||
|
orbit, |
||||
|
dbName: TOPICS_DATABASE, |
||||
|
userAddress: topicAuthorAddress, |
||||
|
}); |
||||
|
|
||||
|
dispatch({ |
||||
|
type: FETCH_USER_DATABASE, |
||||
|
orbit, |
||||
|
dbName: USER_DATABASE, |
||||
|
userAddress: topicAuthorAddress, |
||||
|
}); |
||||
|
} |
||||
|
}, [dispatch, topicAuthorAddress, userAddress]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const topicFound = topics |
||||
|
.find((topic) => topic.id === topicId); |
||||
|
|
||||
|
if (topicFound) { |
||||
|
setTopicSubject(topicFound[TOPIC_SUBJECT]); |
||||
|
} |
||||
|
}, [topicId, topics]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (topicAuthorAddress !== null) { |
||||
|
determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress: topicAuthorAddress }) |
||||
|
.then((userOrbitAddress) => { |
||||
|
const userFound = users |
||||
|
.find((user) => user.id === userOrbitAddress); |
||||
|
|
||||
|
if (userFound) { |
||||
|
setTopicAuthorMeta(userFound); |
||||
|
} |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.error('Error during determination of key-value DB address:', error); |
||||
|
}); |
||||
|
} |
||||
|
}, [topicAuthorAddress, users]); |
||||
|
|
||||
|
const stopClickPropagation = (event) => { |
||||
|
event.stopPropagation(); |
||||
|
}; |
||||
|
|
||||
|
const authorAvatar = useMemo(() => (topicAuthorMeta !== null && topicAuthorMeta[USER_PROFILE_PICTURE] |
||||
|
? ( |
||||
|
<Image |
||||
|
className="profile-picture" |
||||
|
avatar |
||||
|
src={topicAuthorMeta[USER_PROFILE_PICTURE]} |
||||
|
/> |
||||
|
) |
||||
|
: ( |
||||
|
<List.Icon |
||||
|
name="user circle" |
||||
|
size="big" |
||||
|
inverted |
||||
|
color="black" |
||||
|
verticalAlign="middle" |
||||
|
/> |
||||
|
)), [topicAuthorMeta]); |
||||
|
|
||||
|
const authorAvatarLink = useMemo(() => { |
||||
|
if (topicAuthorAddress) { |
||||
|
return ( |
||||
|
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}> |
||||
|
{authorAvatar} |
||||
|
</Link> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return authorAvatar; |
||||
|
}, [authorAvatar, topicAuthorAddress]); |
||||
|
|
||||
|
return useMemo(() => { |
||||
|
const handleTopicClick = () => { |
||||
|
history.push(`/topics/${topicId}`); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<Dimmer.Dimmable as={List.Item} onClick={handleTopicClick} blurring dimmed={loading} className="list-item"> |
||||
|
{authorAvatarLink} |
||||
|
<List.Content className="list-content"> |
||||
|
<List.Header> |
||||
|
<Grid> |
||||
|
<Grid.Column floated="left" width={14}> |
||||
|
{topicSubject !== null |
||||
|
? topicSubject |
||||
|
: <Placeholder><Placeholder.Line length="very long" /></Placeholder>} |
||||
|
</Grid.Column> |
||||
|
<Grid.Column floated="right" width={2} textAlign="right"> |
||||
|
<span className="topic-metadata"> |
||||
|
{t('topic.list.row.topic.id', { id: topicId })} |
||||
|
</span> |
||||
|
</Grid.Column> |
||||
|
</Grid> |
||||
|
</List.Header> |
||||
|
<List.Description> |
||||
|
<Grid verticalAlign="middle"> |
||||
|
<Grid.Column floated="left" width={14}> |
||||
|
{topicAuthor !== null && timeAgo !== null |
||||
|
? ( |
||||
|
<div> |
||||
|
{t('topic.list.row.author', { author: topicAuthor })} |
||||
|
, |
||||
|
<TimeAgo date={timeAgo} /> |
||||
|
</div> |
||||
|
) |
||||
|
: <Placeholder><Placeholder.Line length="long" /></Placeholder>} |
||||
|
</Grid.Column> |
||||
|
<Grid.Column floated="right" width={2} textAlign="right"> |
||||
|
{numberOfReplies !== null |
||||
|
? ( |
||||
|
<span className="topic-metadata"> |
||||
|
{t('topic.list.row.number.of.replies', { numberOfReplies })} |
||||
|
</span> |
||||
|
) |
||||
|
: <Placeholder fluid><Placeholder.Line /></Placeholder>} |
||||
|
</Grid.Column> |
||||
|
</Grid> |
||||
|
</List.Description> |
||||
|
</List.Content> |
||||
|
</Dimmer.Dimmable> |
||||
|
); |
||||
|
}, [authorAvatarLink, history, loading, numberOfReplies, t, timeAgo, topicAuthor, topicId, topicSubject]); |
||||
|
}; |
||||
|
|
||||
|
TopicListRow.defaultProps = { |
||||
|
loading: false, |
||||
|
}; |
||||
|
|
||||
|
TopicListRow.propTypes = { |
||||
|
id: PropTypes.number.isRequired, |
||||
|
topicCallHash: PropTypes.string, |
||||
|
loading: PropTypes.bool, |
||||
|
}; |
||||
|
|
||||
|
export default memo(TopicListRow); |
@ -0,0 +1,21 @@ |
|||||
|
.topic-metadata { |
||||
|
font-size: 12px !important; |
||||
|
font-weight: initial; |
||||
|
} |
||||
|
|
||||
|
.list-item { |
||||
|
display: flex !important; |
||||
|
text-align: start; |
||||
|
} |
||||
|
|
||||
|
.profile-picture { |
||||
|
cursor: pointer; |
||||
|
max-width: 36px; |
||||
|
max-height: 36px; |
||||
|
margin: 0; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
.list-content { |
||||
|
flex-grow: 1; |
||||
|
} |
@ -0,0 +1,64 @@ |
|||||
|
import React, { |
||||
|
useEffect, useMemo, useState, |
||||
|
} from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { useSelector } from 'react-redux'; |
||||
|
import { List } from 'semantic-ui-react'; |
||||
|
import TopicListRow from './TopicListRow'; |
||||
|
import { drizzle } from '../../redux/store'; |
||||
|
import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames'; |
||||
|
|
||||
|
const { contracts: { [FORUM_CONTRACT]: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle; |
||||
|
|
||||
|
const TopicList = (props) => { |
||||
|
const { topicIds } = props; |
||||
|
const [getTopicCallHashes, setGetTopicCallHashes] = useState([]); |
||||
|
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized); |
||||
|
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (drizzleInitialized && !drizzleInitializationFailed) { |
||||
|
const newTopicsFound = topicIds |
||||
|
.filter((topicId) => !getTopicCallHashes |
||||
|
.map((getTopicCallHash) => getTopicCallHash.id) |
||||
|
.includes(topicId)); |
||||
|
|
||||
|
if (newTopicsFound.length > 0) { |
||||
|
setGetTopicCallHashes([ |
||||
|
...getTopicCallHashes, |
||||
|
...newTopicsFound |
||||
|
.map((topicId) => ({ |
||||
|
id: topicId, |
||||
|
hash: getTopicChainData(topicId), |
||||
|
})), |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
|
}, [drizzleInitializationFailed, drizzleInitialized, getTopicCallHashes, topicIds]); |
||||
|
|
||||
|
const topics = useMemo(() => topicIds |
||||
|
.map((topicId) => { |
||||
|
const topicHash = getTopicCallHashes.find((getTopicCallHash) => getTopicCallHash.id === topicId); |
||||
|
|
||||
|
return ( |
||||
|
<TopicListRow |
||||
|
id={topicId} |
||||
|
key={topicId} |
||||
|
topicCallHash={topicHash && topicHash.hash} |
||||
|
loading={topicHash === undefined} |
||||
|
/> |
||||
|
); |
||||
|
}), [getTopicCallHashes, topicIds]); |
||||
|
|
||||
|
return ( |
||||
|
<List selection divided id="topic-list" size="big"> |
||||
|
{topics} |
||||
|
</List> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
TopicList.propTypes = { |
||||
|
topicIds: PropTypes.arrayOf(PropTypes.number).isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default TopicList; |
@ -0,0 +1,3 @@ |
|||||
|
#topic-list{ |
||||
|
height: 100%; |
||||
|
} |
@ -0,0 +1,104 @@ |
|||||
|
import React, { useCallback, useEffect, useMemo } from 'react'; |
||||
|
import { |
||||
|
Form, Input, |
||||
|
} from 'semantic-ui-react'; |
||||
|
import throttle from 'lodash/throttle'; |
||||
|
import { useTranslation } from 'react-i18next'; |
||||
|
import { useSelector } from 'react-redux'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { drizzle } from '../redux/store'; |
||||
|
import { FORUM_CONTRACT } from '../constants/contracts/ContractNames'; |
||||
|
|
||||
|
const { contracts: { [FORUM_CONTRACT]: { methods: { isUserNameTaken } } } } = drizzle; |
||||
|
|
||||
|
const UsernameSelector = (props) => { |
||||
|
const { |
||||
|
initialUsername, username, onChangeCallback, onErrorChangeCallback, |
||||
|
} = props; |
||||
|
const isUserNameTakenResults = useSelector((state) => state.contracts[FORUM_CONTRACT].isUserNameTaken); |
||||
|
const { t } = useTranslation(); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (username.length > 0) { |
||||
|
const checkedUsernames = Object |
||||
|
.values(isUserNameTakenResults) |
||||
|
.map((callCompleted) => ({ |
||||
|
checkedUsername: callCompleted.args[0], |
||||
|
isTaken: callCompleted.value, |
||||
|
})); |
||||
|
|
||||
|
const checkedUsername = checkedUsernames |
||||
|
.find((callCompleted) => callCompleted.checkedUsername === username); |
||||
|
|
||||
|
if (checkedUsername && checkedUsername.isTaken && username !== initialUsername) { |
||||
|
onErrorChangeCallback({ |
||||
|
usernameChecked: true, |
||||
|
error: true, |
||||
|
errorMessage: t('username.selector.error.username.taken.message', { username }), |
||||
|
}); |
||||
|
} else { |
||||
|
onErrorChangeCallback({ |
||||
|
usernameChecked: true, |
||||
|
error: false, |
||||
|
errorMessage: null, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Username input is empty |
||||
|
if (initialUsername && initialUsername !== '') { |
||||
|
onErrorChangeCallback({ |
||||
|
usernameChecked: true, |
||||
|
error: true, |
||||
|
errorMessage: t('username.selector.error.username.empty.message'), |
||||
|
}); |
||||
|
} else { |
||||
|
onErrorChangeCallback({ |
||||
|
usernameChecked: true, |
||||
|
error: false, |
||||
|
errorMessage: null, |
||||
|
}); |
||||
|
} |
||||
|
}, [initialUsername, isUserNameTakenResults, onErrorChangeCallback, t, username, username.length]); |
||||
|
|
||||
|
const checkUsernameTaken = useMemo(() => throttle( |
||||
|
(usernameToCheck) => { |
||||
|
isUserNameTaken.cacheCall(usernameToCheck); |
||||
|
}, 200, |
||||
|
), []); |
||||
|
|
||||
|
const handleInputChange = useCallback((event, { value }) => { |
||||
|
onChangeCallback(value); |
||||
|
|
||||
|
if (value.length > 0) { |
||||
|
checkUsernameTaken(value); |
||||
|
} |
||||
|
}, [checkUsernameTaken, onChangeCallback]); |
||||
|
|
||||
|
return ( |
||||
|
<Form.Field required> |
||||
|
<label htmlFor="form-field-username-selector"> |
||||
|
{t('username.selector.username.field.label')} |
||||
|
</label> |
||||
|
<Input |
||||
|
id="form-field-username-selector" |
||||
|
placeholder={t('username.selector.username.field.placeholder')} |
||||
|
name="usernameInput" |
||||
|
className="form-input" |
||||
|
value={username} |
||||
|
onChange={handleInputChange} |
||||
|
/> |
||||
|
</Form.Field> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
UsernameSelector.propTypes = { |
||||
|
initialUsername: PropTypes.string, |
||||
|
username: PropTypes.string.isRequired, |
||||
|
onChangeCallback: PropTypes.func.isRequired, |
||||
|
onErrorChangeCallback: PropTypes.func.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default UsernameSelector; |
@ -0,0 +1,22 @@ |
|||||
|
export const GENERAL_TAB = { |
||||
|
id: 'general-tab', |
||||
|
intl_display_name_id: 'profile.general.tab.title', |
||||
|
}; |
||||
|
|
||||
|
export const TOPICS_TAB = { |
||||
|
id: 'topics-tab', |
||||
|
intl_display_name_id: 'profile.topics.tab.title', |
||||
|
}; |
||||
|
|
||||
|
export const POSTS_TAB = { |
||||
|
id: 'posts-tab', |
||||
|
intl_display_name_id: 'profile.posts.tab.title', |
||||
|
}; |
||||
|
|
||||
|
const profileTabs = [ |
||||
|
GENERAL_TAB, |
||||
|
TOPICS_TAB, |
||||
|
POSTS_TAB, |
||||
|
]; |
||||
|
|
||||
|
export default profileTabs; |
@ -0,0 +1,2 @@ |
|||||
|
export const REGISTER_STEP_SIGNUP = 'signup'; |
||||
|
export const REGISTER_STEP_PROFILE_INFORMATION = 'profile-information'; |
@ -0,0 +1,2 @@ |
|||||
|
export const TRANSACTION_SUCCESS = 'success'; |
||||
|
export const TRANSACTION_ERROR = 'error'; |
@ -0,0 +1,7 @@ |
|||||
|
export const WEB3_HOST_DEFAULT = '127.0.0.1'; |
||||
|
export const WEB3_PORT_DEFAULT = '8545'; |
||||
|
export const WEB3_PORT_SOCKET_TIMEOUT_DEFAULT = 30000; |
||||
|
export const WEB3_PORT_SOCKET_CONNECT_MAX_ATTEMPTS_DEFAULT = 3; |
||||
|
|
||||
|
export const REACT_APP_RENDEZVOUS_HOST_DEFAULT = '127.0.0.1'; |
||||
|
export const REACT_APP_RENDEZVOUS_PORT_DEFAULT = '9090'; |
@ -0,0 +1 @@ |
|||||
|
export const FORUM_CONTRACT = 'Forum'; |
@ -0,0 +1,13 @@ |
|||||
|
export const USER_SIGNED_UP_EVENT = 'UserSignedUp'; |
||||
|
export const USERNAME_UPDATED_EVENT = 'UsernameUpdated'; |
||||
|
export const TOPIC_CREATED_EVENT = 'TopicCreated'; |
||||
|
export const POST_CREATED_EVENT = 'PostCreated'; |
||||
|
|
||||
|
const forumContractEvents = [ |
||||
|
USER_SIGNED_UP_EVENT, |
||||
|
USERNAME_UPDATED_EVENT, |
||||
|
TOPIC_CREATED_EVENT, |
||||
|
POST_CREATED_EVENT, |
||||
|
]; |
||||
|
|
||||
|
export default forumContractEvents; |
@ -0,0 +1,8 @@ |
|||||
|
import { FORUM_CONTRACT } from '../ContractNames'; |
||||
|
import forumContractEvents from './ForumContractEvents'; |
||||
|
|
||||
|
const appEvents = { |
||||
|
[FORUM_CONTRACT]: forumContractEvents, |
||||
|
}; |
||||
|
|
||||
|
export default appEvents; |
@ -0,0 +1,20 @@ |
|||||
|
export const USER_DATABASE = 'user'; |
||||
|
export const TOPICS_DATABASE = 'topics'; |
||||
|
export const POSTS_DATABASE = 'posts'; |
||||
|
|
||||
|
const databases = [ |
||||
|
{ |
||||
|
address: USER_DATABASE, |
||||
|
type: 'keyvalue', |
||||
|
}, |
||||
|
{ |
||||
|
address: TOPICS_DATABASE, |
||||
|
type: 'keyvalue', |
||||
|
}, |
||||
|
{ |
||||
|
address: POSTS_DATABASE, |
||||
|
type: 'keyvalue', |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
export default databases; |
@ -0,0 +1,7 @@ |
|||||
|
export const POST_CONTENT = 'content'; |
||||
|
|
||||
|
const postsDatabaseKeys = [ |
||||
|
POST_CONTENT, |
||||
|
]; |
||||
|
|
||||
|
export default postsDatabaseKeys; |
@ -0,0 +1,7 @@ |
|||||
|
export const TOPIC_SUBJECT = 'subject'; |
||||
|
|
||||
|
const topicsDatabaseKeys = [ |
||||
|
TOPIC_SUBJECT, |
||||
|
]; |
||||
|
|
||||
|
export default topicsDatabaseKeys; |
@ -0,0 +1,9 @@ |
|||||
|
export const USER_PROFILE_PICTURE = 'profile_picture'; |
||||
|
export const USER_LOCATION = 'location'; |
||||
|
|
||||
|
const userDatabaseKeys = [ |
||||
|
USER_PROFILE_PICTURE, |
||||
|
USER_LOCATION, |
||||
|
]; |
||||
|
|
||||
|
export default userDatabaseKeys; |
@ -1,29 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import { render } from 'react-dom'; |
|
||||
import App from './components/App' |
|
||||
import store from './redux/store'; |
|
||||
import { Drizzle } from '@ezerous/drizzle' |
|
||||
import { Breeze } from '@ezerous/breeze' |
|
||||
|
|
||||
import AppContext from "./components/AppContext"; |
|
||||
|
|
||||
import drizzleOptions from './options/drizzleOptions'; |
|
||||
import * as serviceWorker from './utils/serviceWorker'; |
|
||||
|
|
||||
import './assets/css/index.css'; |
|
||||
import breezeOptions from './options/breezeOptions'; |
|
||||
|
|
||||
const drizzle = new Drizzle(drizzleOptions, store); |
|
||||
const breeze = new Breeze(breezeOptions, store); |
|
||||
|
|
||||
render( |
|
||||
<AppContext.Provider drizzle={drizzle} breeze={breeze}> |
|
||||
<App store={store} /> |
|
||||
</AppContext.Provider>, |
|
||||
document.getElementById('root') |
|
||||
); |
|
||||
|
|
||||
serviceWorker.unregister(); // See also: http://bit.ly/CRA-PWA
|
|
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1,18 @@ |
|||||
|
import './utils/indexedDB/patchIndexedDB'; |
||||
|
import './utils/wdyr'; |
||||
|
import React, { Suspense } from 'react'; |
||||
|
import { render } from 'react-dom'; |
||||
|
import App from './App'; |
||||
|
import store from './redux/store'; |
||||
|
import * as serviceWorker from './utils/serviceWorker'; |
||||
|
import LoadingScreen from './components/LoadingScreen'; |
||||
|
import './assets/css/index.css'; |
||||
|
|
||||
|
render( |
||||
|
<Suspense fallback={<LoadingScreen />}> |
||||
|
<App store={store} /> |
||||
|
</Suspense>, |
||||
|
document.getElementById('root'), |
||||
|
); |
||||
|
|
||||
|
serviceWorker.unregister(); // See also: http://bit.ly/CRA-PWA |
@ -0,0 +1,25 @@ |
|||||
|
import i18n from 'i18next'; |
||||
|
import { initReactI18next } from 'react-i18next'; |
||||
|
import Backend from 'i18next-http-backend'; |
||||
|
import LanguageDetector from 'i18next-browser-languagedetector'; |
||||
|
|
||||
|
const currentLanguage = localStorage.getItem('i18nextLng'); |
||||
|
|
||||
|
if (currentLanguage === null) { |
||||
|
localStorage.setItem('i18nextLng', 'en'); |
||||
|
} |
||||
|
|
||||
|
i18n |
||||
|
.use(Backend) // load translation using http -> see /public/locales
|
||||
|
.use(LanguageDetector) // detect user language
|
||||
|
.use(initReactI18next) // pass the i18n instance to react-i18next.
|
||||
|
.init({ // init i18next
|
||||
|
fallbackLng: 'en', |
||||
|
keySeparator: false, // we do not use keys in form messages.welcome
|
||||
|
debug: process.env.NODE_ENV === 'development', |
||||
|
interpolation: { |
||||
|
escapeValue: false, // not needed for react as it escapes by default
|
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default i18n; |
@ -0,0 +1,100 @@ |
|||||
|
import React, { useState } from 'react'; |
||||
|
import { Dropdown, Menu } from 'semantic-ui-react'; |
||||
|
import { useTranslation } from 'react-i18next'; |
||||
|
import { useHistory } from 'react-router'; |
||||
|
import { useSelector } from 'react-redux'; |
||||
|
import AppContext from '../../../components/AppContext'; |
||||
|
import appLogo from '../../../assets/images/app_logo.png'; |
||||
|
import ClearDatabasesModal from '../../../components/ClearDatabasesModal'; |
||||
|
|
||||
|
const MainLayoutMenu = () => { |
||||
|
const hasSignedUp = useSelector((state) => state.user.hasSignedUp); |
||||
|
const [isClearDatabasesOpen, setIsClearDatabasesOpen] = useState(false); |
||||
|
const history = useHistory(); |
||||
|
const { t } = useTranslation(); |
||||
|
|
||||
|
const handleClearDatabasesClick = () => { |
||||
|
setIsClearDatabasesOpen(true); |
||||
|
}; |
||||
|
|
||||
|
const handleDatabasesCleared = () => { |
||||
|
setIsClearDatabasesOpen(false); |
||||
|
history.push('/home'); |
||||
|
window.location.reload(false); |
||||
|
}; |
||||
|
|
||||
|
const handleCancelDatabasesClear = () => { |
||||
|
setIsClearDatabasesOpen(false); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<AppContext.Consumer> |
||||
|
{() => ( |
||||
|
<Menu color="black" inverted> |
||||
|
<Menu.Item |
||||
|
link |
||||
|
name="home" |
||||
|
key="home" |
||||
|
onClick={() => { history.push('/'); }} |
||||
|
> |
||||
|
<img src={appLogo} alt="app_logo" /> |
||||
|
</Menu.Item> |
||||
|
<Menu.Menu position="right"> |
||||
|
{hasSignedUp && history.location.pathname === '/home' && ( |
||||
|
<Menu.Item |
||||
|
link |
||||
|
name="create-topic" |
||||
|
key="create-topic" |
||||
|
onClick={() => { history.push('/topics/new'); }} |
||||
|
position="right" |
||||
|
> |
||||
|
{t('topbar.button.create.topic')} |
||||
|
</Menu.Item> |
||||
|
)} |
||||
|
{hasSignedUp |
||||
|
? ( |
||||
|
<Menu.Item |
||||
|
link |
||||
|
name="profile" |
||||
|
key="profile" |
||||
|
onClick={() => { history.push('/profile'); }} |
||||
|
> |
||||
|
{t('topbar.button.profile')} |
||||
|
</Menu.Item> |
||||
|
) |
||||
|
: ( |
||||
|
<Menu.Item |
||||
|
link |
||||
|
name="register" |
||||
|
key="register" |
||||
|
onClick={() => { history.push('/auth/register'); }} |
||||
|
> |
||||
|
{t('topbar.button.register')} |
||||
|
</Menu.Item> |
||||
|
)} |
||||
|
</Menu.Menu> |
||||
|
<Dropdown key="overflow" item direction="left"> |
||||
|
<Dropdown.Menu> |
||||
|
<Dropdown.Item |
||||
|
link |
||||
|
name="clear-databases" |
||||
|
key="clear-databases" |
||||
|
onClick={handleClearDatabasesClick} |
||||
|
> |
||||
|
{t('topbar.button.clear.databases')} |
||||
|
</Dropdown.Item> |
||||
|
</Dropdown.Menu> |
||||
|
</Dropdown> |
||||
|
|
||||
|
<ClearDatabasesModal |
||||
|
open={isClearDatabasesOpen} |
||||
|
onDatabasesCleared={handleDatabasesCleared} |
||||
|
onCancel={handleCancelDatabasesClear} |
||||
|
/> |
||||
|
</Menu> |
||||
|
)} |
||||
|
</AppContext.Consumer> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default MainLayoutMenu; |
@ -0,0 +1,21 @@ |
|||||
|
import React from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import MainLayoutMenu from './MainLayoutMenu'; |
||||
|
import './styles.css'; |
||||
|
|
||||
|
const MainLayout = (props) => { |
||||
|
const { children } = props; |
||||
|
|
||||
|
return ( |
||||
|
<div id="main-layout"> |
||||
|
<MainLayoutMenu /> |
||||
|
{children} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
MainLayout.propTypes = { |
||||
|
children: PropTypes.element.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default MainLayout; |
@ -0,0 +1,3 @@ |
|||||
|
#main-layout { |
||||
|
height: 100%; |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
import React from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import Particles from 'react-particles-js'; |
||||
|
import particlesOptions from '../../assets/particles'; |
||||
|
import './styles.css'; |
||||
|
|
||||
|
const RegisterLayout = (props) => { |
||||
|
const { children } = props; |
||||
|
|
||||
|
return ( |
||||
|
<div id="register-layout"> |
||||
|
<Particles className="particles" params={particlesOptions} /> |
||||
|
{children} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
RegisterLayout.propTypes = { |
||||
|
children: PropTypes.element.isRequired, |
||||
|
}; |
||||
|
|
||||
|
export default RegisterLayout; |
@ -0,0 +1,8 @@ |
|||||
|
.particles { |
||||
|
position: fixed; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
z-index: -1; |
||||
|
background: rgba(0, 0, 0, 0) linear-gradient(45deg, rgb(45, 54, 76) 0%, rgb(37, 45, 63) 100%) repeat scroll 0 0; |
||||
|
} |
@ -1,45 +1,40 @@ |
|||||
import web3Options from './web3Options'; |
import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider'; |
||||
import EthereumIdentityProvider from '../orbit/ΕthereumIdentityProvider'; |
import databases from '../constants/orbit/OrbitDatabases'; |
||||
import { orbitConstants } from '@ezerous/breeze' |
import { |
||||
|
REACT_APP_RENDEZVOUS_HOST_DEFAULT, |
||||
|
REACT_APP_RENDEZVOUS_PORT_DEFAULT, |
||||
|
} from '../constants/configuration/defaults'; |
||||
|
|
||||
const { web3 } = web3Options; |
const REACT_APP_RENDEZVOUS_HOST = process.env.REACT_APP_RENDEZVOUS_HOST || REACT_APP_RENDEZVOUS_HOST_DEFAULT; |
||||
EthereumIdentityProvider.setWeb3(web3); |
const REACT_APP_RENDEZVOUS_PORT = process.env.REACT_APP_RENDEZVOUS_PORT || REACT_APP_RENDEZVOUS_PORT_DEFAULT; |
||||
|
|
||||
const breezeOptions = { |
const breezeOptions = { |
||||
ipfs: { |
ipfs: { |
||||
|
repo: 'concordia', |
||||
config: { |
config: { |
||||
Addresses: { |
Addresses: { |
||||
Swarm: [ |
Swarm: [ |
||||
// Use local signaling server (see also rendezvous script in package.json)
|
// Use local signaling server (see also rendezvous script in package.json)
|
||||
// For more information: https://github.com/libp2p/js-libp2p-webrtc-star
|
// For more information: https://github.com/libp2p/js-libp2p-webrtc-star
|
||||
'/ip4/127.0.0.1/tcp/9090/wss/p2p-webrtc-star' |
`/ip4/${REACT_APP_RENDEZVOUS_HOST}/tcp/${REACT_APP_RENDEZVOUS_PORT}/wss/p2p-webrtc-star`, |
||||
|
|
||||
// Use the following public servers if needed
|
// Use the following public servers if needed
|
||||
// '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
|
// '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
|
||||
// '/dns4/ wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
|
// '/dns4/ wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
|
||||
] |
], |
||||
}, |
}, |
||||
}, |
}, |
||||
preload: { |
preload: { |
||||
enabled: false |
enabled: false, |
||||
}, |
}, |
||||
init: { |
init: { |
||||
emptyRepo: true |
emptyRepo: true, |
||||
} |
}, |
||||
}, |
}, |
||||
orbit: { |
orbit: { |
||||
identityProvider: EthereumIdentityProvider, |
identityProvider: EthereumContractIdentityProvider, |
||||
databases: [ |
databases, |
||||
{ |
|
||||
name: 'topics', |
|
||||
type: orbitConstants.ORBIT_TYPE_KEYVALUE |
|
||||
}, |
}, |
||||
{ |
|
||||
name: 'posts', |
|
||||
type: orbitConstants.ORBIT_TYPE_KEYVALUE |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
}; |
}; |
||||
|
|
||||
export default breezeOptions; |
export default breezeOptions; |
||||
|
@ -1,17 +1,14 @@ |
|||||
// See also: https://truffleframework.com/docs/drizzle/reference/drizzle-options
|
// Check out the documentation: https://truffleframework.com/docs/drizzle/reference/drizzle-options
|
||||
import { contracts } from 'concordia-contracts'; |
import { contracts } from 'concordia-contracts'; |
||||
import web3Options from './web3Options'; |
import web3Options from './web3Options'; |
||||
|
import appEvents from '../constants/contracts/events'; |
||||
|
|
||||
const drizzleOptions = { |
const drizzleOptions = { |
||||
web3: { |
web3: web3Options, |
||||
customProvider: web3Options.web3 |
|
||||
}, |
|
||||
contracts, |
contracts, |
||||
events: { |
events: { ...appEvents }, |
||||
Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated'] |
|
||||
}, |
|
||||
reloadWindowOnNetworkChange: true, |
reloadWindowOnNetworkChange: true, |
||||
reloadWindowOnAccountChange: true // We need it to reinitialize breeze and create new Orbit databases
|
reloadWindowOnAccountChange: true, // We need it to reinitialize breeze and create new Orbit databases
|
||||
}; |
}; |
||||
|
|
||||
export default drizzleOptions; |
export default drizzleOptions; |
||||
|
@ -1,14 +1,29 @@ |
|||||
import Web3 from 'web3'; |
import Web3 from 'web3'; |
||||
import EthereumIdentityProvider from '../orbit/ΕthereumIdentityProvider'; |
import { |
||||
|
WEB3_HOST_DEFAULT, |
||||
|
WEB3_PORT_DEFAULT, |
||||
|
WEB3_PORT_SOCKET_CONNECT_MAX_ATTEMPTS_DEFAULT, |
||||
|
WEB3_PORT_SOCKET_TIMEOUT_DEFAULT, |
||||
|
} from '../constants/configuration/defaults'; |
||||
|
|
||||
const { WEB3_URL, WEB3_PORT } = process.env; |
const { WEB3_HOST, WEB3_PORT, WEBSOCKET_TIMEOUT } = process.env; |
||||
|
|
||||
const web3 = new Web3(Web3.givenProvider || `ws://${WEB3_URL}:${WEB3_PORT}`); |
const web3WebsocketOptions = { |
||||
|
keepAlive: true, |
||||
|
timeout: WEBSOCKET_TIMEOUT !== undefined ? WEBSOCKET_TIMEOUT : WEB3_PORT_SOCKET_TIMEOUT_DEFAULT, |
||||
|
reconnect: { |
||||
|
maxAttempts: WEB3_PORT_SOCKET_CONNECT_MAX_ATTEMPTS_DEFAULT, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
EthereumIdentityProvider.setWeb3(web3); |
const web3 = (WEB3_HOST !== undefined && WEB3_PORT !== undefined) |
||||
|
? new Web3.providers.WebsocketProvider(`ws://${WEB3_HOST}:${WEB3_PORT}`) |
||||
|
: new Web3(Web3.givenProvider || new Web3.providers.WebsocketProvider( |
||||
|
`ws://${WEB3_HOST_DEFAULT}:${WEB3_PORT_DEFAULT}`, web3WebsocketOptions, |
||||
|
)); |
||||
|
|
||||
const web3Options = { |
const web3Options = { |
||||
web3 |
customProvider: web3, |
||||
}; |
}; |
||||
|
|
||||
export default web3Options; |
export default web3Options; |
||||
|
@ -1,23 +0,0 @@ |
|||||
import level from 'level'; |
|
||||
|
|
||||
/* Used in development only to store the identity.signatures.publicKey so developers don't have to |
|
||||
repeatedly sign theOrbitDB creation transaction in MetaMask when React development server reloads |
|
||||
the app */ |
|
||||
const concordiaDB = level('./concordia/identities'); |
|
||||
|
|
||||
async function storeIdentitySignaturePubKey(key, signaturePubKey) { |
|
||||
await concordiaDB.put(key, signaturePubKey); |
|
||||
} |
|
||||
|
|
||||
// If it exists, it returns the identity.signatures.publicKey for the given key (key is the
|
|
||||
// concatenation of identity.publicKey + identity.signatures.id
|
|
||||
async function getIdentitySignaturePubKey(key) { |
|
||||
try { |
|
||||
return await concordiaDB.get(key); |
|
||||
} catch (err) { |
|
||||
if (err && err.notFound) return null; // Not found
|
|
||||
throw err; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export { storeIdentitySignaturePubKey, getIdentitySignaturePubKey }; |
|
@ -1,7 +0,0 @@ |
|||||
// https://github.com/orbitdb/orbit-db/blob/master/GUIDE.md#address
|
|
||||
export async function determineDBAddress({orbit, dbName, type, identityId}) { |
|
||||
const ipfsMultihash = (await orbit.determineAddress(dbName, type, { |
|
||||
accessController: { write: [identityId] }, |
|
||||
})).root; |
|
||||
return `/orbitdb/${ipfsMultihash}/${dbName}`; |
|
||||
} |
|
@ -1,108 +0,0 @@ |
|||||
import { getIdentitySignaturePubKey, storeIdentitySignaturePubKey } from './levelUtils'; |
|
||||
import IdentityProvider from "orbit-db-identity-provider"; |
|
||||
|
|
||||
const LOGGING_PREFIX = 'EthereumIdentityProvider: '; |
|
||||
|
|
||||
class EthereumIdentityProvider extends IdentityProvider{ |
|
||||
constructor(options = {}) { |
|
||||
if(!EthereumIdentityProvider.web3) |
|
||||
throw new Error(LOGGING_PREFIX + "Couldn't create identity, because web3 wasn't set. " + |
|
||||
"Please use setWeb3(web3) first!"); |
|
||||
|
|
||||
super(options); |
|
||||
|
|
||||
// Orbit's Identity Id (user's Ethereum address) - Optional (will be grabbed later if omitted)
|
|
||||
const id = options.id; |
|
||||
if(id){ |
|
||||
if(EthereumIdentityProvider.web3.utils.isAddress(id)) |
|
||||
this.id = options.id; |
|
||||
else |
|
||||
throw new Error(LOGGING_PREFIX + "Couldn't create identity, because an invalid id was supplied."); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static get type() { return 'ethereum'; } |
|
||||
|
|
||||
async getId() { |
|
||||
// Id wasn't in the constructor, grab it now
|
|
||||
if(!this.id) { |
|
||||
const accounts = await EthereumIdentityProvider.web3.eth.getAccounts(); |
|
||||
if(!accounts[0]) |
|
||||
throw new Error(LOGGING_PREFIX + "Couldn't create identity, because no web3 accounts were found (locked Metamask?)."); |
|
||||
|
|
||||
this.id = accounts[0]; |
|
||||
} |
|
||||
return this.id; |
|
||||
} |
|
||||
|
|
||||
async signIdentity(data) { |
|
||||
if (process.env.NODE_ENV === 'development') { //Don't sign repeatedly while in development
|
|
||||
console.debug(LOGGING_PREFIX + 'Attempting to find stored Orbit identity data...'); |
|
||||
const signaturePubKey = await getIdentitySignaturePubKey(data); |
|
||||
if (signaturePubKey) { |
|
||||
const identityInfo = { |
|
||||
id: this.id, |
|
||||
pubKeySignId: data, |
|
||||
signaturePubKey, |
|
||||
}; |
|
||||
if (await EthereumIdentityProvider.verifyIdentityInfo(identityInfo)) { |
|
||||
console.debug(LOGGING_PREFIX + 'Found and verified stored Orbit identity data!'); |
|
||||
return signaturePubKey; |
|
||||
} |
|
||||
console.debug(LOGGING_PREFIX + "Stored Orbit identity data couldn't be verified."); |
|
||||
} else |
|
||||
console.debug(LOGGING_PREFIX + 'No stored Orbit identity data were found.'); |
|
||||
} |
|
||||
return await this.doSignIdentity(data); |
|
||||
} |
|
||||
|
|
||||
async doSignIdentity(data) { |
|
||||
try { |
|
||||
const signaturePubKey = await EthereumIdentityProvider.web3.eth.personal.sign(data, this.id, ''); |
|
||||
if (process.env.NODE_ENV === 'development') { |
|
||||
storeIdentitySignaturePubKey(data, signaturePubKey) |
|
||||
.then(() => { |
|
||||
console.debug(LOGGING_PREFIX + 'Successfully stored current Orbit identity data.'); |
|
||||
}) |
|
||||
.catch(() => { |
|
||||
console.warn(LOGGING_PREFIX + "Couldn't store current Orbit identity data..."); |
|
||||
}); |
|
||||
} |
|
||||
return signaturePubKey; // Password not required for MetaMask
|
|
||||
} catch (error) { |
|
||||
if(error.code && error.code === 4001){ |
|
||||
console.debug(LOGGING_PREFIX + 'User denied message signature.'); |
|
||||
return await this.doSignIdentity(data); |
|
||||
} |
|
||||
else{ |
|
||||
console.error(LOGGING_PREFIX + 'Failed to sign data.'); |
|
||||
console.error(error); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static async verifyIdentity(identity) { |
|
||||
// Verify that identity was signed by the ID
|
|
||||
return new Promise(resolve => { |
|
||||
resolve(EthereumIdentityProvider.web3.eth.accounts.recover(identity.publicKey + identity.signatures.id, |
|
||||
identity.signatures.publicKey) === identity.id) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
static async verifyIdentityInfo(identityInfo) { |
|
||||
// Verify that identity was signed by the ID
|
|
||||
return new Promise(resolve => { |
|
||||
resolve(EthereumIdentityProvider.web3.eth.accounts.recover(identityInfo.pubKeySignId, |
|
||||
identityInfo.signaturePubKey) === identityInfo.id) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
// Initialize by supplying a web3 object
|
|
||||
static setWeb3(web3){ |
|
||||
EthereumIdentityProvider.web3 = web3; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
EthereumIdentityProvider.web3 = {}; |
|
||||
|
|
||||
export default EthereumIdentityProvider; |
|
@ -0,0 +1,20 @@ |
|||||
|
import { |
||||
|
POST_CREATED_EVENT, |
||||
|
TOPIC_CREATED_EVENT, |
||||
|
USER_SIGNED_UP_EVENT, |
||||
|
USERNAME_UPDATED_EVENT, |
||||
|
} from '../../constants/contracts/events/ForumContractEvents'; |
||||
|
|
||||
|
export const FORUM_EVENT_USER_SIGNED_UP = 'FORUM_EVENT_USER_SIGNED_UP'; |
||||
|
export const FORUM_EVENT_USERNAME_UPDATED = 'FORUM_EVENT_USERNAME_UPDATED'; |
||||
|
export const FORUM_EVENT_TOPIC_CREATED = 'FORUM_EVENT_TOPIC_CREATED'; |
||||
|
export const FORUM_EVENT_POST_CREATED = 'FORUM_EVENT_POST_CREATED'; |
||||
|
|
||||
|
const eventActionMap = { |
||||
|
[USER_SIGNED_UP_EVENT]: FORUM_EVENT_USER_SIGNED_UP, |
||||
|
[USERNAME_UPDATED_EVENT]: FORUM_EVENT_USERNAME_UPDATED, |
||||
|
[TOPIC_CREATED_EVENT]: FORUM_EVENT_TOPIC_CREATED, |
||||
|
[POST_CREATED_EVENT]: FORUM_EVENT_POST_CREATED, |
||||
|
}; |
||||
|
|
||||
|
export default eventActionMap; |
@ -0,0 +1,2 @@ |
|||||
|
export const FETCH_USER_DATABASE = 'FETCH_USER_DATABASE'; |
||||
|
export const UPDATE_ORBIT_DATA = 'UPDATE_ORBIT_DATA'; |
@ -0,0 +1,32 @@ |
|||||
|
import { UPDATE_ORBIT_DATA } from '../actions/peerDbReplicationActions'; |
||||
|
|
||||
|
const initialState = { |
||||
|
users: [], |
||||
|
topics: [], |
||||
|
posts: [], |
||||
|
}; |
||||
|
|
||||
|
const peerDbReplicationReducer = (state = initialState, action) => { |
||||
|
const { type } = action; |
||||
|
|
||||
|
if (type === UPDATE_ORBIT_DATA) { |
||||
|
const { users, topics, posts } = action; |
||||
|
|
||||
|
return { |
||||
|
...state, |
||||
|
users: [ |
||||
|
...users, |
||||
|
], |
||||
|
topics: [ |
||||
|
...topics, |
||||
|
], |
||||
|
posts: [ |
||||
|
...posts, |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return state; |
||||
|
}; |
||||
|
|
||||
|
export default peerDbReplicationReducer; |
@ -0,0 +1,13 @@ |
|||||
|
import { put, takeEvery } from 'redux-saga/effects'; |
||||
|
import { CONTRACT_EVENT_FIRED } from '@ezerous/drizzle/src/contracts/constants'; |
||||
|
import eventActionMap from '../actions/contractEventActions'; |
||||
|
|
||||
|
function* eventBreakDown({ event }) { |
||||
|
yield put({ type: eventActionMap[event.event], event: { ...event } }); |
||||
|
} |
||||
|
|
||||
|
function* eventSaga() { |
||||
|
yield takeEvery(CONTRACT_EVENT_FIRED, eventBreakDown); |
||||
|
} |
||||
|
|
||||
|
export default eventSaga; |
@ -1,22 +1,34 @@ |
|||||
import { put, all, take } from 'redux-saga/effects' |
import { |
||||
|
call, put, all, take, |
||||
|
} from 'redux-saga/effects'; |
||||
|
|
||||
import { breezeActions } from '@ezerous/breeze' |
import { breezeActions } from '@ezerous/breeze'; |
||||
import { drizzleActions } from '@ezerous/drizzle' |
import { drizzleActions } from '@ezerous/drizzle'; |
||||
|
|
||||
|
import { forumContract } from 'concordia-contracts'; |
||||
|
import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider'; |
||||
|
|
||||
function* initOrbitDatabases(action) { |
function* initOrbitDatabases(action) { |
||||
const { account, breeze } = action; |
const { account, breeze } = action; |
||||
yield put(breezeActions.orbit.orbitInit(breeze, account)); //same as breeze.initOrbit(account);
|
// same as breeze.initOrbit(account);
|
||||
|
yield put(breezeActions.orbit.orbitInit(breeze, account + EthereumContractIdentityProvider.contractAddress)); |
||||
} |
} |
||||
|
|
||||
function* orbitSaga() { |
function* orbitSaga() { |
||||
const res = yield all([ |
const res = yield all([ |
||||
take(drizzleActions.drizzle.DRIZZLE_INITIALIZED), |
take(drizzleActions.drizzle.DRIZZLE_INITIALIZED), |
||||
take(breezeActions.breeze.BREEZE_INITIALIZED), |
take(breezeActions.breeze.BREEZE_INITIALIZED), |
||||
take(drizzleActions.account.ACCOUNTS_FETCHED) |
take(drizzleActions.account.ACCOUNTS_FETCHED), |
||||
]); |
]); |
||||
|
|
||||
|
const { drizzle: { web3 } } = res[0]; |
||||
|
const networkId = yield call([web3.eth.net, web3.eth.net.getId]); |
||||
|
const contractAddress = forumContract.networks[networkId].address; |
||||
|
|
||||
|
EthereumContractIdentityProvider.setContractAddress(contractAddress); |
||||
|
EthereumContractIdentityProvider.setWeb3(web3); |
||||
|
|
||||
yield initOrbitDatabases({ breeze: res[1].breeze, account: res[2].accounts[0] }); |
yield initOrbitDatabases({ breeze: res[1].breeze, account: res[2].accounts[0] }); |
||||
} |
} |
||||
|
|
||||
export default orbitSaga |
export default orbitSaga; |
||||
|
|
||||
|
@ -0,0 +1,112 @@ |
|||||
|
import { |
||||
|
call, put, select, takeEvery, |
||||
|
} from 'redux-saga/effects'; |
||||
|
import { |
||||
|
addOrbitDB, |
||||
|
ORBIT_DB_READY, |
||||
|
ORBIT_DB_REPLICATED, |
||||
|
ORBIT_DB_WRITE, |
||||
|
} from '@ezerous/breeze/src/orbit/orbitActions'; |
||||
|
import determineKVAddress from '../../utils/orbitUtils'; |
||||
|
import { FETCH_USER_DATABASE, UPDATE_ORBIT_DATA } from '../actions/peerDbReplicationActions'; |
||||
|
import { POSTS_DATABASE, TOPICS_DATABASE, USER_DATABASE } from '../../constants/orbit/OrbitDatabases'; |
||||
|
import userDatabaseKeys from '../../constants/orbit/UserDatabaseKeys'; |
||||
|
import { TOPIC_SUBJECT } from '../../constants/orbit/TopicsDatabaseKeys'; |
||||
|
import { POST_CONTENT } from '../../constants/orbit/PostsDatabaseKeys'; |
||||
|
|
||||
|
function* fetchUserDb({ orbit, userAddress, dbName }) { |
||||
|
const peerDbAddress = yield call(determineKVAddress, { |
||||
|
orbit, dbName, userAddress, |
||||
|
}); |
||||
|
|
||||
|
yield put(addOrbitDB({ address: peerDbAddress, type: 'keyvalue' })); |
||||
|
} |
||||
|
|
||||
|
function* updateReduxState({ database }) { |
||||
|
const { users, topics, posts } = yield select((state) => ({ |
||||
|
users: state.orbitData.users, |
||||
|
topics: state.orbitData.topics, |
||||
|
posts: state.orbitData.posts, |
||||
|
})); |
||||
|
|
||||
|
if (database.dbname === USER_DATABASE) { |
||||
|
const oldUsersUnchanged = users |
||||
|
.filter((user) => database.id !== user.id); |
||||
|
|
||||
|
yield put({ |
||||
|
type: UPDATE_ORBIT_DATA, |
||||
|
users: [ |
||||
|
...oldUsersUnchanged, |
||||
|
{ |
||||
|
id: database.id, |
||||
|
// Don't ask how.. it just works
|
||||
|
...Object |
||||
|
.entries(database.all) |
||||
|
.filter(([key]) => userDatabaseKeys.includes(key)) |
||||
|
.reduce(((acc, keyValue) => { |
||||
|
const [key, value] = keyValue; |
||||
|
acc[key] = value; |
||||
|
|
||||
|
return acc; |
||||
|
}), {}), |
||||
|
}, |
||||
|
], |
||||
|
topics: [...topics], |
||||
|
posts: [...posts], |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (database.dbname === TOPICS_DATABASE) { |
||||
|
const oldTopicsUnchanged = topics |
||||
|
.filter((topic) => !Object |
||||
|
.keys(database.all) |
||||
|
.map((key) => parseInt(key, 10)) |
||||
|
.includes(topic.id)); |
||||
|
|
||||
|
yield put({ |
||||
|
type: UPDATE_ORBIT_DATA, |
||||
|
users: [...users], |
||||
|
topics: [ |
||||
|
...oldTopicsUnchanged, |
||||
|
...Object |
||||
|
.entries(database.all) |
||||
|
.map(([key, value]) => ({ |
||||
|
id: parseInt(key, 10), |
||||
|
[TOPIC_SUBJECT]: value[TOPIC_SUBJECT], |
||||
|
})), |
||||
|
], |
||||
|
posts: [...posts], |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (database.dbname === POSTS_DATABASE) { |
||||
|
const oldPostsUnchanged = posts |
||||
|
.filter((post) => !Object |
||||
|
.keys(database.all) |
||||
|
.map((key) => parseInt(key, 10)) |
||||
|
.includes(post.id)); |
||||
|
|
||||
|
yield put({ |
||||
|
type: UPDATE_ORBIT_DATA, |
||||
|
users: [...users], |
||||
|
topics: [...topics], |
||||
|
posts: [ |
||||
|
...oldPostsUnchanged, |
||||
|
...Object.entries(database.all).map(([key, value]) => ({ |
||||
|
id: parseInt(key, 10), |
||||
|
[POST_CONTENT]: value[POST_CONTENT], |
||||
|
})), |
||||
|
], |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function* peerDbReplicationSaga() { |
||||
|
yield takeEvery(FETCH_USER_DATABASE, fetchUserDb); |
||||
|
|
||||
|
yield takeEvery(ORBIT_DB_REPLICATED, updateReduxState); |
||||
|
yield takeEvery(ORBIT_DB_READY, updateReduxState); |
||||
|
yield takeEvery(ORBIT_DB_WRITE, updateReduxState); |
||||
|
} |
||||
|
|
||||
|
export default peerDbReplicationSaga; |
@ -0,0 +1,22 @@ |
|||||
|
import { breeze } from '../../redux/store'; |
||||
|
|
||||
|
const purgeIndexedDBs = async () => { |
||||
|
const { ipfs, orbit } = breeze; |
||||
|
|
||||
|
if (orbit) await orbit.stop(); |
||||
|
if (ipfs) await ipfs.stop(); |
||||
|
|
||||
|
const databases = await indexedDB.databases(); |
||||
|
return Promise.all( |
||||
|
databases.map((db) => new Promise( |
||||
|
(resolve, reject) => { |
||||
|
const request = indexedDB.deleteDatabase(db.name); |
||||
|
request.onblocked = resolve; |
||||
|
request.onsuccess = resolve; |
||||
|
request.onerror = reject; |
||||
|
}, |
||||
|
)), |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default purgeIndexedDBs; |
@ -0,0 +1,46 @@ |
|||||
|
/* Patches browsers that do not yet support indexedDB.databases() |
||||
|
(https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/databases)
|
||||
|
See also https://gist.github.com/rmehner/b9a41d9f659c9b1c3340#gistcomment-3449418) */
|
||||
|
if (window.indexedDB && typeof window.indexedDB.databases === 'undefined') { |
||||
|
const LOCALSTORAGE_CACHE_KEY = 'indexedDBDatabases'; |
||||
|
|
||||
|
// Store a key value map of databases
|
||||
|
const getFromStorage = () => JSON.parse(window.localStorage[LOCALSTORAGE_CACHE_KEY] || '{}'); |
||||
|
|
||||
|
// Write the database to local storage
|
||||
|
const writeToStorage = (value) => { window.localStorage[LOCALSTORAGE_CACHE_KEY] = JSON.stringify(value); }; |
||||
|
|
||||
|
IDBFactory.prototype.databases = () => Promise.resolve( |
||||
|
Object.entries(getFromStorage()).reduce((acc, [name, version]) => { |
||||
|
acc.push({ name, version }); |
||||
|
return acc; |
||||
|
}, []), |
||||
|
); |
||||
|
|
||||
|
// Intercept the existing open handler to write our DBs names
|
||||
|
// and versions to localStorage
|
||||
|
const { open } = IDBFactory.prototype; |
||||
|
|
||||
|
// eslint-disable-next-line func-names
|
||||
|
IDBFactory.prototype.open = function (...args) { |
||||
|
const dbName = args[0]; |
||||
|
const version = args[1] || 1; |
||||
|
const existing = getFromStorage(); |
||||
|
writeToStorage({ ...existing, [dbName]: version }); |
||||
|
return open.apply(this, args); |
||||
|
}; |
||||
|
|
||||
|
// Intercept the existing deleteDatabase handler remove our
|
||||
|
// dbNames from localStorage
|
||||
|
const { deleteDatabase } = IDBFactory.prototype; |
||||
|
|
||||
|
// eslint-disable-next-line func-names
|
||||
|
IDBFactory.prototype.deleteDatabase = function (...args) { |
||||
|
const dbName = args[0]; |
||||
|
const existing = getFromStorage(); |
||||
|
delete existing[dbName]; |
||||
|
writeToStorage(existing); |
||||
|
return deleteDatabase.apply(this, args); |
||||
|
}; |
||||
|
console.debug('IndexedDB patched successfully!'); |
||||
|
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue