Browse Source

Merge branch 'implement-ui' into 'develop'

Implement ui

See merge request ecentrics/concordia!5
develop
Apostolos Fanakis 4 years ago
parent
commit
54d747c8c2
  1. 22
      .dockerignore
  2. 4
      .gitignore
  3. 37
      README.md
  4. 67
      docker/Makefile
  5. 204
      docker/README.md
  6. 72
      docker/concordia-app/Dockerfile
  7. 22
      docker/concordia-app/nginx.conf
  8. 11
      docker/concordia-app/test-app.sh
  9. 66
      docker/concordia-contracts/Dockerfile
  10. 6
      docker/concordia-contracts/migrate.sh
  11. 16
      docker/concordia-contracts/test-contracts.sh
  12. 34
      docker/docker-compose.yml
  13. 7
      docker/env/concordia.docker.env
  14. 20
      docker/env/concordia.example.env
  15. 14
      docker/env/contracts.docker.env
  16. 7
      docker/env/contracts.example.env
  17. 5
      docker/env/ganache.docker.env
  18. 6
      docker/env/ganache.test.docker.env
  19. 10
      docker/ganache/Dockerfile
  20. 37
      docker/ganache/start-blockchain.sh
  21. 11
      package.json
  22. 0
      packages/concordia-app/.dockerignore
  23. 12
      packages/concordia-app/.env.development.example
  24. 60
      packages/concordia-app/.eslintrc.js
  25. 22
      packages/concordia-app/.gitignore
  26. 31
      packages/concordia-app/package.json
  27. 1
      packages/concordia-app/public/index.html
  28. 81
      packages/concordia-app/public/locales/en/translation.json
  29. 4
      packages/concordia-app/public/manifest.json
  30. 25
      packages/concordia-app/src/App.jsx
  31. 90
      packages/concordia-app/src/Routes.jsx
  32. 3
      packages/concordia-app/src/assets/css/app.css
  33. 19
      packages/concordia-app/src/assets/css/index.css
  34. 8
      packages/concordia-app/src/assets/css/loading-component.css
  35. 1
      packages/concordia-app/src/assets/images/metamask_logo.svg
  36. BIN
      packages/concordia-app/src/assets/images/orbitdb_logo.png
  37. 1
      packages/concordia-app/src/assets/images/orbitdb_logo.svg
  38. 32
      packages/concordia-app/src/assets/particles.js
  39. 34
      packages/concordia-app/src/components/App.jsx
  40. 72
      packages/concordia-app/src/components/AppContext.js
  41. 81
      packages/concordia-app/src/components/AppContext.jsx
  42. 151
      packages/concordia-app/src/components/ClearDatabasesModal/index.jsx
  43. 19
      packages/concordia-app/src/components/CoreLayoutContainer.jsx
  44. 46
      packages/concordia-app/src/components/CustomLoadingTabPane.jsx
  45. 9
      packages/concordia-app/src/components/HomeContainer.jsx
  46. 72
      packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx
  47. 163
      packages/concordia-app/src/components/InitializationScreen/index.jsx
  48. 73
      packages/concordia-app/src/components/LoadingComponent.jsx
  49. 136
      packages/concordia-app/src/components/LoadingContainer.jsx
  50. 9
      packages/concordia-app/src/components/LoadingScreen.jsx
  51. 38
      packages/concordia-app/src/components/MenuComponent.jsx
  52. 2
      packages/concordia-app/src/components/NotFound.jsx
  53. 183
      packages/concordia-app/src/components/PostCreate/index.jsx
  54. 13
      packages/concordia-app/src/components/PostCreate/styles.css
  55. 165
      packages/concordia-app/src/components/PostList/PostListRow/index.jsx
  56. 9
      packages/concordia-app/src/components/PostList/PostListRow/styles.css
  57. 72
      packages/concordia-app/src/components/PostList/index.jsx
  58. 3
      packages/concordia-app/src/components/PostList/styles.css
  59. 139
      packages/concordia-app/src/components/SignUpForm.jsx
  60. 190
      packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx
  61. 21
      packages/concordia-app/src/components/TopicList/TopicListRow/styles.css
  62. 64
      packages/concordia-app/src/components/TopicList/index.jsx
  63. 3
      packages/concordia-app/src/components/TopicList/styles.css
  64. 104
      packages/concordia-app/src/components/UsernameSelector.jsx
  65. 22
      packages/concordia-app/src/constants/ProfileTabs.js
  66. 2
      packages/concordia-app/src/constants/RegisterSteps.js
  67. 2
      packages/concordia-app/src/constants/TransactionStatus.js
  68. 7
      packages/concordia-app/src/constants/configuration/defaults.js
  69. 1
      packages/concordia-app/src/constants/contracts/ContractNames.js
  70. 13
      packages/concordia-app/src/constants/contracts/events/ForumContractEvents.js
  71. 8
      packages/concordia-app/src/constants/contracts/events/index.js
  72. 20
      packages/concordia-app/src/constants/orbit/OrbitDatabases.js
  73. 7
      packages/concordia-app/src/constants/orbit/PostsDatabaseKeys.js
  74. 7
      packages/concordia-app/src/constants/orbit/TopicsDatabaseKeys.js
  75. 9
      packages/concordia-app/src/constants/orbit/UserDatabaseKeys.js
  76. 29
      packages/concordia-app/src/index.js
  77. 18
      packages/concordia-app/src/index.jsx
  78. 25
      packages/concordia-app/src/intl/index.js
  79. 100
      packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx
  80. 21
      packages/concordia-app/src/layouts/MainLayout/index.jsx
  81. 3
      packages/concordia-app/src/layouts/MainLayout/styles.css
  82. 22
      packages/concordia-app/src/layouts/RegisterLayout/index.jsx
  83. 8
      packages/concordia-app/src/layouts/RegisterLayout/styles.css
  84. 69
      packages/concordia-app/src/options/breezeOptions.js
  85. 17
      packages/concordia-app/src/options/drizzleOptions.js
  86. 25
      packages/concordia-app/src/options/web3Options.js
  87. 23
      packages/concordia-app/src/orbit/levelUtils.js
  88. 7
      packages/concordia-app/src/orbit/orbitUtils.js
  89. 108
      packages/concordia-app/src/orbit/ΕthereumIdentityProvider.js
  90. 20
      packages/concordia-app/src/redux/actions/contractEventActions.js
  91. 2
      packages/concordia-app/src/redux/actions/peerDbReplicationActions.js
  92. 32
      packages/concordia-app/src/redux/reducers/peerDbReplicationReducer.js
  93. 38
      packages/concordia-app/src/redux/reducers/userReducer.js
  94. 13
      packages/concordia-app/src/redux/sagas/eventSaga.js
  95. 42
      packages/concordia-app/src/redux/sagas/orbitSaga.js
  96. 112
      packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js
  97. 28
      packages/concordia-app/src/redux/sagas/rootSaga.js
  98. 79
      packages/concordia-app/src/redux/sagas/userSaga.js
  99. 30
      packages/concordia-app/src/redux/store.js
  100. 22
      packages/concordia-app/src/utils/indexedDB/indexedDBUtils.js

