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, |
|||
"workspaces": { |
|||
"packages": ["packages/*"], |
|||
"nohoist": ["**/web3", "**/web3/**"] |
|||
"packages": [ |
|||
"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 { |
|||
margin: 10em; |
|||
padding: 0; |
|||
body.app { |
|||
overflow: auto; |
|||
margin: 1em !important; |
|||
} |
|||
|
|||
#root { |
|||
height: 100%; |
|||
} |
|||
|
|||
.i18next-newlines { |
|||
white-space: pre-line !important; |
|||
} |
|||
|
|||
.text-secondary { |
|||
color: gray; |
|||
font-style: italic; |
|||
} |
|||
|
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 EthereumIdentityProvider from '../orbit/ΕthereumIdentityProvider'; |
|||
import { orbitConstants } from '@ezerous/breeze' |
|||
import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider'; |
|||
import databases from '../constants/orbit/OrbitDatabases'; |
|||
import { |
|||
REACT_APP_RENDEZVOUS_HOST_DEFAULT, |
|||
REACT_APP_RENDEZVOUS_PORT_DEFAULT, |
|||
} from '../constants/configuration/defaults'; |
|||
|
|||
const { web3 } = web3Options; |
|||
EthereumIdentityProvider.setWeb3(web3); |
|||
const REACT_APP_RENDEZVOUS_HOST = process.env.REACT_APP_RENDEZVOUS_HOST || REACT_APP_RENDEZVOUS_HOST_DEFAULT; |
|||
const REACT_APP_RENDEZVOUS_PORT = process.env.REACT_APP_RENDEZVOUS_PORT || REACT_APP_RENDEZVOUS_PORT_DEFAULT; |
|||
|
|||
const breezeOptions = { |
|||
ipfs: { |
|||
repo: 'concordia', |
|||
config: { |
|||
Addresses: { |
|||
Swarm: [ |
|||
// Use local signaling server (see also rendezvous script in package.json)
|
|||
// 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
|
|||
// '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
|
|||
// '/dns4/ wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
|
|||
] |
|||
], |
|||
}, |
|||
}, |
|||
preload: { |
|||
enabled: false |
|||
enabled: false, |
|||
}, |
|||
init: { |
|||
emptyRepo: true |
|||
} |
|||
emptyRepo: true, |
|||
}, |
|||
}, |
|||
orbit: { |
|||
identityProvider: EthereumIdentityProvider, |
|||
databases: [ |
|||
{ |
|||
name: 'topics', |
|||
type: orbitConstants.ORBIT_TYPE_KEYVALUE |
|||
identityProvider: EthereumContractIdentityProvider, |
|||
databases, |
|||
}, |
|||
{ |
|||
name: 'posts', |
|||
type: orbitConstants.ORBIT_TYPE_KEYVALUE |
|||
} |
|||
] |
|||
} |
|||
}; |
|||
|
|||
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 web3Options from './web3Options'; |
|||
import appEvents from '../constants/contracts/events'; |
|||
|
|||
const drizzleOptions = { |
|||
web3: { |
|||
customProvider: web3Options.web3 |
|||
}, |
|||
web3: web3Options, |
|||
contracts, |
|||
events: { |
|||
Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated'] |
|||
}, |
|||
events: { ...appEvents }, |
|||
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; |
|||
|
@ -1,14 +1,29 @@ |
|||
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 = { |
|||
web3 |
|||
customProvider: web3, |
|||
}; |
|||
|
|||
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 { drizzleActions } from '@ezerous/drizzle' |
|||
import { breezeActions } from '@ezerous/breeze'; |
|||
import { drizzleActions } from '@ezerous/drizzle'; |
|||
|
|||
function * initOrbitDatabases (action) { |
|||
const { account, breeze} = action; |
|||
yield put(breezeActions.orbit.orbitInit(breeze, account)); //same as breeze.initOrbit(account);
|
|||
import { forumContract } from 'concordia-contracts'; |
|||
import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider'; |
|||
|
|||
function* initOrbitDatabases(action) { |
|||
const { account, breeze } = action; |
|||
// same as breeze.initOrbit(account);
|
|||
yield put(breezeActions.orbit.orbitInit(breeze, account + EthereumContractIdentityProvider.contractAddress)); |
|||
} |
|||
|
|||
function * orbitSaga () { |
|||
function* orbitSaga() { |
|||
const res = yield all([ |
|||
take(drizzleActions.drizzle.DRIZZLE_INITIALIZED), |
|||
take(breezeActions.breeze.BREEZE_INITIALIZED), |
|||
take(drizzleActions.account.ACCOUNTS_FETCHED) |
|||
take(drizzleActions.account.ACCOUNTS_FETCHED), |
|||
]); |
|||
|
|||
yield initOrbitDatabases({breeze:res[1].breeze, account: res[2].accounts[0]}); |
|||
} |
|||
const { drizzle: { web3 } } = res[0]; |
|||
const networkId = yield call([web3.eth.net, web3.eth.net.getId]); |
|||
const contractAddress = forumContract.networks[networkId].address; |
|||
|
|||
export default orbitSaga |
|||
EthereumContractIdentityProvider.setContractAddress(contractAddress); |
|||
EthereumContractIdentityProvider.setWeb3(web3); |
|||
|
|||
yield initOrbitDatabases({ breeze: res[1].breeze, account: res[2].accounts[0] }); |
|||
} |
|||
|
|||
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