22
.dockerignore

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

4
.gitignore

@ -21,8 +21,12 @@ yarn-error.log*
# Docker volumes # Docker volumes
docker/volumes docker/volumes
docker/ganache/volumes
docker/reports docker/reports
# Env var files
docker/env/concordia.env docker/env/concordia.env
docker/env/contracts.env
# Misc # Misc
.env.local .env.local

37
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

67
docker/Makefile

@ -0,0 +1,67 @@
PACKAGES := $(abspath ${CURDIR}/../packages)
REPORTS := $(abspath ${CURDIR}/reports)
GANACHE_VOLUMES := $(abspath ${CURDIR}/ganache/volumes)
run: compose-run build-contracts-migrate run-contracts-migrate build-app run-app
@echo "Concordia is up and running, head over to http://localhost:7777."
# Targets for building/running/stopping the blockchain and rendezvous server (using the docker-compose file)
compose-build:
@docker-compose -f ./docker-compose.yml -p concordia build
compose-run:
@docker-compose -f ./docker-compose.yml -p concordia up -d
compose-stop:
@docker-compose -f ./docker-compose.yml -p concordia down
compose-stop-clean-data:
@docker-compose -f ./docker-compose.yml -p concordia down -v
# Ganache targets
build-ganache:
@docker build ../ -f ./ganache/Dockerfile -t concordia-ganache
run-ganache:
@docker network create --driver bridge concordia_ganache_network || true &&\
docker run -d -v ${GANACHE_VOLUMES}/ganache_keys:/home/ganache_keys -p 8545:8545 --env-file=./env/ganache.docker.env --name concordia-ganache --net=concordia_ganache_network concordia-ganache:latest
run-ganache-test:
@docker network create --driver bridge concordia_ganache_test_network || true &&\
docker run --rm -d -p 8546:8546 --env-file=./env/ganache.test.docker.env --name concordia-ganache-test --net=concordia_ganache_test_network concordia-ganache:latest
# Rendezvous targets
run-rendezvous:
@docker network create --driver bridge concordia_rendezvous_network || true &&\
docker run -d -p 9090:9090 --name concordia-rendezvous libp2p/js-libp2p-webrtc-star:version-0.20.5
# Contracts targets
build-contracts:
@docker build ../ -f ./concordia-contracts/Dockerfile --target compile -t concordia-contracts --build-arg TZ=Europe/Athens
build-contracts-migrate:
@docker build ../ -f ./concordia-contracts/Dockerfile -t concordia-contracts-migrate --build-arg TZ=Europe/Athens
build-contracts-tests:
@docker build ../ -f ./concordia-contracts/Dockerfile --target test -t concordia-contracts-tests --build-arg TZ=Europe/Athens
run-contracts-tests:
@docker run --rm -v ${REPORTS}/contracts/:/usr/test-reports/ --env-file=./env/contracts.docker.env --net=concordia_ganache_test_network concordia-contracts-tests:latest
run-contracts-tests-host-chain:
@docker run --rm -v ${REPORTS}/contracts/:/usr/test-reports/ --env-file=./env/contracts.env --net=host concordia-contracts-tests:latest
run-contracts-migrate:
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/usr/src/concordia/packages/concordia-contracts/build/ --env-file=./env/contracts.docker.env --net=concordia_ganache_network concordia-contracts-migrate:latest
run-contracts-migrate-host-chain:
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/usr/src/concordia/packages/concordia-contracts/build/ --env-file=./env/contracts.env --net=host concordia-contracts-migrate:latest
get-contracts:
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/build --entrypoint=sh concordia-contracts:latest -c 'cp /usr/src/concordia/packages/concordia-contracts/build/* /build'
# App targets
build-app:
@docker build ../ -f ./concordia-app/Dockerfile -t concordia-app --build-arg TZ=Europe/Athens
build-app-tests:
@docker build ../ -f ./concordia-app/Dockerfile --target test -t concordia-app-tests --build-arg TZ=Europe/Athens
run-app-tests:
@docker run --rm -v ${REPORTS}/app/:/usr/test-reports/ --env-file=./env/concordia.docker.env concordia-app-tests:latest
run-app:
@docker create --env-file=./env/concordia.docker.env -p 7777:80 --name concordia-app --net=concordia_ganache_network concordia-app:latest &&\
docker network connect concordia_rendezvous_network concordia-app &&\
docker start concordia-app
run-app-host-chain:
@docker run -d --env-file=./env/concordia.env --name concordia-app --net=host concordia-app:latest
# Other
clean-images:
@docker rmi `docker images -q -f "dangling=true"`

204
docker/README.md

@ -0,0 +1,204 @@
# Concordia Dockerized
This page provides information about the provided docker images, their configuration and supported deployment
strategies.
TLDR: head down to [Putting it all together/Scripts](#piat-mkfile-targets) for a quick setup.
## Services
Concordia requires at the minimum two services to work, a blockchain and a rendezvous server.
Additionally, the Concordia application code must be provided to the user. Currently, the only way of distributing the
application code is via a webserver as a web application.
### Ganache
Ganache is a personal blockchain software used during development. It is a very convenient way of developing and testing
dApps. More information can be found in the project's [website](https://www.trufflesuite.com/ganache).
Note that any other Ethereum compliant blockchain can be used.
### Rendezvous
Concordia uses a distributed database to store forum data. A rendezvous server is needed in order for users to discover
peers in the network and get access to the data.
### Application
The Concordia application is a React app that handles interactions with the contracts and the distributed database used.
## Docker images
This repository provides docker images to easily setup (and destroy) instances of all required services Concordia.
Furthermore, we provide an image that builds the contracts and handles their migration to the blockchain in use.
### Ganache
The Dockerfile is provided in the path `./ganache`. The image makes use of the environment variables described
bellow.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| ACCOUNTS_NUMBER | 10 | Set the number of accounts generated |
| ACCOUNTS_ETHER | 100 | Set the amount of ETH assigned to each account |
| MNEMONIC | NaN | The mnemonic phrase sued as a seed for deterministic account generation |
| HOST | 0.0.0.0 | The hostname to listen on |
| PORT | 8545 | The port to listen on |
| NETWORK_ID | 5778 | The network id used to identify ganache |
Note that the Ganache instance running inside the container will save the generated blockchain keys in the path
`/home/ganache_keys/keys.json`. If you need to access the keys (eg for getting a private key and importing in Metamask)
you can mount a volume to this path to have easier access.
Also, the database used by Ganache for storing blockchain information is placed in the path `/home/ganache_db/`. You can
maintain the blockchain state between runs by mounting a volume to the database path. To do that, add the docker flag
`-v host/absolute/path/to/ganache_db:/home/ganache_db`.
### Rendezvous
The rendezvous server used here is `js-libp2p-webrtc-star`. The server listens on port 9090. More information can be
found on the github page of the project [here](https://github.com/libp2p/js-libp2p-webrtc-star).
### Contracts
This is a provision system that compiles and deploys the contracts to any Ethereum blockchain.
A Dockerfile is provided in the path `./concordia-contracts` that will build the contracts used by Concordia and
handle their deployment to any Ethereum network defined using env-vars upon container run. Dockerfile contains three
useful stages, described in the table bellow.
| Stage name | Entrypoint | Usage |
| --- | --- | --- |
| compile | Exits immediately | Compiles the contracts |
| test | Runs contract tests | Compiles contracts and runs tests using blockchain defined by env vars |
| runtime | Migrates contracts | Compiles contracts and migrates to the blockchain defined by env vars. Does **not** run tests |
The image makes use of the environment variables described bellow.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| MIGRATE_NETWORK | develop | Set the network where the contracts will be deployed/tested (set this to "env" unless you know what you're doing) |
| DEPLOY_CHAIN_HOST | NaN | Set the hostname of the blockchain network that will be used for deployment (requires network to be "env") |
| DEPLOY_CHAIN_PORT | NaN | Set the port of the blockchain network that will be used for deployment (requires network to be "env") |
| TEST_CHAIN_HOST | NaN | Set the hostname of the blockchain network that will be used for testing (requires network to be "env") |
| TEST_CHAIN_PORT | NaN | Set the port of the blockchain network that will be used for testing (requires network to be "env") |
You can find the contract artifacts in the directory `/usr/src/concordia/packages/concordia-contracts/build/` inside
the image.
**Attention**: make sure the targeted blockchain is up and running before trying to migrate the contracts.
### Application
The Dockerfile provided in the path `./concordia-application` builds the application for production and serves
the resulting build using an nginx server. Dockerfile contains two useful stages, described in the table bellow.
| Stage name | Entrypoint | Usage |
| --- | --- | --- |
| test | Runs tests | Fetches npm packages and runs tests |
| runtime | Serves application | Builds for production and serves it through nginx |
The image makes use of the environment variables described bellow.
| Environment variable | Default value | Usage |
| --- | --- | --- |
| REACT_APP_RENDEZVOUS_HOST | 127.0.0.1 | Set the hostname of the rendezvous server |
| REACT_APP_RENDEZVOUS_PORT | 9090 | Set the port of the rendezvous server |
**Attention**: this image will copy the contract artifacts from the directory `/packages/concordia-contracts/build`.
The image is bound the these artifacts after build. If the contracts change or get re-deployed the image must be
re-built to use the new artifacts.
**Attention**: make sure the contracts have been deployed before **building** this image. Also, make sure the rendezvous
server is up and running.
## Docker Compose
A docker-compose file also is provided. The docker-compose handles the lifecycle of the Ganache and Rendezvous server
containers.
## Putting it all together
You can find some ready to use scripts for common scenarios like dev deploys and testing in the `./docker` directory.
These scripts are documented in the following chapters.
### <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.

72
docker/concordia-app/Dockerfile

@ -0,0 +1,72 @@
# --------------------------------------------------
# Stage 1 (Init application build base)
# --------------------------------------------------
FROM node:14-buster as base
LABEL maintainers.1="Apostolos Fanakis <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 .

22
docker/concordia-app/nginx.conf

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

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

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

66
docker/concordia-contracts/Dockerfile

@ -0,0 +1,66 @@
# --------------------------------------------------
# Stage 1 (Init contracts build base)
# --------------------------------------------------
FROM node:14-alpine as base
LABEL maintainers.1="Apostolos Fanakis <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"]

6
docker/concordia-contracts/migrate.sh

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

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

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

34
docker/docker-compose.yml

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

7
docker/env/concordia.docker.env

@ -0,0 +1,7 @@
# Variables needed in runtime (in browser)
REACT_APP_RENDEZVOUS_HOST=rendezvous
REACT_APP_RENDEZVOUS_PORT=9090
# If the rendezvous server is running on host use these instead
#REACT_APP_RENDEZVOUS_HOST=127.0.0.1
#REACT_APP_RENDEZVOUS_PORT=9090

20
docker/env/concordia.example.env

@ -0,0 +1,20 @@
# Set to "CI" if in CI environment, anything else (including unset) will be ignored
BUILD_ENV={CI}
# Docker compose variables
VIRTUAL_HOST=example.com
VIRTUAL_PORT=3000
# If you uncomment the lines below, Concordia will become available through https BUT the rendezvous
# server will stop working and IPFS initialization won't complete
#LETSENCRYPT_HOST=example.com
#LETSENCRYPT_EMAIL=someemail.email.com
# Variables needed in runtime
# TO-NEVER-DO: change CONCORDIA_HOST to localhost
CONCORDIA_HOST=0.0.0.0
CONCORDIA_PORT=3000
# Variables needed in runtime (in browser)
REACT_APP_RENDEZVOUS_HOST=xx.xxx.xxx.xxx
REACT_APP_RENDEZVOUS_PORT=9090

14
docker/env/contracts.docker.env

@ -0,0 +1,14 @@
# Variables needed in runtime
MIGRATE_NETWORK=env
DEPLOY_CHAIN_HOST=concordia-ganache
DEPLOY_CHAIN_PORT=8545
TEST_CHAIN_HOST=concordia-ganache-test
TEST_CHAIN_PORT=8546
# If the blockchain is running on host use these instead
#DEPLOY_CHAIN_HOST=127.0.0.1
#DEPLOY_CHAIN_PORT=8545
#TEST_CHAIN_HOST=127.0.0.1
#TEST_CHAIN_PORT=8546

7
docker/env/contracts.example.env

@ -0,0 +1,7 @@
# Variables needed in runtime
MIGRATE_NETWORK=env
DEPLOY_CHAIN_HOST=xx.xxx.xxx.xxx
DEPLOY_CHAIN_PORT=8545
TEST_CHAIN_HOST=xx.xxx.xxx.xxx
TEST_CHAIN_PORT=8545

5
docker/env/ganache.docker.env

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

6
docker/env/ganache.test.docker.env

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

10
docker/ganache/Dockerfile

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

37
docker/ganache/start-blockchain.sh

@ -0,0 +1,37 @@
#!/bin/sh
N_ACCOUNTS="${ACCOUNTS_NUMBER:-10}"
ETHER="${ACCOUNTS_ETHER:-10}"
HOST="${HOST:-"0.0.0.0"}"
PORT="${PORT:-8545}"
ID="${NETWORK_ID:-5778}"
if [ -z "${MNEMONIC}" ]; then
echo "Starting Ganache with non deterministic address generation"
node /app/ganache-core.docker.cli.js \
--accounts "$N_ACCOUNTS" \
--defaultBalanceEther "$ETHER" \
--host "$HOST" \
--port "$PORT" \
--networkId "$ID" \
--account_keys_path "/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

11
package.json

@ -1,8 +1,13 @@
{ {
"name": "apella", "name": "concordia",
"private": true, "private": true,
"workspaces": { "workspaces": {
"packages": ["packages/*"], "packages": [
"nohoist": ["**/web3", "**/web3/**"] "packages/*"
],
"nohoist": [
"**/web3",
"**/web3/**"
]
} }
} }

0
packages/concordia-app/.dockerignore

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

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

60
packages/concordia-app/.eslintrc.js

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

22
packages/concordia-app/.gitignore

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

31
packages/concordia-app/package.json

@ -8,10 +8,8 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"postinstall": "patch-package", "postinstall": "patch-package",
"analyze": "source-map-explorer 'build/static/js/*.js'" "analyze": "react-scripts build && source-map-explorer 'build/static/js/*.js' --gzip",
}, "lint": "eslint --ext js,jsx . --format table"
"eslintConfig": {
"extends": "react-app"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -26,25 +24,38 @@
] ]
}, },
"dependencies": { "dependencies": {
"@ezerous/breeze": "~0.2.0", "@ezerous/breeze": "~0.4.0",
"@ezerous/drizzle": "~0.4.0", "@ezerous/drizzle": "~0.4.1",
"@ezerous/eth-identity-provider": "~0.1.2",
"@reduxjs/toolkit": "~1.4.0", "@reduxjs/toolkit": "~1.4.0",
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
"concordia-contracts": "~0.1.0", "concordia-contracts": "~0.1.0",
"level": "~6.0.1", "i18next": "^19.8.3",
"orbit-db-identity-provider": "~0.3.1", "i18next-browser-languagedetector": "^6.0.1",
"i18next-http-backend": "^1.0.21",
"lodash": "^4.17.20",
"prop-types": "~15.7.2", "prop-types": "~15.7.2",
"react": "~16.13.1", "react": "~16.13.1",
"react-dom": "~16.13.1", "react-dom": "~16.13.1",
"react-i18next": "^11.7.3",
"react-particles-js": "^3.4.0",
"react-redux": "~7.2.1", "react-redux": "~7.2.1",
"react-router": "~5.2.0", "react-router": "^5.2.0",
"react-router-dom": "~5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "~3.4.3", "react-scripts": "~3.4.3",
"redux-saga": "~1.1.3", "redux-saga": "~1.1.3",
"react-timeago": "~5.2.0",
"semantic-ui-css": "~2.4.1", "semantic-ui-css": "~2.4.1",
"semantic-ui-react": "~1.2.1", "semantic-ui-react": "~1.2.1",
"web3": "1.3.0" "web3": "1.3.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.1.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.2.0",
"patch-package": "~6.2.2", "patch-package": "~6.2.2",
"postinstall-postinstall": "~2.1.0", "postinstall-postinstall": "~2.1.0",
"source-map-explorer": "~2.5.0" "source-map-explorer": "~2.5.0"

1
packages/concordia-app/public/index.html

@ -19,7 +19,6 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" />
<title>Concordia</title> <title>Concordia</title>
</head> </head>
<body> <body>

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

@ -0,0 +1,81 @@
{
"board.header.no.topics.message": "There are no topics yet!",
"board.sub.header.no.topics.guest": "Sign up and be the first to post.",
"board.sub.header.no.topics.user": "Be the first to post.",
"clear.databases.modal.cancel.button": "Cancel, keep databases",
"clear.databases.modal.clear.button": "Yes, delete databases",
"clear.databases.modal.clearing.progress.message": "This might take a minute...",
"clear.databases.modal.clearing.progress.title": "Clearing all Concordia databases",
"clear.databases.modal.description.body.user": "Although this action is generally recoverable some of your topics and posts may be permanently lost.",
"clear.databases.modal.description.pre": "You are about to clear the Concordia databases stored locally in your browser.",
"clear.databases.modal.form.username.label.guest": "Please type concordia to confirm.",
"clear.databases.modal.form.username.label.user": "Please type your username to confirm.",
"clear.databases.modal.title": "Clear all Concordia databases. Are you sure?",
"custom.loading.tab.pane.default.generic.message": "Magic in the background",
"edit.information.modal.form.cancel.button": "Cancel",
"edit.information.modal.form.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.",
"edit.information.modal.form.error.message.header": "Form contains errors",
"edit.information.modal.form.location.field.label": "Location",
"edit.information.modal.form.location.field.placeholder": "Location",
"edit.information.modal.form.profile.picture.field.label": "Profile picture URL",
"edit.information.modal.form.profile.picture.field.placeholder": "URL",
"edit.information.modal.form.submit.button": "Submit",
"edit.information.modal.title": "Edit profile information",
"post.create.form.send.button": "Post",
"post.form.content.field.placeholder": "Message",
"post.form.subject.field.placeholder": "Subject",
"post.list.row.post.id": "#{{id}}",
"profile.general.tab.address.row.title": "Account address:",
"profile.general.tab.clear.databases.button.title": "Clear databases",
"profile.general.tab.edit.info.button.title": "Edit information",
"profile.general.tab.location.row.not.set": "Not set",
"profile.general.tab.location.row.title": "Location:",
"profile.general.tab.number.of.posts.row.title": "Number of posts:",
"profile.general.tab.number.of.topics.row.title": "Number of topics created:",
"profile.general.tab.posts.db.address.row.title": "PostsDB:",
"profile.general.tab.registration.date.row.title": "Member since:",
"profile.general.tab.save.info.button.title": "Save information",
"profile.general.tab.title": "General",
"profile.general.tab.topics.db.address.row.title": "TopicsDB:",
"profile.general.tab.user.db.address.row.title": "UserDB:",
"profile.general.tab.username.row.title": "Username:",
"profile.posts.tab.title": "Posts",
"profile.topics.tab.title": "Topics",
"profile.user.has.no.posts.header.message": "{{user}} has not posted yet",
"profile.user.has.no.topics.header.message": "{{user}} has created no topics yet",
"register.card.header": "Sign Up",
"register.form.button.back": "Back",
"register.form.header.already.member.message": "There is already an account for this address.\nIf you want to create another account please change your address.",
"register.form.personal.information.step.button.skip": "Skip for now",
"register.form.personal.information.step.button.submit": "Submit",
"register.form.personal.information.step.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.",
"register.form.personal.information.step.error.message.header": "Form contains errors",
"register.form.personal.information.step.location.field.label": "Location",
"register.form.personal.information.step.location.field.placeholder": "Location",
"register.form.personal.information.step.profile.picture.field.label": "Profile picture URL",
"register.form.personal.information.step.profile.picture.field.placeholder": "URL",
"register.form.profile.information.step.description": "Give a hint about who you are",
"register.form.profile.information.step.title": "Profile Information",
"register.form.sign.up.step.button.guest": "Continue as guest",
"register.form.sign.up.step.button.submit": "Sign Up",
"register.form.sign.up.step.description": "Create a Concordia account",
"register.form.sign.up.step.error.message.header": "Form contains errors",
"register.form.sign.up.step.title": "Sign Up",
"register.p.account.address": "Account address:",
"topbar.button.clear.databases": "Clear databases",
"topbar.button.create.topic": "Create topic",
"topbar.button.profile": "Profile",
"topbar.button.register": "Sign Up",
"topic.create.form.content.field.label": "First post content",
"topic.create.form.content.field.placeholder": "Message",
"topic.create.form.post.button": "Post",
"topic.create.form.subject.field.label": "Topic subject",
"topic.create.form.subject.field.placeholder": "Subject",
"topic.list.row.author": "by {{author}}",
"topic.list.row.number.of.replies": "{{numberOfReplies}} replies",
"topic.list.row.topic.id": "#{{id}}",
"username.selector.error.username.empty.message": "Username is required",
"username.selector.error.username.taken.message": "The username {{username}} is already taken.",
"username.selector.username.field.label": "Username",
"username.selector.username.field.placeholder": "Username"
}

4
packages/concordia-app/public/manifest.json

@ -1,6 +1,6 @@
{ {
"short_name": "Apella", "short_name": "Concordia",
"name": "Apella", "name": "Concordia",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

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

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

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

@ -0,0 +1,90 @@
import React, { Fragment, lazy, Suspense } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import LoadingScreen from './components/LoadingScreen';
import RegisterLayout from './layouts/RegisterLayout';
const routesConfig = [
{
exact: true,
path: '/',
component: () => <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;

3
packages/concordia-app/src/assets/css/app.css

@ -1,3 +0,0 @@
body {
margin: 1em !important;
}

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

@ -1,4 +1,17 @@
body { body.app {
margin: 10em; overflow: auto;
padding: 0; margin: 1em !important;
}
#root {
height: 100%;
}
.i18next-newlines {
white-space: pre-line !important;
}
.text-secondary {
color: gray;
font-style: italic;
} }

8
packages/concordia-app/src/assets/css/loading-component.css

@ -2,16 +2,16 @@ body {
overflow: hidden; overflow: hidden;
} }
ul {
list-style-position: inside;
}
.loading-screen { .loading-screen {
margin-top: 10em; margin-top: 10em;
text-align: center; text-align: center;
font-size: large; font-size: large;
} }
.loading-screen ul {
list-style-position: inside;
}
.loading-img { .loading-img {
margin-bottom: 3em; margin-bottom: 3em;
height: 12em; height: 12em;

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

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

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
packages/concordia-app/src/assets/images/orbitdb_logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

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

@ -0,0 +1,32 @@
const particlesOptions = {
particles: {
number: {
value: 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;

34
packages/concordia-app/src/components/App.jsx

@ -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

72
packages/concordia-app/src/components/AppContext.js

@ -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
};

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

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

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

@ -0,0 +1,151 @@
import React, {
useCallback, useMemo, useState,
useEffect,
} from 'react';
import {
Button, Form, Input, Modal,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import purgeIndexedDBs from '../../utils/indexedDB/indexedDBUtils';
const ClearDatabasesModal = (props) => {
const {
open, onDatabasesCleared, onCancel,
} = props;
const [confirmationInput, setConfirmationInput] = useState('');
const [userConfirmed, setUserConfirmed] = useState(false);
const [isClearing, setIsClearing] = useState(false);
const user = useSelector((state) => state.user);
const { t } = useTranslation();
useEffect(() => {
if (user.hasSignedUp && confirmationInput === user.username) {
setUserConfirmed(true);
} else if (!user.hasSignedUp && confirmationInput === '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;

19
packages/concordia-app/src/components/CoreLayoutContainer.jsx

@ -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
};

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

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

9
packages/concordia-app/src/components/HomeContainer.jsx

@ -1,9 +0,0 @@
import React, { Component } from 'react';
class HomeContainer extends Component {
render() {
return(<p>TODO: Home Container</p>);
}
}
export default HomeContainer;

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

@ -0,0 +1,72 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Container, Progress } from 'semantic-ui-react';
// Images
import metamaskLogo from '../../../assets/images/metamask_logo.svg';
import ethereumLogo from '../../../assets/images/ethereum_logo.svg';
import ipfsLogo from '../../../assets/images/ipfs_logo.svg';
import orbitdbLogo from '../../../assets/images/orbitdb_logo.svg';
import appLogo from '../../../assets/images/app_logo.png';
const LoadingComponent = (props) => {
useEffect(() => function cleanup() {
document.body.classList.add('app');
}, []);
const {
imageType, messageList, progressType, title, message, progress,
} = props;
let imageSrc; let imageAlt; let listItems; let indicating; let
error;
if (imageType === 'metamask') {
imageSrc = metamaskLogo;
imageAlt = 'metamask_logo';
} else if (imageType === 'ethereum') {
imageSrc = ethereumLogo;
imageAlt = 'ethereum_logo';
} else if (imageType === 'ipfs') {
imageSrc = ipfsLogo;
imageAlt = 'ipfs_logo';
} else if (imageType === 'orbit') {
imageSrc = orbitdbLogo;
imageAlt = 'orbitdb_logo';
} else if (imageType === 'app') {
imageSrc = appLogo;
imageAlt = 'app_logo';
}
if (progressType === 'indicating') indicating = true;
else if (progressType === 'error') error = true;
if (messageList) {
listItems = messageList.map((listItem) => <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;

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

@ -0,0 +1,163 @@
import React, { Children } from 'react';
import { breezeConstants } from '@ezerous/breeze';
import { useSelector } from 'react-redux';
import CustomLoader from './CustomLoader';
// CSS
import '../../assets/css/loading-component.css';
import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
const InitializationLoader = ({ children }) => {
const initializing = useSelector((state) => state.drizzleStatus.initializing);
const failed = useSelector((state) => state.drizzleStatus.failed);
const ipfsStatus = useSelector((state) => state.ipfs.status);
const orbitStatus = useSelector((state) => state.orbit.status);
const web3Status = useSelector((state) => state.web3.status);
const web3NetworkId = useSelector((state) => state.web3.networkId);
const web3NetworkFailed = useSelector((state) => state.web3.networkFailed);
const web3AccountsFailed = useSelector((state) => state.web3.accountsFailed);
const contractInitialized = useSelector((state) => state.contracts[FORUM_CONTRACT].initialized);
const contractDeployed = useSelector((state) => state.contracts[FORUM_CONTRACT].deployed);
const userFetched = useSelector((state) => state.user.address);
if (!window.ethereum) {
return (
<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;

73
packages/concordia-app/src/components/LoadingComponent.jsx

@ -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;

136
packages/concordia-app/src/components/LoadingContainer.jsx

@ -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);

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

@ -0,0 +1,9 @@
import React from 'react';
const LoadingScreen = () => (
<div>
Loading
</div>
);
export default LoadingScreen;

38
packages/concordia-app/src/components/MenuComponent.jsx

@ -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);

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

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

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

@ -0,0 +1,183 @@
import React, {
memo, useCallback, useEffect, useState,
} from 'react';
import {
Button, Feed, Form, Icon, Image, TextArea,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import determineKVAddress from '../../utils/orbitUtils';
import { POSTS_DATABASE, USER_DATABASE } from '../../constants/orbit/OrbitDatabases';
import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions';
import { USER_PROFILE_PICTURE } from '../../constants/orbit/UserDatabaseKeys';
import { breeze, drizzle } from '../../redux/store';
import './styles.css';
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus';
import { POST_CONTENT } from '../../constants/orbit/PostsDatabaseKeys';
import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
import { POST_CREATED_EVENT } from '../../constants/contracts/events/ForumContractEvents';
const { contracts: { [FORUM_CONTRACT]: { methods: { createPost } } } } = drizzle;
const { orbit } = breeze;
const PostCreate = (props) => {
const {
topicId, initialPostSubject, account,
} = props;
const transactionStack = useSelector((state) => state.transactionStack);
const transactions = useSelector((state) => state.transactions);
const [postContent, setPostContent] = useState('');
const [userProfilePictureUrl, setUserProfilePictureUrl] = useState();
const [createPostCacheSendStackId, setCreatePostCacheSendStackId] = useState('');
const [posting, setPosting] = useState(false);
const [storingPost, setStoringPost] = useState(false);
const userAddress = useSelector((state) => state.user.address);
const users = useSelector((state) => state.orbitData.users);
const dispatch = useDispatch();
const { t } = useTranslation();
useEffect(() => {
if (userAddress) {
determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress })
.then((userOrbitAddress) => {
const userFound = users
.find((user) => user.id === userOrbitAddress);
if (userFound) {
setUserProfilePictureUrl(userFound[USER_PROFILE_PICTURE]);
} else {
dispatch({
type: FETCH_USER_DATABASE,
orbit,
dbName: USER_DATABASE,
userAddress,
});
}
})
.catch((error) => {
console.error('Error during determination of key-value DB address:', error);
});
}
}, [dispatch, userAddress, users]);
const handleInputChange = useCallback((event) => {
if (posting) {
return;
}
switch (event.target.name) {
case 'postContent':
setPostContent(event.target.value);
break;
default:
break;
}
}, [posting]);
useEffect(() => {
if (posting && !storingPost && transactionStack && transactionStack[createPostCacheSendStackId]
&& transactions[transactionStack[createPostCacheSendStackId]]) {
if (transactions[transactionStack[createPostCacheSendStackId]].status === TRANSACTION_ERROR) {
setPosting(false);
} else if (transactions[transactionStack[createPostCacheSendStackId]].status === TRANSACTION_SUCCESS) {
const {
receipt: { events: { [POST_CREATED_EVENT]: { returnValues: { postID: contractPostId } } } },
} = transactions[transactionStack[createPostCacheSendStackId]];
const { stores } = orbit;
const postsDb = Object.values(stores).find((store) => store.dbname === POSTS_DATABASE);
postsDb
.put(contractPostId, {
[POST_CONTENT]: postContent,
}, { pin: true })
.then(() => {
setPostContent('');
setPosting(false);
setStoringPost(false);
setCreatePostCacheSendStackId('');
})
.catch((reason) => {
console.log(reason);
});
setStoringPost(true);
}
}
}, [
createPostCacheSendStackId, initialPostSubject, postContent, posting, storingPost, transactionStack,
transactions,
]);
const savePost = useCallback(() => {
if (postContent === '') {
return;
}
setPosting(true);
setCreatePostCacheSendStackId(createPost.cacheSend(...[topicId], { from: account }));
}, [account, postContent, topicId]);
return (
<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);

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

@ -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;
}

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

@ -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);

9
packages/concordia-app/src/components/PostList/PostListRow/styles.css

@ -0,0 +1,9 @@
.post-profile-picture {
margin: 5px 0 0 0;
}
.post-summary-meta-index {
float: right;
font-size: 12px;
opacity: 0.4;
}

72
packages/concordia-app/src/components/PostList/index.jsx

@ -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;

3
packages/concordia-app/src/components/PostList/styles.css

@ -0,0 +1,3 @@
#post-list{
height: 100%;
}

139
packages/concordia-app/src/components/SignUpForm.jsx

@ -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);

190
packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx

@ -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 })}
,&nbsp;
<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);

21
packages/concordia-app/src/components/TopicList/TopicListRow/styles.css

@ -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;
}

64
packages/concordia-app/src/components/TopicList/index.jsx

@ -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;

3
packages/concordia-app/src/components/TopicList/styles.css

@ -0,0 +1,3 @@
#topic-list{
height: 100%;
}

104
packages/concordia-app/src/components/UsernameSelector.jsx

@ -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;

22
packages/concordia-app/src/constants/ProfileTabs.js

@ -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;

2
packages/concordia-app/src/constants/RegisterSteps.js

@ -0,0 +1,2 @@
export const REGISTER_STEP_SIGNUP = 'signup';
export const REGISTER_STEP_PROFILE_INFORMATION = 'profile-information';

2
packages/concordia-app/src/constants/TransactionStatus.js

@ -0,0 +1,2 @@
export const TRANSACTION_SUCCESS = 'success';
export const TRANSACTION_ERROR = 'error';

7
packages/concordia-app/src/constants/configuration/defaults.js

@ -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';

1
packages/concordia-app/src/constants/contracts/ContractNames.js

@ -0,0 +1 @@
export const FORUM_CONTRACT = 'Forum';

13
packages/concordia-app/src/constants/contracts/events/ForumContractEvents.js

@ -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;

8
packages/concordia-app/src/constants/contracts/events/index.js

@ -0,0 +1,8 @@
import { FORUM_CONTRACT } from '../ContractNames';
import forumContractEvents from './ForumContractEvents';
const appEvents = {
[FORUM_CONTRACT]: forumContractEvents,
};
export default appEvents;

20
packages/concordia-app/src/constants/orbit/OrbitDatabases.js

@ -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;

7
packages/concordia-app/src/constants/orbit/PostsDatabaseKeys.js

@ -0,0 +1,7 @@
export const POST_CONTENT = 'content';
const postsDatabaseKeys = [
POST_CONTENT,
];
export default postsDatabaseKeys;

7
packages/concordia-app/src/constants/orbit/TopicsDatabaseKeys.js

@ -0,0 +1,7 @@
export const TOPIC_SUBJECT = 'subject';
const topicsDatabaseKeys = [
TOPIC_SUBJECT,
];
export default topicsDatabaseKeys;

9
packages/concordia-app/src/constants/orbit/UserDatabaseKeys.js

@ -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;

29
packages/concordia-app/src/index.js

@ -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

18
packages/concordia-app/src/index.jsx

@ -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

25
packages/concordia-app/src/intl/index.js

@ -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;

100
packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx

@ -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;

21
packages/concordia-app/src/layouts/MainLayout/index.jsx

@ -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;

3
packages/concordia-app/src/layouts/MainLayout/styles.css

@ -0,0 +1,3 @@
#main-layout {
height: 100%;
}

22
packages/concordia-app/src/layouts/RegisterLayout/index.jsx

@ -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;

8
packages/concordia-app/src/layouts/RegisterLayout/styles.css

@ -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;
}

69
packages/concordia-app/src/options/breezeOptions.js

@ -1,45 +1,40 @@
import web3Options from './web3Options'; import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider';
import EthereumIdentityProvider from '../orbit/ΕthereumIdentityProvider'; import databases from '../constants/orbit/OrbitDatabases';
import { orbitConstants } from '@ezerous/breeze' import {
REACT_APP_RENDEZVOUS_HOST_DEFAULT,
REACT_APP_RENDEZVOUS_PORT_DEFAULT,
} from '../constants/configuration/defaults';
const { web3 } = web3Options; const REACT_APP_RENDEZVOUS_HOST = process.env.REACT_APP_RENDEZVOUS_HOST || REACT_APP_RENDEZVOUS_HOST_DEFAULT;
EthereumIdentityProvider.setWeb3(web3); const REACT_APP_RENDEZVOUS_PORT = process.env.REACT_APP_RENDEZVOUS_PORT || REACT_APP_RENDEZVOUS_PORT_DEFAULT;
const breezeOptions = { const breezeOptions = {
ipfs: { ipfs: {
config: { repo: 'concordia',
Addresses: { config: {
Swarm: [ Addresses: {
// Use local signaling server (see also rendezvous script in package.json) Swarm: [
// For more information: https://github.com/libp2p/js-libp2p-webrtc-star // Use local signaling server (see also rendezvous script in package.json)
'/ip4/127.0.0.1/tcp/9090/wss/p2p-webrtc-star' // For more information: https://github.com/libp2p/js-libp2p-webrtc-star
`/ip4/${REACT_APP_RENDEZVOUS_HOST}/tcp/${REACT_APP_RENDEZVOUS_PORT}/wss/p2p-webrtc-star`,
// Use the following public servers if needed // Use the following public servers if needed
// '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star', // '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
// '/dns4/ wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star' // '/dns4/ wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
] ],
}, },
},
preload: {
enabled: false
},
init: {
emptyRepo: true
}
}, },
orbit: { preload: {
identityProvider: EthereumIdentityProvider, enabled: false,
databases: [ },
{ init: {
name: 'topics', emptyRepo: true,
type: orbitConstants.ORBIT_TYPE_KEYVALUE },
}, },
{ orbit: {
name: 'posts', identityProvider: EthereumContractIdentityProvider,
type: orbitConstants.ORBIT_TYPE_KEYVALUE databases,
} },
]
}
}; };
export default breezeOptions; export default breezeOptions;

17
packages/concordia-app/src/options/drizzleOptions.js

@ -1,17 +1,14 @@
// See also: https://truffleframework.com/docs/drizzle/reference/drizzle-options // Check out the documentation: https://truffleframework.com/docs/drizzle/reference/drizzle-options
import { contracts } from 'concordia-contracts'; import { contracts } from 'concordia-contracts';
import web3Options from './web3Options'; import web3Options from './web3Options';
import appEvents from '../constants/contracts/events';
const drizzleOptions = { const drizzleOptions = {
web3: { web3: web3Options,
customProvider: web3Options.web3 contracts,
}, events: { ...appEvents },
contracts, reloadWindowOnNetworkChange: true,
events: { reloadWindowOnAccountChange: true, // We need it to reinitialize breeze and create new Orbit databases
Forum: ['UserSignedUp', 'UsernameUpdated', 'TopicCreated', 'PostCreated']
},
reloadWindowOnNetworkChange: true,
reloadWindowOnAccountChange: true // We need it to reinitialize breeze and create new Orbit databases
}; };
export default drizzleOptions; export default drizzleOptions;

25
packages/concordia-app/src/options/web3Options.js

@ -1,14 +1,29 @@
import Web3 from 'web3'; import Web3 from 'web3';
import EthereumIdentityProvider from '../orbit/ΕthereumIdentityProvider'; import {
WEB3_HOST_DEFAULT,
WEB3_PORT_DEFAULT,
WEB3_PORT_SOCKET_CONNECT_MAX_ATTEMPTS_DEFAULT,
WEB3_PORT_SOCKET_TIMEOUT_DEFAULT,
} from '../constants/configuration/defaults';
const { WEB3_URL, WEB3_PORT } = process.env; const { WEB3_HOST, WEB3_PORT, WEBSOCKET_TIMEOUT } = process.env;
const web3 = new Web3(Web3.givenProvider || `ws://${WEB3_URL}:${WEB3_PORT}`); const web3WebsocketOptions = {
keepAlive: true,
timeout: WEBSOCKET_TIMEOUT !== undefined ? WEBSOCKET_TIMEOUT : WEB3_PORT_SOCKET_TIMEOUT_DEFAULT,
reconnect: {
maxAttempts: WEB3_PORT_SOCKET_CONNECT_MAX_ATTEMPTS_DEFAULT,
},
};
EthereumIdentityProvider.setWeb3(web3); const web3 = (WEB3_HOST !== undefined && WEB3_PORT !== undefined)
? new Web3.providers.WebsocketProvider(`ws://${WEB3_HOST}:${WEB3_PORT}`)
: new Web3(Web3.givenProvider || new Web3.providers.WebsocketProvider(
`ws://${WEB3_HOST_DEFAULT}:${WEB3_PORT_DEFAULT}`, web3WebsocketOptions,
));
const web3Options = { const web3Options = {
web3 customProvider: web3,
}; };
export default web3Options; export default web3Options;

23
packages/concordia-app/src/orbit/levelUtils.js

@ -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 };

7
packages/concordia-app/src/orbit/orbitUtils.js

@ -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}`;
}

108
packages/concordia-app/src/orbit/ΕthereumIdentityProvider.js

@ -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;

20
packages/concordia-app/src/redux/actions/contractEventActions.js

@ -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;

2
packages/concordia-app/src/redux/actions/peerDbReplicationActions.js

@ -0,0 +1,2 @@
export const FETCH_USER_DATABASE = 'FETCH_USER_DATABASE';
export const UPDATE_ORBIT_DATA = 'UPDATE_ORBIT_DATA';

32
packages/concordia-app/src/redux/reducers/peerDbReplicationReducer.js

@ -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;

38
packages/concordia-app/src/redux/reducers/userReducer.js

@ -1,31 +1,31 @@
import { USER_DATA_UPDATED } from '../actions/userActions'; import { USER_DATA_UPDATED } from '../actions/userActions';
const initialState = { const initialState = {
username: '', username: '',
address: null, address: null,
hasSignedUp: false, hasSignedUp: false,
}; };
const userReducer = (state = initialState, action) => { const userReducer = (state = initialState, action) => {
const { type } = action; const { type } = action;
if(type === USER_DATA_UPDATED) { if (type === USER_DATA_UPDATED) {
const { address, username } = action; const { address, username } = action;
if(username){ if (username) {
return { return {
username: username, username,
address: address, address,
hasSignedUp: true, hasSignedUp: true,
}; };
}
return {
username: '',
address,
hasSignedUp: false,
};
} }
return {
username: '',
address,
hasSignedUp: false,
};
}
return state; return state;
}; };
export default userReducer; export default userReducer;

13
packages/concordia-app/src/redux/sagas/eventSaga.js

@ -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;

42
packages/concordia-app/src/redux/sagas/orbitSaga.js

@ -1,22 +1,34 @@
import { put, all, take } from 'redux-saga/effects' import {
call, put, all, take,
} from 'redux-saga/effects';
import { breezeActions } from '@ezerous/breeze' import { breezeActions } from '@ezerous/breeze';
import { drizzleActions } from '@ezerous/drizzle' import { drizzleActions } from '@ezerous/drizzle';
function * initOrbitDatabases (action) { import { forumContract } from 'concordia-contracts';
const { account, breeze} = action; import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider';
yield put(breezeActions.orbit.orbitInit(breeze, account)); //same as breeze.initOrbit(account);
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([ const res = yield all([
take(drizzleActions.drizzle.DRIZZLE_INITIALIZED), take(drizzleActions.drizzle.DRIZZLE_INITIALIZED),
take(breezeActions.breeze.BREEZE_INITIALIZED), take(breezeActions.breeze.BREEZE_INITIALIZED),
take(drizzleActions.account.ACCOUNTS_FETCHED) take(drizzleActions.account.ACCOUNTS_FETCHED),
]); ]);
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;

112
packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js

@ -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;

28
packages/concordia-app/src/redux/sagas/rootSaga.js

@ -1,17 +1,21 @@
import { all, fork } from 'redux-saga/effects'; import { all, fork } from 'redux-saga/effects';
import { drizzleSagas } from '@ezerous/drizzle'; import { drizzleSagas } from '@ezerous/drizzle';
import { breezeSagas } from '@ezerous/breeze' import { breezeSagas } from '@ezerous/breeze';
import orbitSaga from './orbitSaga' import orbitSaga from './orbitSaga';
import userSaga from './userSaga' import userSaga from './userSaga';
import peerDbReplicationSaga from './peerDbReplicationSaga';
import eventSaga from './eventSaga';
export default function* root() { export default function* root() {
const sagas = [ const sagas = [
...drizzleSagas, ...drizzleSagas,
...breezeSagas, ...breezeSagas,
orbitSaga, eventSaga,
userSaga orbitSaga,
]; userSaga,
yield all( peerDbReplicationSaga,
sagas.map((saga) => fork(saga)), ];
); yield all(
sagas.map((saga) => fork(saga)),
);
} }

79
packages/concordia-app/src/redux/sagas/userSaga.js

@ -1,42 +1,55 @@
import { all, call, put, take } from 'redux-saga/effects'; /* eslint-disable no-console */
import {
all, call, put, take, takeLatest,
} from 'redux-saga/effects';
import { drizzleActions } from '@ezerous/drizzle'; import { drizzleActions } from '@ezerous/drizzle';
import { USER_DATA_UPDATED, USER_DATA_ERROR } from '../actions/userActions'; import { USER_DATA_UPDATED, USER_DATA_ERROR } from '../actions/userActions';
import { FORUM_EVENT_USER_SIGNED_UP } from '../actions/contractEventActions';
function * fetchUserData ({drizzle, account}) { import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
const contract = drizzle.contracts['Forum'];
const transaction = yield call(contract.methods.hasUserSignedUp, account); function* fetchUserData({ drizzle, account }) {
const contract = drizzle.contracts[FORUM_CONTRACT];
try { const transaction = yield call(contract.methods.hasUserSignedUp, account);
const dispatchArgs = { address: account };
const callResult = yield call(transaction.call, { address: account }); try {
const dispatchArgs = { address: account };
// User has signed up, fetch his username const callResult = yield call(transaction.call, { address: account });
if (callResult) {
const txObj2 = yield call(contract.methods.getUsername, account); // User has signed up, fetch his username
dispatchArgs.username = yield call(txObj2.call, { if (callResult) {
address: account const txObj2 = yield call(contract.methods.getUsername, account);
}); dispatchArgs.username = yield call(txObj2.call, {
} address: account,
});
yield put({
type: USER_DATA_UPDATED, ...dispatchArgs
});
} catch (error) {
console.error(error);
yield put({ type: USER_DATA_ERROR });
} }
yield put({
type: USER_DATA_UPDATED, ...dispatchArgs,
});
} catch (error) {
console.error(error);
yield put({ type: USER_DATA_ERROR });
}
}
function* userHasSignedUp({ event }) {
yield put({
type: USER_DATA_UPDATED,
...{
address: event.returnValues.userAddress, username: event.returnValues.username,
},
});
} }
function* userSaga() {
const res = yield all([
take(drizzleActions.drizzle.DRIZZLE_INITIALIZED),
take(drizzleActions.account.ACCOUNTS_FETCHED),
]);
function * userSaga () { yield fetchUserData({ drizzle: res[0].drizzle, account: res[1].accounts[0] });
const res = yield all([
take(drizzleActions.drizzle.DRIZZLE_INITIALIZED),
take(drizzleActions.account.ACCOUNTS_FETCHED)
]);
yield fetchUserData({drizzle:res[0].drizzle, account: res[1].accounts[0]}); yield takeLatest(FORUM_EVENT_USER_SIGNED_UP, userHasSignedUp);
} }
export default userSaga export default userSaga;

30
packages/concordia-app/src/redux/store.js

@ -1,24 +1,34 @@
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { drizzleReducers, drizzleMiddlewares, generateContractsInitialState } from '@ezerous/drizzle'; import {
import { breezeReducers } from '@ezerous/breeze' drizzleReducers, drizzleMiddlewares, generateContractsInitialState, Drizzle,
import userReducer from './reducers/userReducer' } from '@ezerous/drizzle';
import { Breeze, breezeReducers } from '@ezerous/breeze';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import userReducer from './reducers/userReducer';
import rootSaga from './sagas/rootSaga'; import rootSaga from './sagas/rootSaga';
import drizzleOptions from '../options/drizzleOptions'; import drizzleOptions from '../options/drizzleOptions';
import peerDbReplicationReducer from './reducers/peerDbReplicationReducer';
import breezeOptions from '../options/breezeOptions';
const initialState = { const initialState = {
contracts: generateContractsInitialState(drizzleOptions), contracts: generateContractsInitialState(drizzleOptions),
}; };
const sagaMiddleware = createSagaMiddleware(); const sagaMiddleware = createSagaMiddleware();
const store = configureStore({ const store = configureStore({
reducer: {...drizzleReducers, ...breezeReducers, user: userReducer }, reducer: {
middleware: getDefaultMiddleware({ ...drizzleReducers, ...breezeReducers, user: userReducer, orbitData: peerDbReplicationReducer,
serializableCheck: false, //https://redux.js.org/style-guide/style-guide/#do-not-put-non-serializable-values-in-state-or-actions },
}).concat(drizzleMiddlewares).concat(sagaMiddleware), middleware: getDefaultMiddleware({
preloadedState: initialState // https://redux.js.org/style-guide/style-guide/#do-not-put-non-serializable-values-in-state-or-actions
}) serializableCheck: false,
}).concat(drizzleMiddlewares).concat(sagaMiddleware),
preloadedState: initialState,
});
export const drizzle = new Drizzle(drizzleOptions, store);
export const breeze = new Breeze(breezeOptions, store);
sagaMiddleware.run(rootSaga); sagaMiddleware.run(rootSaga);
export default store; export default store;

22
packages/concordia-app/src/utils/indexedDB/indexedDBUtils.js

@ -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;

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

Loading…
Cancel
Save