diff --git a/.dockerignore b/.dockerignore index 6629ae8a684..02ef9c61713 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ **/node_modules **/docker-compose.yml **/docker-compose.production.yml +stats.json \ No newline at end of file diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml new file mode 100644 index 00000000000..48fa5a33347 --- /dev/null +++ b/.github/workflows/desktop.yml @@ -0,0 +1,113 @@ +name: Desktop-release + +on: + push: + branches: + - desktop + +jobs: + release-mac: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [macos-11] + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Install Node.js, NPM and Yarn + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: npm install, lint and/or test + run: | + yarn + cd src/desktop + yarn + + - name: Build/release Electron app + uses: samuelmeuli/action-electron-builder@v1 + with: + # GitHub token, automatically provided to the action + # (No need to define this secret in the repo settings) + github_token: ${{ secrets.github_token }} + package_root: "./src/desktop/" + mac_certs: ${{ secrets.mac_certs }} + mac_certs_password: ${{ secrets.mac_certs_password }} + args: "--publish always" + max_attempts: "5" + env: + NODE_OPTIONS: '--max_old_space_size=4096' + + release-win: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [windows-latest] + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Install Node.js, NPM and Yarn + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: npm install, lint and/or test + run: | + yarn + cd src/desktop + yarn + + - name: Build/release Electron app + uses: samuelmeuli/action-electron-builder@v1 + with: + # GitHub token, automatically provided to the action + # (No need to define this secret in the repo settings) + github_token: ${{ secrets.github_token }} + package_root: "./src/desktop/" + windows_certs: ${{ secrets.windows_certs }} + windows_certs_password: ${{ secrets.windows_certs_password }} + args: "--publish always" + max_attempts: "5" + env: + NODE_OPTIONS: '--max_old_space_size=4096' + + release-unix: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Install Node.js, NPM and Yarn + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: npm install, lint and/or test + run: | + yarn + cd src/desktop + yarn + + - name: Build/release Electron app + uses: samuelmeuli/action-electron-builder@v1 + with: + # GitHub token, automatically provided to the action + # (No need to define this secret in the repo settings) + github_token: ${{ secrets.github_token }} + package_root: "./src/desktop/" + args: "--publish always" + max_attempts: "5" + env: + NODE_OPTIONS: '--max_old_space_size=4096' diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 77bbb2c7d9a..bac3987ea0d 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -8,11 +8,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x] + node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: npm install, lint and/or test @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check Out Repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Login to Docker Hub uses: docker/login-action@v1 @@ -57,18 +57,20 @@ jobs: uses: appleboy/ssh-action@master env: USE_PRIVATE: ${{secrets.USE_PRIVATE}} + REDIS_HOST_PASSWORD: ${{secrets.REDIS_HOST_PASSWORD}} with: host: ${{ secrets.SSH_HOST_EU }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} port: ${{ secrets.SSH_PORT }} - envs: USE_PRIVATE + envs: USE_PRIVATE, REDIS_HOST_PASSWORD script: | export USE_PRIVATE=$USE_PRIVATE + export REDIS_HOST_PASSWORD=$REDIS_HOST_PASSWORD cd ~/vision-production git pull origin master docker pull ecency/vision:latest - docker stack deploy -c docker-compose.yml -c docker-compose.production.yml vis + docker-compose -f docker-compose.production.yml up -d deploy-US: needs: build @@ -78,18 +80,20 @@ jobs: uses: appleboy/ssh-action@master env: USE_PRIVATE: ${{secrets.USE_PRIVATE}} + REDIS_HOST_PASSWORD: ${{secrets.REDIS_HOST_PASSWORD}} with: host: ${{ secrets.SSH_HOST_US }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} port: ${{ secrets.SSH_PORT }} - envs: USE_PRIVATE + envs: USE_PRIVATE, REDIS_HOST_PASSWORD script: | export USE_PRIVATE=$USE_PRIVATE + export REDIS_HOST_PASSWORD=$REDIS_HOST_PASSWORD cd ~/vision-production git pull origin master docker pull ecency/vision:latest - docker stack deploy -c docker-compose.yml -c docker-compose.production.yml vision + docker-compose -f docker-compose.production.yml up -d deploy-SG: needs: build @@ -99,15 +103,17 @@ jobs: uses: appleboy/ssh-action@master env: USE_PRIVATE: ${{secrets.USE_PRIVATE}} + REDIS_HOST_PASSWORD: ${{secrets.REDIS_HOST_PASSWORD}} with: host: ${{ secrets.SSH_HOST_SG }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} port: ${{ secrets.SSH_PORT }} - envs: USE_PRIVATE + envs: USE_PRIVATE, REDIS_HOST_PASSWORD script: | export USE_PRIVATE=$USE_PRIVATE + export REDIS_HOST_PASSWORD=$REDIS_HOST_PASSWORD cd ~/vision-production git pull origin master docker pull ecency/vision:latest - docker stack deploy -c docker-compose.yml -c docker-compose.production.yml vision + docker-compose -f docker-compose.production.yml up -d diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 6b8f39ffece..fb398adfbc5 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -8,11 +8,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x] + node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: npm install, lint and/or test @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check Out Repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Login to Docker Hub uses: docker/login-action@v1 @@ -57,15 +57,17 @@ jobs: uses: appleboy/ssh-action@master env: USE_PRIVATE: ${{secrets.USE_PRIVATE}} + REDIS_HOST_PASSWORD: ${{secrets.REDIS_HOST_PASSWORD}} with: host: ${{ secrets.SSH_STAGING_HOST }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} port: ${{ secrets.SSH_PORT }} - envs: USE_PRIVATE + envs: USE_PRIVATE, REDIS_HOST_PASSWORD script: | export USE_PRIVATE=$USE_PRIVATE + export REDIS_HOST_PASSWORD=$REDIS_HOST_PASSWORD cd ~/vision-staging git pull origin development docker pull ecency/vision:development - docker stack deploy -c docker-compose.yml vision + docker-compose up -d diff --git a/.gitignore b/.gitignore index e1e8fc6b9ec..13c3e8cedb9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ src/config.ts .vscode dev.sh +stats.json \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000000..be539834c53 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn precommit diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000000..06b2bf71485 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "printWidth": 100, + "singleQuote": false, + "trailingComma": "none", + "tabWidth": 2 +} diff --git a/Dockerfile b/Dockerfile index f066bbae6be..4bb3a34f2cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12.16.2 as base +FROM node:16.13.2 as base WORKDIR /var/app @@ -18,7 +18,7 @@ FROM base as dependencies RUN yarn install --non-interactive --frozen-lockfile --ignore-optional --production ### BUILD MINIFIED PRODUCTION ## -FROM node:12.16.2-alpine as production +FROM node:16.13.2 as production # Add Tini ENV TINI_VERSION v0.18.0 diff --git a/README.md b/README.md index ba26c72a3f9..4adf6cc61ab 100644 --- a/README.md +++ b/README.md @@ -32,25 +32,49 @@ Feel free to test it out and submit improvements and pull requests. - node ^12.0.0 - yarn -##### Clone +##### Clone + `$ git clone https://github.com/ecency/ecency-vision` `$ cd ecency-vision` ##### Install dependencies + `$ yarn` ##### Edit config file or define environment variables + `$ nano src/config.ts` ##### Environment variables -* `USE_PRIVATE` - if instance has private api address and auth (0 or 1 value) +- `USE_PRIVATE` - if instance has private api address and auth (0 or 1 value) +- `HIVESIGNER_ID` - This is a special application Hive account. If unset, "ecency.app" is the account used. +- `HIVESIGNER_SECRET` - This is a secret your site shares with HIVE_SIGNER in order to communicate securely. + +* `REDIS_URL` - support for caching amp pages + +###### Hivesigner Variables + +When setting up another service like Ecency with Ecency-vision software: + +1. You may leave `HIVESIGNER_ID` and `HIVESIGNER_SECRET` environment variables unset and optionally set USE_PRIVATE=1 and leave "base" in the constants/defaults.json set to "https://ecency.com". Your new site will contain more features as it will use Ecency's private API. This is by far the easiest option. +2. You may change `base` to the URL of your own site, but you will have to set environment variables `HIVESIGNER_ID` and `HIVESIGNER_SECRET`; set USE_PRIVATE=0 as well as configure your the `HIVESIGNER_ID` account at the [Hivesigner website.](https://hivesigner.com/profile). Hivesigner will need a `secret`, in the form of a long lowercase hexadecimal number. The HIVESIGNER_SECRET should be set to this value. + +###### Hivesigner Login Process + +In order to validate a login, and do posting level operations, this software relies on Hivesigner. A user @alice will use login credentials to login to the site via one of several methods, but the site will communicate with Hivesigner and ask it to do all posting operations on behalf of @alice. Hivesigner can and will do this because both @alice will have given posting authority to the `HIVESIGNER_ID` user and the `HIVESIGNER_ID` user will have given its posting authority to Hivesigner. + +##### Edit "default" values + +If you are setting up your own website other than Ecency.com, you can still leave the value `base` as "https://ecency.com". However, you should change `name`, `title` and `twitterHandle`. There are also a lot of static pages that are Ecency specific. ##### Start website in dev + `$ yarn start` ##### Start desktop in dev + `$ cd src/desktop` `$ yarn` `$ yarn dev` @@ -71,7 +95,9 @@ docker run -it --rm -p 3000:3000 ecency/vision:latest ``` Configure the instance using following environment variables: - * `USE_PRIVATE` + +- `USE_PRIVATE` +- `REDIS_URL` ```bash docker run -it --rm -p 3000:3000 -e USE_PRIVATE=1 ecency/vision:latest @@ -89,6 +115,13 @@ docker stack deploy -c docker-compose.yml -c docker-compose.production.yml visio [![Contributors](https://contrib.rocks/image?repo=ecency/ecency-vision)](https://github.com/ecency/ecency-vision/graphs/contributors) +## Note to DEVS + +- Make PRs more clear with description, screenshots or videos, linking to issues, if no issue exist create one that describes PR and mention in PR. Reviewers may or may not run code, but PR should be reviewable even without running, visials helps there. +- PR should have title WIP, if it is not ready yet. Once ready, run yarn test and update all tests, make sure linting also done before requesting for review. +- Creating component?! Make sure to create simple tests, you can check other components for examples. +- Always make sure component and pages stay fast without unnecessary re-renders because those will slow down app/performance. +- ## Issues @@ -98,7 +131,7 @@ If you find a security issue please report details to: security@ecency.com We will evaluate the risk and make a patch available before filing the issue. -[//]: # 'LINKS' +[//]: # "LINKS" [ecency_vision]: https://ecency.com [ecency_desktop]: https://desktop.ecency.com [ecency_alpha]: https://alpha.ecency.com diff --git a/docker-compose.production.yml b/docker-compose.production.yml index b577a3b2b7f..c6e5ccdd110 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -1,5 +1,77 @@ -version: '3.7' +version: "3.7" services: - app: + redis: + image: redis:alpine + environment: + - REDIS_HOST_PASSWORD + restart: always + ports: + - "6379:6379" + hostname: redis + command: + - /bin/sh + - -c + - redis-server --save 20 1 --loglevel warning --requirepass "$${REDIS_HOST_PASSWORD:?REDIS_HOST_PASSWORD variable is not set}" + volumes: + - redis:/data + + web1: + image: ecency/vision:latest + environment: + - USE_PRIVATE + - REDIS_HOST_PASSWORD + restart: always + ports: + - "3001:3000" + hostname: web1 + depends_on: + - redis + + web2: + image: ecency/vision:latest + environment: + - USE_PRIVATE + - REDIS_HOST_PASSWORD + restart: always + ports: + - "3002:3000" + hostname: web2 + depends_on: + - redis + + web3: image: ecency/vision:latest + environment: + - USE_PRIVATE + - REDIS_HOST_PASSWORD + restart: always + ports: + - "3003:3000" + hostname: web3 + depends_on: + - redis + + web4: + image: ecency/vision:latest + environment: + - USE_PRIVATE + - REDIS_HOST_PASSWORD + restart: always + ports: + - "3004:3000" + hostname: web4 + depends_on: + - redis + + nginx: + build: ./nginx + ports: + - '3000:80' + depends_on: + - web1 + - web2 + +volumes: + redis: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml index 2a491e6bebf..648b67570ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,28 +1,77 @@ -version: '3.7' +version: "3.7" services: - app: + redis: + image: redis:alpine + environment: + - REDIS_HOST_PASSWORD + restart: always + ports: + - "6379:6379" + hostname: redis + command: + - /bin/sh + - -c + - redis-server --save 20 1 --loglevel warning --requirepass "$${REDIS_HOST_PASSWORD:?REDIS_HOST_PASSWORD variable is not set}" + volumes: + - redis:/data + + web1: image: ecency/vision:development environment: - USE_PRIVATE + - REDIS_HOST_PASSWORD + restart: always ports: - - "3000:3000" - deploy: - replicas: 4 - resources: - limits: - cpus: "0.9" - memory: 2048M - update_config: - parallelism: 4 - order: start-first - failure_action: rollback - delay: 10s - rollback_config: - parallelism: 0 - order: stop-first - restart_policy: - condition: any - delay: 5s - max_attempts: 5 - window: 30s + - "3001:3000" + hostname: web1 + depends_on: + - redis + + web2: + image: ecency/vision:development + environment: + - USE_PRIVATE + - REDIS_HOST_PASSWORD + restart: always + ports: + - "3002:3000" + hostname: web2 + depends_on: + - redis + + web3: + image: ecency/vision:development + environment: + - USE_PRIVATE + - REDIS_HOST_PASSWORD + restart: always + ports: + - "3003:3000" + hostname: web3 + depends_on: + - redis + + web4: + image: ecency/vision:development + environment: + - USE_PRIVATE + - REDIS_HOST_PASSWORD + restart: always + ports: + - "3004:3000" + hostname: web4 + depends_on: + - redis + + nginx: + build: ./nginx + ports: + - '3000:80' + depends_on: + - web1 + - web2 + +volumes: + redis: + driver: local diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000000..cf813e2a586 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:1.21.6 +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 00000000000..109d9085e69 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,14 @@ +upstream loadbalancer { + server web1:3000; + server web2:3000; + server web3:3000; + server web4:3000; +} + +server { + listen 80; + server_name localhost; + location / { + proxy_pass http://loadbalancer; + } +} diff --git a/package.json b/package.json index fca69c46cd3..074981a42a5 100644 --- a/package.json +++ b/package.json @@ -1,111 +1,167 @@ { "name": "ecency-vision", - "version": "3.0.22", + "version": "3.0.35", "private": true, "license": "MIT", "scripts": { "start": "razzle start", "build": "razzle build --noninteractive", "test": "TZ=UTC razzle test --env=jsdom", + "prepare": "husky install", + "format": "prettier --ignore-unknown --write './src/**/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc", + "precommit": "lint-staged", "start:prod": "NODE_ENV=production node build/server.js" }, "dependencies": { - "@ecency/render-helper": "^2.2.16", - "@hiveio/dhive": "^1.1.0", + "@ecency/render-helper": "^2.2.24", + "@ecency/render-helper-amp": "^1.1.0", + "@firebase/analytics": "^0.8.0", + "@firebase/app": "^0.7.28", + "@firebase/messaging": "^0.9.16", + "@hiveio/dhive": "^1.2.5", "@hiveio/hivescript": "^1.2.7", - "@types/bs58": "^4.0.1", - "@types/diff-match-patch": "^1.0.32", - "@types/lodash": "^4.14.175", - "@types/numeral": "^0.0.28", - "@types/rss": "^0.0.28", - "@types/sortablejs": "^1.10.6", - "@types/speakingurl": "^13.0.2", - "@types/webscopeio__react-textarea-autocomplete": "^4.7.2", + "@loadable/component": "^5.15.2", + "@loadable/server": "^5.15.2", + "@tanstack/react-query": "^4.29.7", + "@tanstack/react-query-devtools": "^4.29.7", + "@types/tus-js-client": "^2.1.0", "@webscopeio/react-textarea-autocomplete": "^4.8.1", "axios": "^0.21.2", + "axios-cookiejar-support": "^4.0.6", "bs58": "^4.0.1", "connected-react-router": "^6.8.0", "cookie-parser": "^1.4.5", "currency-symbol-map": "^4.0.4", "debounce": "^1.2.1", "diff-match-patch": "^1.0.5", - "express": "^4.17.1", + "express": "^4.17.3", "history": "^4.7.2", "hive-uri": "^0.2.3", - "hivesigner": "^3.2.5", + "hivesigner": "^3.3.4", "html-react-parser": "^1.2.1", "i18next": "^19.4.4", "i18next-browser-languagedetector": "^4.2.0", "immutability-helper": "^3.0.2", + "js-base64": "^3.7.5", "js-cookie": "^2.2.1", + "js-md5": "^0.7.3", + "lightweight-charts": "^3.8.0", + "loadable-ts-transformer": "^1.0.0-alpha.3", "medium-zoom": "^1.0.6", - "moment": "^2.26.0", + "moment": "^2.29.4", "node-cache": "^5.1.0", - "node-sass": "^6.0.1", + "node-html-parser": "^5.3.3", "numeral": "^2.0.6", "path-to-regexp": "^6.1.0", - "qs": "^6.9.4", + "qrcode": "^1.5.1", + "qs": "^6.10.3", "query-string": "^6.13.1", "react": "^16.8.6", + "react-beautiful-dnd": "^13.1.0", "react-bootstrap": "^1.0.1", "react-datetime": "^3.0.4", "react-dom": "^16.8.6", "react-fast-compare": "^3.2.0", + "react-google-recaptcha": "^2.1.0", + "react-grid-layout": "^1.3.4", "react-helmet": "^6.0.0", "react-highcharts": "^16.1.0", "react-img-webp": "^2.0.2", + "react-in-viewport": "^1.0.0-alpha.30", "react-popper": "^2.2.5", "react-redux": "^7.2.0", + "react-resize-detector": "^7.1.2", "react-router-dom": "^5.0.1", "react-sortablejs": "^2.0.11", + "react-textarea-autosize": "^8.4.1", + "react-twitter-embed": "^4.0.4", + "react-use": "^17.4.0", + "react-virtualized": "^9.22.3", + "redis": "4.6.7", "redux": "4.0.5", "redux-thunk": "^2.3.0", "rss": "^1.2.2", + "sass": "^1.56.2", "serialize-javascript": "^3.1.0", "sortablejs": "^1.13.0", "speakingurl": "^14.0.1", + "tough-cookie": "^4.1.2", + "tus-js-client": "^3.1.0", + "use-async-effect": "^2.2.6", + "use-indexeddb": "^2.0.2", + "uuid": "^9.0.0", "xss": "^1.0.8" }, "devDependencies": { + "@loadable/webpack-plugin": "^5.15.2", + "@types/bs58": "^4.0.1", "@types/bytebuffer": "^5.0.41", "@types/cookie-parser": "^1.4.2", + "@types/diff-match-patch": "^1.0.32", "@types/express": "^4.17.0", "@types/jest": "^26.0.24", "@types/js-cookie": "^2.2.6", + "@types/js-md5": "^0.7.0", + "@types/loadable__component": "^5.13.4", + "@types/loadable__server": "^5.12.6", + "@types/lodash": "^4.14.191", "@types/node": "^12.6.6", + "@types/numeral": "^2.0.2", "@types/path-to-regexp": "^1.7.0", + "@types/qrcode": "^1.4.2", "@types/react": "^16.8.23", + "@types/react-beautiful-dnd": "^13.1.2", "@types/react-dom": "^16.8.4", + "@types/react-google-recaptcha": "^2.1.5", + "@types/react-grid-layout": "^1.3.2", "@types/react-helmet": "^6.0.0", "@types/react-highcharts": "^16.0.3", "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^4.3.4", "@types/react-test-renderer": "^16.9.2", + "@types/react-virtualized": "^9.21.21", + "@types/redis": "^4.0.11", + "@types/resize-observer-browser": "^0.1.7", + "@types/rss": "^0.0.29", "@types/serialize-javascript": "^1.5.0", + "@types/sortablejs": "^1.15.0", + "@types/speakingurl": "^13.0.3", + "@types/uuid": "^9.0.0", "@types/webpack-env": "^1.14.0", + "@types/webscopeio__react-textarea-autocomplete": "^4.7.2", "babel-preset-razzle": "^4.0.5", "html-webpack-plugin": "4.5.2", + "husky": "^8.0.1", "jest": "^26.0.0", + "lint-staged": "^13.0.3", "mini-css-extract-plugin": "0.9.0", "mockdate": "^3.0.2", - "postcss": "8.2.10", + "postcss": "8.2.13", + "prettier": "^2.7.1", "razzle": "^4.0.5", "razzle-dev-utils": "^4.0.5", "razzle-plugin-scss": "^4.0.5", "razzle-plugin-typescript": "^3.0.0", "react-test-renderer": "^16.13.1", + "timers": "^0.1.1", "ts-jest": "^26.4.2", "tslint": "^5.18.0", "tslint-react": "^4.0.0", - "typescript": "^3.5.3", + "typescript": "4.1.6", "url-loader": "^4.1.1", "webpack": "4.46.0", + "webpack-bundle-analyzer": "^4.8.0", "webpack-dev-server": "3.11.0" }, "resolutions": { "jest": "^26.0.0", "@types/react": "^16.8.23" }, + "lint-staged": { + "*.{js,jsx,ts,tsx,css,md,json}": [ + "yarn format" + ] + }, "jest": { "transform": { "\\.(ts|tsx)$": "ts-jest", diff --git a/public/assets/arrow1-about.png b/public/assets/arrow1-about.png new file mode 100644 index 00000000000..2fcc8b7a0d0 Binary files /dev/null and b/public/assets/arrow1-about.png differ diff --git a/public/assets/arrow2-about.png b/public/assets/arrow2-about.png new file mode 100644 index 00000000000..a61b36c1062 Binary files /dev/null and b/public/assets/arrow2-about.png differ diff --git a/public/assets/arrow3-about.png b/public/assets/arrow3-about.png new file mode 100644 index 00000000000..a6ed1e9946e Binary files /dev/null and b/public/assets/arrow3-about.png differ diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js new file mode 100644 index 00000000000..6e1024ce492 --- /dev/null +++ b/public/firebase-messaging-sw.js @@ -0,0 +1,48 @@ +// Scripts for firebase and firebase messaging +importScripts('https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js'); +importScripts('https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js'); + +// Initialize the Firebase app in the service worker by passing the generated config +var firebaseConfig = { + apiKey: 'AIzaSyDKF-JWDMmUs5ozjK7ZdgG4beHRsAMd2Yw', + authDomain: 'esteem-ded08.firebaseapp.com', + databaseURL: 'https://esteem-ded08.firebaseio.com', + projectId: 'esteem-ded08', + storageBucket: 'esteem-ded08.appspot.com', + messagingSenderId: '211285790917', + appId: '1:211285790917:web:c259d25ed1834c683760ac', + measurementId: 'G-TYQD1N3NR3' +}; + +firebase.initializeApp(firebaseConfig); + +// Retrieve firebase messaging +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage(function (payload) { + //console.log('Received bg notification', payload); + const notificationTitle = payload.notification?.title || 'Ecency'; + + self.registration.showNotification(notificationTitle, { + body: payload.notification?.body, + icon: payload.notification?.image || 'https://ecency.com/static/media/logo-circle.2df6f251.svg', + data: payload.data, + }); +}); + +self.addEventListener('notificationclick', function (event) { + const data = event.notification.data; + let url = 'https://ecency.com'; + const fullPermlink = data.permlink1 + data.permlink2 + data.permlink3; + if (['vote', 'unvote', 'spin', 'inactive'].includes(data.type)) { + url += '/@' + data.target; + } else { + // delegation, mention, transfer, follow, unfollow, ignore, blacklist, reblog + url += '/@' + data.source; + } + if (fullPermlink) { + url += '/' + fullPermlink; + } + + clients.openWindow(url, '_blank'); +}); \ No newline at end of file diff --git a/razzle.config.js b/razzle.config.js index d0a140e0092..fcc4629bbc4 100644 --- a/razzle.config.js +++ b/razzle.config.js @@ -1,26 +1,58 @@ -'use strict'; +"use strict"; +const LoadableWebpackPlugin = require("@loadable/webpack-plugin"); +const { loadableTransformer } = require("loadable-ts-transformer"); +const path = require("path"); + +const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; module.exports = { - plugins: ['typescript', 'scss'], + plugins: ["typescript", "scss"], + resolve: { + alias: { + styles: path.join(__dirname, "src/style/") + } + }, options: { - buildType: 'iso' + buildType: "iso" }, modifyWebpackConfig({ env: { target, // the target 'node' or 'web' - dev, // is this a development build? true or false + dev // is this a development build? true or false }, webpackConfig, // the created webpack config webpackObject, // the imported webpack node module options: { pluginOptions, // the options passed to the plugin ({ name:'pluginname', options: { key: 'value'}}) razzleOptions, // the modified options passed to Razzle in the `options` key in `razzle.config.js` (options: { key: 'value'}) - webpackOptions, // the modified options that was used to configure webpack/ webpack loaders and plugins + webpackOptions // the modified options that was used to configure webpack/ webpack loaders and plugins }, - paths, // the modified paths that will be used by Razzle. + paths // the modified paths that will be used by Razzle. }) { // Do some stuff to webpackConfig - webpackConfig.devtool = dev ? 'source-map' : false; + if (target === "web") { + const filename = path.resolve(__dirname, "build"); + // saving stats file to build folder + // without this, stats files will go into + // build/public folder + webpackConfig.plugins.push( + new LoadableWebpackPlugin({ + outputAsset: true, + writeToDisk: { filename } + }) + ); + } + + // Enable SSR lazy-loading + const tsLoader = webpackConfig.module.rules.find( + (rule) => !(rule.test instanceof Array) && rule.test && rule.test.test(".tsx") + ); + tsLoader.use[0].options.getCustomTransformers = () => ({ before: [loadableTransformer] }); + + if (target === "web" && dev) { + webpackConfig.plugins.push(new BundleAnalyzerPlugin()); + } + webpackConfig.devtool = dev ? "source-map" : false; return webpackConfig; } }; diff --git a/src/client/base-handlers.ts b/src/client/base-handlers.ts index f70264b9161..3cab5199c31 100644 --- a/src/client/base-handlers.ts +++ b/src/client/base-handlers.ts @@ -1,73 +1,72 @@ // Base event handlers for browser window -import {history} from "../common/store"; +import { history } from "../common/store"; import routes from "../common/routes"; -import {pathToRegexp} from "path-to-regexp"; +import { pathToRegexp } from "path-to-regexp"; // Global drag&drop const handleDragOver = (e: DragEvent) => { - if (!(e.target && e.dataTransfer)) { - return; - } + if (!(e.target && e.dataTransfer)) { + return; + } - e.preventDefault(); - e.dataTransfer.effectAllowed = 'none'; - e.dataTransfer.dropEffect = 'none'; -} + e.preventDefault(); + e.dataTransfer.effectAllowed = "none"; + e.dataTransfer.dropEffect = "none"; +}; // Global click handler const handleClick = (e: Event) => { - const el = e.target as HTMLElement; - - // Anchor link handler - if (el.tagName === "A" || (el.parentElement && el.parentElement.tagName === "A")) { + const el = e.target as HTMLElement; - const href = el.getAttribute("href") || (el.parentElement ? el.parentElement.getAttribute("href") : null); + // Anchor link handler + if (el.tagName === "A" || (el.parentElement && el.parentElement.tagName === "A")) { + const href = + el.getAttribute("href") || (el.parentElement ? el.parentElement.getAttribute("href") : null); - if (href && href.startsWith("/") && href.indexOf("#") !== -1) { - const [route, anchor] = href.split("#"); + if (href && href.startsWith("/") && href.indexOf("#") !== -1) { + const [route, anchor] = href.split("#"); - // make sure link matches with one of app routes - if (Object.values(routes).find(p => pathToRegexp(p).test(route))) { - e.preventDefault(); + // make sure link matches with one of app routes + if (Object.values(routes).find((p) => pathToRegexp(p).test(route))) { + e.preventDefault(); - let delay = 75; + let delay = 75; - if (history!.location.pathname !== route) { - history!.push(href); - } + if (history!.location.pathname !== route) { + history!.push(href); + } - // scroll to anchor element - const el = document.getElementById(anchor); - if (el) { - setTimeout(() => { - el.scrollIntoView(); - }, delay); - } - } + // scroll to anchor element + const el = document.getElementById(anchor); + if (el) { + setTimeout(() => { + el.scrollIntoView(); + }, delay); } + } } - - // Handle links in static pages. (faq etc...) - if (el.tagName === "A") { - if (el.classList.contains("push-link")) { - e.preventDefault(); - const href = el.getAttribute("href"); - if (href && href.startsWith("/")) { - - // make sure link matches with one of app routes - if (Object.values(routes).find(p => pathToRegexp(p).test(href))) { - e.preventDefault(); - history!.push(href); - } - } + } + + // Handle links in static pages. (faq etc...) + if (el.tagName === "A") { + if (el.classList.contains("push-link")) { + e.preventDefault(); + const href = el.getAttribute("href"); + if (href && href.startsWith("/")) { + // make sure link matches with one of app routes + if (Object.values(routes).find((p) => pathToRegexp(p).test(href))) { + e.preventDefault(); + history!.push(href); } + } } -} + } +}; document.addEventListener("DOMContentLoaded", function () { - document.body.addEventListener('dragover', handleDragOver); - document.body.addEventListener('click', handleClick); + document.body.addEventListener("dragover", handleDragOver); + document.body.addEventListener("click", handleClick); }); diff --git a/src/client/index.tsx b/src/client/index.tsx index 9ec79ed008f..d818976bbbf 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,30 +1,25 @@ import React from "react"; -import {hydrate} from "react-dom"; -import {Provider} from "react-redux"; -import {ConnectedRouter} from "connected-react-router"; - +import { hydrate } from "react-dom"; +import { Provider } from "react-redux"; +import { ConnectedRouter } from "connected-react-router"; import configureStore from "../common/store/configure"; - -import {hasKeyChainAct} from "../common/store/global"; -import {clientStoreTasks} from "../common/store/helper"; - -import {history} from "../common/store"; - +import { hasKeyChainAct } from "../common/store/global"; +import { clientStoreTasks } from "../common/store/helper"; +import { history } from "../common/store"; import App from "../common/app"; - -import {AppWindow} from "./window"; - -import "../style/theme-day.scss"; -import "../style/theme-night.scss"; - -import './base-handlers'; +import { AppWindow } from "./window"; +import "../style/style.scss"; +import "./base-handlers"; +import { loadableReady } from "@loadable/component"; +import { queryClient } from "../common/core"; +import { Hydrate, QueryClientProvider } from "@tanstack/react-query"; declare var window: AppWindow; const store = configureStore(window["__PRELOADED_STATE__"]); -if (process.env.NODE_ENV === 'production') { - console.log(`@@@@@@@(((((@@@@@@@@@@@@@ +if (process.env.NODE_ENV === "production") { + console.log(`@@@@@@@(((((@@@@@@@@@@@@@ @@@(((((((((((((@@@@@@@@@ @((((@@@@@@@@@((((@@@@@@@ @(((@@@(((((@@@((((%@@@@@ @@ -33,48 +28,58 @@ if (process.env.NODE_ENV === 'production') { ((((@@@@@@&&&@@@@@@@@@((( ((((@@@@@@@@@@@@@@@@@(((( (((((%@@@@@@@@@%%(((((((@ -@@(((((((((((((((((((@@@@` -); - console.log('%c%s', 'font-size: 16px;', 'We are hiring!'); - console.log( - '%c%s', - 'font-size: 12px;', - 'Are you developer, looking ways to contribute? \nhttps://github.com/ecency/ecency-vision \n\n' - ); +@@(((((((((((((((((((@@@@`); + console.log("%c%s", "font-size: 16px;", "We are hiring!"); + console.log( + "%c%s", + "font-size: 12px;", + "Are you developer, looking ways to contribute? \nhttps://github.com/ecency/ecency-vision \n\n" + ); } -hydrate( - - - - - , +loadableReady().then(() => { + hydrate( + + {/*@ts-ignore*/} + + + + + + + + , document.getElementById("root") -); + ); -clientStoreTasks(store); + clientStoreTasks(store); -// Check & activate keychain support -window.addEventListener("load", () => { + // Check & activate keychain support + window.addEventListener("load", () => { setTimeout(() => { - if (window.hive_keychain) { - window.hive_keychain.requestHandshake(() => { - store.dispatch(hasKeyChainAct()); - }); - } + if (window.hive_keychain) { + window.hive_keychain.requestHandshake(() => { + store.dispatch(hasKeyChainAct()); + }); + } }, 50); + }); }); if (module.hot) { - module.hot.accept("../common/app", () => { - hydrate( - - - - - , - document.getElementById("root") - ); - }); + module.hot.accept("../common/app", () => { + hydrate( + + {/*@ts-ignore*/} + + + + + + + + , + document.getElementById("root") + ); + }); } - diff --git a/src/client/window.ts b/src/client/window.ts index 86e6305162a..dfb19312bf8 100644 --- a/src/client/window.ts +++ b/src/client/window.ts @@ -1,13 +1,13 @@ -import {KeyChainImpl} from "../common/helper/keychain"; +import { KeyChainImpl } from "../common/helper/keychain"; export interface AppWindow extends Window { - usePrivate: boolean; - nws?: WebSocket; - comTag?: {}; - hive_keychain?: KeyChainImpl; - twttr?: { - widgets?: { - load: () => void - } - } + usePrivate: boolean; + nws?: WebSocket; + comTag?: {}; + hive_keychain?: KeyChainImpl; + twttr: { + widgets?: { + load: () => void; + }; + }; } diff --git a/src/common/api/auth-api.ts b/src/common/api/auth-api.ts index 63c0dc2a64c..4ffbdd23c9b 100644 --- a/src/common/api/auth-api.ts +++ b/src/common/api/auth-api.ts @@ -1,16 +1,17 @@ import axios from "axios"; -import {apiBase} from "./helper"; +import { apiBase } from "./helper"; -export const hsTokenRenew = (code: string): Promise<{ - username: string; - access_token: string; - refresh_token: string; - expires_in: number; +export const hsTokenRenew = ( + code: string +): Promise<{ + username: string; + access_token: string; + refresh_token: string; + expires_in: number; }> => - axios - .post(apiBase(`/auth-api/hs-token-refresh`), { - code, - }) - .then((resp) => resp.data); - + axios + .post(apiBase(`/auth-api/hs-token-refresh`), { + code + }) + .then((resp) => resp.data); diff --git a/src/common/api/bridge.ts b/src/common/api/bridge.ts index 46ab40fc2d7..9699b8ebbcf 100644 --- a/src/common/api/bridge.ts +++ b/src/common/api/bridge.ts @@ -1,179 +1,219 @@ -import {Entry} from "../store/entries/types"; -import {Community} from "../store/communities/types"; -import {Subscription} from "../store/subscriptions/types"; - -import {client as hiveClient} from "./hive"; - -export const dataLimit = typeof window !== "undefined" && window.screen.width < 540 ? 5 : 20 || 20 - -const bridgeApiCall = (endpoint: string, params: {}): Promise => hiveClient.call("bridge", endpoint, params); - -const resolvePost = (post: Entry, observer: string): Promise => { - const {json_metadata: json} = post; - - if (json.original_author && json.original_permlink && json.tags && json.tags[0] === "cross-post") { - return getPost(json.original_author, json.original_permlink, observer).then(resp => { - if (resp) { - return { - ...post, - original_entry: resp - } - } +import { Entry } from "../store/entries/types"; +import { Community } from "../store/communities/types"; +import { Subscription } from "../store/subscriptions/types"; + +import { Client } from "@hiveio/dhive"; +import SERVERS from "../constants/servers.json"; + +export const bridgeServer = new Client(SERVERS, { + timeout: 3000, + failoverThreshold: 3, + consoleOnFailover: true +}); +export const dataLimit = typeof window !== "undefined" && window.screen.width < 540 ? 5 : 20 || 20; + +const bridgeApiCall = (endpoint: string, params: {}): Promise => + bridgeServer.call("bridge", endpoint, params); + +export const resolvePost = (post: Entry, observer: string, num?: number): Promise => { + const { json_metadata: json } = post; + + if ( + json.original_author && + json.original_permlink && + json.tags && + json.tags[0] === "cross-post" + ) { + return getPost(json.original_author, json.original_permlink, observer, num) + .then((resp) => { + if (resp) { + return { + ...post, + original_entry: resp, + num + }; + } - return post; - }).catch(() => { - return post; - }) - } + return post; + }) + .catch(() => { + return post; + }); + } - return new Promise((resolve) => { - resolve(post); - }); -} + return new Promise((resolve) => { + resolve({ ...post, num }); + }); +}; const resolvePosts = (posts: Entry[], observer: string): Promise => { - const promises = posts.map(p => resolvePost(p, observer)); + const promises = posts.map((p) => resolvePost(p, observer)); - return Promise.all(promises); -} + return Promise.all(promises); +}; export const getPostsRanked = ( - sort: string, - start_author: string = "", - start_permlink: string = "", - limit: number = dataLimit, - tag: string = "", - observer: string = "" + sort: string, + start_author: string = "", + start_permlink: string = "", + limit: number = dataLimit, + tag: string = "", + observer: string = "" ): Promise => { - return bridgeApiCall("get_ranked_posts", { - sort, - start_author, - start_permlink, - limit, - tag, - observer, - }).then(resp => { - if (resp) { - return resolvePosts(resp, observer); - } + return bridgeApiCall("get_ranked_posts", { + sort, + start_author, + start_permlink, + limit, + tag, + observer + }).then((resp) => { + if (resp) { + return resolvePosts(resp, observer); + } - return resp; - }) -} + return resp; + }); +}; export const getAccountPosts = ( - sort: string, - account: string, - start_author: string = "", - start_permlink: string = "", - limit: number = dataLimit, - observer: string = "" + sort: string, + account: string, + start_author: string = "", + start_permlink: string = "", + limit: number = dataLimit, + observer: string = "" ): Promise => { - - return bridgeApiCall("get_account_posts", { - sort, - account, - start_author, - start_permlink, - limit, - observer, - }).then(resp => { - if (resp) { - return resolvePosts(resp, observer); - } - - return resp; - }) -} - -export const getPost = (author: string = "", permlink: string = "", observer: string = ""): Promise => { - return bridgeApiCall("get_post", { - author, - permlink, - observer, - }).then(resp => { - if (resp) { - return resolvePost(resp, observer); - } - - return resp; - }) -} - -export interface AccountNotification { - date: string; - id: number; - msg: string; - score: number; - type: string; - url: string; -} - -export const getAccountNotifications = (account: string, lastId: number | null = null, limit = 50): Promise => { - const params: { account: string, last_id?: number, limit: number } = { - account, limit + return bridgeApiCall("get_account_posts", { + sort, + account, + start_author, + start_permlink, + limit, + observer + }).then((resp) => { + if (resp) { + return resolvePosts(resp, observer); } - if (lastId) { - params.last_id = lastId; + return resp; + }); +}; + +export const getPost = ( + author: string = "", + permlink: string = "", + observer: string = "", + num?: number +): Promise => { + return bridgeApiCall("get_post", { + author, + permlink, + observer + }).then((resp) => { + if (resp) { + return resolvePost(resp, observer, num); } - return bridgeApiCall("account_notifications", params); -} + return resp; + }); +}; + +export const getPostHeader = ( + author: string = "", + permlink: string = "" +): Promise => { + return bridgeApiCall("get_post_header", { + author, + permlink + }).then((resp) => { + return resp; + }); +}; -export const getDiscussion = (author: string, permlink: string): Promise | null> => - bridgeApiCall | null>("get_discussion", { - author, - permlink, - }); +export interface AccountNotification { + date: string; + id: number; + msg: string; + score: number; + type: string; + url: string; +} -export const getCommunity = (name: string, observer: string | undefined = ""): Promise => - bridgeApiCall("get_community", {name, observer}); +export const getAccountNotifications = ( + account: string, + lastId: number | null = null, + limit = 50 +): Promise => { + const params: { account: string; last_id?: number; limit: number } = { + account, + limit + }; + + if (lastId) { + params.last_id = lastId; + } + + return bridgeApiCall("account_notifications", params); +}; + +export const getDiscussion = ( + author: string, + permlink: string +): Promise | null> => + bridgeApiCall | null>("get_discussion", { + author, + permlink + }); + +export const getCommunity = ( + name: string, + observer: string | undefined = "" +): Promise => + bridgeApiCall("get_community", { name, observer }); export const getCommunities = ( - last: string = "", - limit: number = 100, - query?: string | null, - sort: string = "rank", - observer: string = "" + last: string = "", + limit: number = 100, + query?: string | null, + sort: string = "rank", + observer: string = "" ): Promise => - bridgeApiCall("list_communities", { - last, - limit, - query, - sort, - observer, - }); + bridgeApiCall("list_communities", { + last, + limit, + query, + sort, + observer + }); export const normalizePost = (post: any): Promise => - bridgeApiCall("normalize_post", { - post, - }); + bridgeApiCall("normalize_post", { + post + }); -export const getSubscriptions = ( - account: string -): Promise => - bridgeApiCall("list_all_subscriptions", { - account - }); +export const getSubscriptions = (account: string): Promise => + bridgeApiCall("list_all_subscriptions", { + account + }); - -export const getSubscribers = ( - community: string -): Promise => - bridgeApiCall("list_subscribers", { - community - }); +export const getSubscribers = (community: string): Promise => + bridgeApiCall("list_subscribers", { + community + }); export interface AccountRelationship { - follows: boolean, - ignores: boolean, - is_blacklisted: boolean, - follows_blacklists: boolean + follows: boolean; + ignores: boolean; + is_blacklisted: boolean; + follows_blacklists: boolean; } -export const getRelationshipBetweenAccounts = (follower: string, following: string): Promise => - bridgeApiCall("get_relationship_between_accounts", [follower, following]); - - - +export const getRelationshipBetweenAccounts = ( + follower: string, + following: string +): Promise => + bridgeApiCall("get_relationship_between_accounts", [ + follower, + following + ]); diff --git a/src/common/api/firebase.ts b/src/common/api/firebase.ts new file mode 100644 index 00000000000..abfc8149bbe --- /dev/null +++ b/src/common/api/firebase.ts @@ -0,0 +1,71 @@ +import { getMessaging, getToken, MessagePayload, Messaging, onMessage } from "@firebase/messaging"; +import { FirebaseApp, initializeApp } from "@firebase/app"; +import { Analytics, getAnalytics, logEvent } from "@firebase/analytics"; + +let app: FirebaseApp; +export let FCM: Messaging; +export let FA: Analytics; + +export function initFirebase(initMessaging = true) { + if (typeof window === "undefined") { + return; + } + + app = initializeApp({ + apiKey: "AIzaSyDKF-JWDMmUs5ozjK7ZdgG4beHRsAMd2Yw", + authDomain: "esteem-ded08.firebaseapp.com", + databaseURL: "https://esteem-ded08.firebaseio.com", + projectId: "esteem-ded08", + storageBucket: "esteem-ded08.appspot.com", + messagingSenderId: "211285790917", + appId: "1:211285790917:web:c259d25ed1834c683760ac", + measurementId: "G-TYQD1N3NR3" + }); + if (initMessaging) { + FCM = getMessaging(app); + } + FA = getAnalytics(app); + + logEvent(FA, "test-event"); +} + +export const handleMessage = (payload: MessagePayload) => { + const notificationTitle = payload.notification?.title || "Ecency"; + + const notification = new Notification(notificationTitle, { + body: payload.notification?.body, + icon: payload.notification?.image + }); + + notification.onclick = () => { + let url = "https://ecency.com"; + const data = (payload.data || {}) as any; + const fullPermlink = data.permlink1 + data.permlink2 + data.permlink3; + + if (["vote", "unvote", "spin", "inactive"].includes(data.type)) { + url += "/@" + data.target; + } else { + // delegation, mention, transfer, follow, unfollow, ignore, blacklist, reblog + url += "/@" + data.source; + } + if (fullPermlink) { + url += "/" + fullPermlink; + } + + window.open(url, "_blank"); + }; +}; + +export const getFcmToken = () => + getToken(FCM, { + vapidKey: + "BA3SrGKAKMU_6PXOFwD9EQ1wIPzyYt90Q9ByWb3CkazBe8Isg7xr9Cgy0ka6SctHDW0VZLShTV_UDYNxewzWDjk" + }); + +export const listenFCM = (callback: Function) => { + onMessage(FCM, (p) => { + //console.log('Received fg message', p); + handleMessage(p); + callback(); + }); +}; diff --git a/src/common/api/hive-engine.ts b/src/common/api/hive-engine.ts index deb5a901bf4..3d367a337b0 100644 --- a/src/common/api/hive-engine.ts +++ b/src/common/api/hive-engine.ts @@ -2,6 +2,7 @@ import axios from "axios"; import HiveEngineToken from "../helper/hive-engine-wallet"; import { TransactionConfirmation } from "@hiveio/dhive"; import { broadcastPostingJSON } from "./operations"; +import engine from "../constants/engine.json"; interface TokenBalance { symbol: string; @@ -42,9 +43,9 @@ export interface TokenStatus { precision: number; } -const HIVE_ENGINE_RPC_URL = "https://api.hive-engine.com/rpc/contracts"; +const HIVE_ENGINE_RPC_URL = engine.engineRpcUrl; -const getTokenBalances = (account: string): Promise => { +export const getTokenBalances = (account: string): Promise => { const data = { jsonrpc: "2.0", method: "find", @@ -52,15 +53,15 @@ const getTokenBalances = (account: string): Promise => { contract: "tokens", table: "balances", query: { - account: account, - }, + account: account + } }, - id: 1, + id: 1 }; return axios .post(HIVE_ENGINE_RPC_URL, data, { - headers: { "Content-type": "application/json" }, + headers: { "Content-type": "application/json" } }) .then((r) => r.data.result) .catch((e) => { @@ -76,15 +77,15 @@ const getTokens = (tokens: string[]): Promise => { contract: "tokens", table: "tokens", query: { - symbol: { $in: tokens }, - }, + symbol: { $in: tokens } + } }, - id: 2, + id: 2 }; return axios .post(HIVE_ENGINE_RPC_URL, data, { - headers: { "Content-type": "application/json" }, + headers: { "Content-type": "application/json" } }) .then((r) => r.data.result) .catch((e) => { @@ -92,42 +93,42 @@ const getTokens = (tokens: string[]): Promise => { }); }; -export const getHiveEngineTokenBalances = async ( - account: string -): Promise => { -// commented just to try removing the non-existing unknowing HiveEngineTokenBalance type -// ): Promise => { +export const getHiveEngineTokenBalances = async (account: string): Promise => { + // commented just to try removing the non-existing unknowing HiveEngineTokenBalance type + // ): Promise => { const balances = await getTokenBalances(account); const tokens = await getTokens(balances.map((t) => t.symbol)); return balances.map((balance) => { const token = tokens.find((t) => t.symbol == balance.symbol); - const tokenMetadata = token && JSON.parse(token!.metadata) as TokenMetadata; + const tokenMetadata = token && (JSON.parse(token!.metadata) as TokenMetadata); return new HiveEngineToken({ ...balance, ...token, ...tokenMetadata } as any); }); }; -export const getUnclaimedRewards = async( - account: string -): Promise => { - return (axios - .get(`https://scot-api.hive-engine.com/@${account}?hive=1`) - .then((r) => r.data) - .then((r) => Object.values(r)) - .then((r) => r.filter((t) => (t as TokenStatus).pending_token > 0)) as any) - .catch(() => { - return []; - }); +export const getUnclaimedRewards = async (account: string): Promise => { + const rewardsUrl = engine.engineRewardsUrl; + return ( + axios + .get(`${rewardsUrl}/@${account}?hive=1`) + .then((r) => r.data) + .then((r) => Object.values(r)) + .then((r) => r.filter((t) => (t as TokenStatus).pending_token > 0)) as any + ).catch(() => { + return []; + }); }; export const claimRewards = async ( account: string, tokens: string[] ): Promise => { - const json = JSON.stringify(tokens.map((r) => { - return { symbol: r }; - })); + const json = JSON.stringify( + tokens.map((r) => { + return { symbol: r }; + }) + ); return broadcastPostingJSON(account, "scot_claim_token", json); }; @@ -135,7 +136,7 @@ export const claimRewards = async ( export const stakeTokens = async ( account: string, token: string, - amount: string, + amount: string ): Promise => { const json = JSON.stringify({ contractName: "tokens", @@ -143,9 +144,47 @@ export const stakeTokens = async ( contractPayload: { symbol: token, to: account, - quantity: amount, - }, + quantity: amount + } }); return broadcastPostingJSON(account, "ssc-mainnet-hive", json); }; + +export const getMetrics: any = async (symbol?: any, account?: any) => { + const data = { + jsonrpc: "2.0", + method: "find", + params: { + contract: "market", + table: "metrics", + query: { + symbol: symbol, + account: account + } + }, + id: 1 + }; + + // const result = await axios + // .post(HIVE_ENGINE_RPC_URL, data, { + // headers: { "Content-type": "application/json" } + // }) + // return result; + return axios + .post(HIVE_ENGINE_RPC_URL, data, { + headers: { "Content-type": "application/json" } + }) + .then((r) => r.data.result) + .catch((e) => { + return []; + }); +}; + +export const getMarketData = async (symbol: any) => { + const url: any = engine.chartApi; + const { data: history } = await axios.get(`${url}`, { + params: { symbol, interval: "daily" } + }); + return history; +}; diff --git a/src/common/api/hive.ts b/src/common/api/hive.ts index 5a06261c433..5da3780c022 100644 --- a/src/common/api/hive.ts +++ b/src/common/api/hive.ts @@ -1,486 +1,658 @@ -import {Client, RCAPI, utils} from "@hiveio/dhive"; +import { Client, RCAPI, utils } from "@hiveio/dhive"; -import {RCAccount} from "@hiveio/dhive/lib/chain/rc"; +import { RCAccount } from "@hiveio/dhive/lib/chain/rc"; -import {TrendingTag} from "../store/trending-tags/types"; -import {DynamicProps} from "../store/dynamic-props/types"; -import {FullAccount, AccountProfile, AccountFollowStats} from "../store/accounts/types"; +import { TrendingTag } from "../store/trending-tags/types"; +import { DynamicProps } from "../store/dynamic-props/types"; +import { FullAccount, AccountProfile, AccountFollowStats } from "../store/accounts/types"; +import { Entry } from "../store/entries/types"; import parseAsset from "../helper/parse-asset"; -import {vestsToRshares} from "../helper/vesting"; +import { vestsToRshares } from "../helper/vesting"; import isCommunity from "../helper/is-community"; import SERVERS from "../constants/servers.json"; -import { dataLimit } from './bridge'; -import moment from "moment"; +import { dataLimit } from "./bridge"; +import moment, { Moment } from "moment"; export const client = new Client(SERVERS, { - timeout: 4000, - failoverThreshold: 2, - consoleOnFailover: true, + timeout: 3000, + failoverThreshold: 3, + consoleOnFailover: true }); export interface Vote { - percent: number; - reputation: number; - rshares: string; - time: string; - timestamp?: number; - voter: string; - weight: number; - reward?: number; + percent: number; + reputation: number; + rshares: string; + time: string; + timestamp?: number; + voter: string; + weight: number; + reward?: number; } export interface DynamicGlobalProperties { - hbd_print_rate: number; - total_vesting_fund_hive: string; - total_vesting_shares: string; - hbd_interest_rate: number; - head_block_number: number; - vesting_reward_percent: number; - virtual_supply: string; + hbd_print_rate: number; + total_vesting_fund_hive: string; + total_vesting_shares: string; + hbd_interest_rate: number; + head_block_number: number; + vesting_reward_percent: number; + virtual_supply: string; } export interface FeedHistory { - current_median_history: { - base: string; - quote: string; - }; + current_median_history: { + base: string; + quote: string; + }; +} + +export interface ChainProps { + account_creation_fee: string; + maximum_block_size: number; + hbd_interest_rate: number; + account_subsidy_budget: number; + account_subsidy_decay: number; } export interface RewardFund { - recent_claims: string; - reward_balance: string; + recent_claims: string; + reward_balance: string; } export interface DelegatedVestingShare { - id: number; - delegatee: string; - delegator: string; - min_delegation_time: string; - vesting_shares: string; + id: number; + delegatee: string; + delegator: string; + min_delegation_time: string; + vesting_shares: string; } export interface Follow { - follower: string; - following: string; - what: string[]; + follower: string; + following: string; + what: string[]; } export interface MarketStatistics { - hbd_volume: string; - highest_bid: string; - hive_volume: string; - latest: string; - lowest_ask: string; - percent_change: string; + hbd_volume: string; + highest_bid: string; + hive_volume: string; + latest: string; + lowest_ask: string; + percent_change: string; } export interface OpenOrdersData { - id: number, - created: string, - expiration: string, - seller: string, - orderid: number, - for_sale: number, - sell_price: { - base: string, - quote: string - }, - real_price: string, - rewarded: boolean + id: number; + created: string; + expiration: string; + seller: string; + orderid: number; + for_sale: number; + sell_price: { + base: string; + quote: string; + }; + real_price: string; + rewarded: boolean; } export interface OrdersDataItem { - created: string; - hbd: number; - hive: number; - order_price: { - base: string; - quote: string; - } - real_price: string; + created: string; + hbd: number; + hive: number; + order_price: { + base: string; + quote: string; + }; + real_price: string; +} + +export interface MarketCandlestickDataItem { + hive: { + high: number; + low: number; + open: number; + close: number; + volume: number; + }; + id: number; + non_hive: { + high: number; + low: number; + open: number; + close: number; + volume: number; + }; + open: string; + seconds: number; } export interface TradeDataItem { - current_pays: string; - date: number; - open_pays: string; + current_pays: string; + date: number; + open_pays: string; } export interface OrdersData { - bids: OrdersDataItem[]; - asks: OrdersDataItem[]; - trading: OrdersDataItem[]; + bids: OrdersDataItem[]; + asks: OrdersDataItem[]; + trading: OrdersDataItem[]; +} + +interface ApiError { + error: string; + data: any; } +const handleError = (error: any) => { + debugger; + return { error: "api error", data: error }; +}; + export const getPost = (username: string, permlink: string): Promise => - client.call("condenser_api", "get_content", [username, permlink]); + client.call("condenser_api", "get_content", [username, permlink]); export const getMarketStatistics = (): Promise => - client.call("condenser_api", "get_ticker", []); + client.call("condenser_api", "get_ticker", []); export const getOrderBook = (limit: number = 500): Promise => - client.call("condenser_api", "get_order_book", [limit]); + client.call("condenser_api", "get_order_book", [limit]); export const getOpenOrder = (user: string): Promise => - client.call("condenser_api", "get_open_orders", [user]); + client.call("condenser_api", "get_open_orders", [user]); export const getTradeHistory = (limit: number = 1000): Promise => { - let today = moment(Date.now()).subtract(10, 'h').format().split('+')[0]; - return client.call("condenser_api", "get_trade_history", [today, "1969-12-31T23:59:59",limit]);} + let todayEarlier = moment(Date.now()).subtract(10, "h").format().split("+")[0]; + let todayNow = moment(Date.now()).format().split("+")[0]; + return client.call("condenser_api", "get_trade_history", [todayEarlier, todayNow, limit]); +}; + +export const getMarketBucketSizes = (): Promise => + client.call("condenser_api", "get_market_history_buckets", []); + +export const getMarketHistory = ( + seconds: number, + startDate: Moment, + endDate: Moment +): Promise => { + let todayEarlier = startDate.format().split("+")[0]; + let todayNow = endDate.format().split("+")[0]; + return client.call("condenser_api", "get_market_history", [seconds, todayEarlier, todayNow]); +}; export const getActiveVotes = (author: string, permlink: string): Promise => - client.database.call("get_active_votes", [author, permlink]); + client.database.call("get_active_votes", [author, permlink]); export const getTrendingTags = (afterTag: string = "", limit: number = 250): Promise => - client.database - .call("get_trending_tags", [afterTag, limit]) - .then((tags: TrendingTag[]) => { - return tags - .filter((x) => x.name !== "") - .filter((x) => !isCommunity(x.name)) - .map((x) => x.name) - } - ); + client.database.call("get_trending_tags", [afterTag, limit]).then((tags: TrendingTag[]) => { + return tags + .filter((x) => x.name !== "") + .filter((x) => !isCommunity(x.name)) + .map((x) => x.name); + }); + +export const getAllTrendingTags = ( + afterTag: string = "", + limit: number = 250 +): Promise => + client.database.call("get_trending_tags", [afterTag, limit]).then((tags: TrendingTag[]) => { + return tags.filter((x) => x.name !== "").filter((x) => !isCommunity(x.name)); + }); export const lookupAccounts = (q: string, limit = 50): Promise => - client.database.call("lookup_accounts", [q, limit]); + client.database.call("lookup_accounts", [q, limit]); export const getAccounts = (usernames: string[]): Promise => { - return client.database.getAccounts(usernames).then((resp: any[]): FullAccount[] => - resp.map((x) => { - const account: FullAccount = { - name: x.name, - owner: x.owner, - active: x.active, - posting: x.posting, - memo_key: x.memo_key, - post_count: x.post_count, - created: x.created, - reputation: x.reputation, - posting_json_metadata: x.posting_json_metadata, - last_vote_time: x.last_vote_time, - last_post: x.last_post, - json_metadata: x.json_metadata, - reward_hive_balance: x.reward_hive_balance, - reward_hbd_balance: x.reward_hbd_balance, - reward_vesting_hive: x.reward_vesting_hive, - reward_vesting_balance: x.reward_vesting_balance, - balance: x.balance, - hbd_balance: x.hbd_balance, - savings_balance: x.savings_balance, - savings_hbd_balance: x.savings_hbd_balance, - next_vesting_withdrawal: x.next_vesting_withdrawal, - vesting_shares: x.vesting_shares, - delegated_vesting_shares: x.delegated_vesting_shares, - received_vesting_shares: x.received_vesting_shares, - vesting_withdraw_rate: x.vesting_withdraw_rate, - to_withdraw: x.to_withdraw, - withdrawn: x.withdrawn, - witness_votes: x.witness_votes, - proxy: x.proxy, - proxied_vsf_votes: x.proxied_vsf_votes, - voting_manabar: x.voting_manabar, - voting_power: x.voting_power, - downvote_manabar: x.downvote_manabar, - __loaded: true, - }; - - let profile: AccountProfile | undefined; - - try { - profile = JSON.parse(x.posting_json_metadata!).profile; - } catch (e) { - } - - if (!profile) { - try { - profile = JSON.parse(x.json_metadata!).profile; - } catch (e) { - } - } - - if (!profile) { - profile = { - about: '', - cover_image: '', - location: '', - name: '', - profile_image: '', - website: '', - } - } - - return {...account, profile}; - }) - ); + return client.database.getAccounts(usernames).then((resp: any[]): FullAccount[] => + resp.map((x) => { + const account: FullAccount = { + name: x.name, + owner: x.owner, + active: x.active, + posting: x.posting, + memo_key: x.memo_key, + post_count: x.post_count, + created: x.created, + reputation: x.reputation, + posting_json_metadata: x.posting_json_metadata, + last_vote_time: x.last_vote_time, + last_post: x.last_post, + json_metadata: x.json_metadata, + reward_hive_balance: x.reward_hive_balance, + reward_hbd_balance: x.reward_hbd_balance, + reward_vesting_hive: x.reward_vesting_hive, + reward_vesting_balance: x.reward_vesting_balance, + balance: x.balance, + hbd_balance: x.hbd_balance, + savings_balance: x.savings_balance, + savings_hbd_balance: x.savings_hbd_balance, + savings_hbd_last_interest_payment: x.savings_hbd_last_interest_payment, + savings_hbd_seconds_last_update: x.savings_hbd_seconds_last_update, + savings_hbd_seconds: x.savings_hbd_seconds, + next_vesting_withdrawal: x.next_vesting_withdrawal, + pending_claimed_accounts: x.pending_claimed_accounts, + vesting_shares: x.vesting_shares, + delegated_vesting_shares: x.delegated_vesting_shares, + received_vesting_shares: x.received_vesting_shares, + vesting_withdraw_rate: x.vesting_withdraw_rate, + to_withdraw: x.to_withdraw, + withdrawn: x.withdrawn, + witness_votes: x.witness_votes, + proxy: x.proxy, + recovery_account: x.recovery_account, + proxied_vsf_votes: x.proxied_vsf_votes, + voting_manabar: x.voting_manabar, + voting_power: x.voting_power, + downvote_manabar: x.downvote_manabar, + __loaded: true + }; + + let profile: AccountProfile | undefined; + + try { + profile = JSON.parse(x.posting_json_metadata!).profile; + } catch (e) {} + + if (!profile) { + try { + profile = JSON.parse(x.json_metadata!).profile; + } catch (e) {} + } + + if (!profile) { + profile = { + about: "", + cover_image: "", + location: "", + name: "", + profile_image: "", + website: "" + }; + } + + return { ...account, profile }; + }) + ); }; -export const getAccount = (username: string): Promise => getAccounts([username]).then((resp) => resp[0]); +export const getAccount = (username: string): Promise => + getAccounts([username]).then((resp) => resp[0]); export const getAccountFull = (username: string): Promise => - getAccount(username).then(async (account) => { - let follow_stats: AccountFollowStats | undefined; - try { - follow_stats = await getFollowCount(username); - } catch (e) { - } + getAccount(username).then(async (account) => { + let follow_stats: AccountFollowStats | undefined; + try { + follow_stats = await getFollowCount(username); + } catch (e) {} - return {...account, follow_stats}; - }); + return { ...account, follow_stats }; + }); export const getFollowCount = (username: string): Promise => - client.database.call("get_follow_count", [username]); + client.database.call("get_follow_count", [username]); export const getFollowing = ( - follower: string, - startFollowing: string, - followType = "blog", - limit = 100 -): Promise => client.database.call("get_following", [follower, startFollowing, followType, limit]); + follower: string, + startFollowing: string, + followType = "blog", + limit = 100 +): Promise => + client.database.call("get_following", [follower, startFollowing, followType, limit]); export const getFollowers = ( - following: string, - startFollowing: string, - followType = "blog", - limit = 100 -): Promise => client.database.call("get_followers", [following, startFollowing === "" ? null : startFollowing, followType, limit]); + following: string, + startFollowing: string, + followType = "blog", + limit = 100 +): Promise => + client.database.call("get_followers", [ + following, + startFollowing === "" ? null : startFollowing, + followType, + limit + ]); export const findRcAccounts = (username: string): Promise => - new RCAPI(client).findRCAccounts([username]) + new RCAPI(client).findRCAccounts([username]); export const getDynamicGlobalProperties = (): Promise => - client.database.getDynamicGlobalProperties().then((r: any) => { - return({ - total_vesting_fund_hive: r.total_vesting_fund_hive || r.total_vesting_fund_steem, - total_vesting_shares: r.total_vesting_shares, - hbd_print_rate: r.hbd_print_rate || r.sbd_print_rate, - hbd_interest_rate: r.hbd_interest_rate, - head_block_number: r.head_block_number, - vesting_reward_percent: r.vesting_reward_percent, - virtual_supply: r.virtual_supply - })}); - -export const getAccountHistory = (username: string, filters: any[], start: number = -1, limit: number = 20): Promise => { - - return client.call("condenser_api", "get_account_history", [username, start, limit, ...filters]); -} + client.database.getDynamicGlobalProperties().then((r: any) => { + return { + total_vesting_fund_hive: r.total_vesting_fund_hive || r.total_vesting_fund_steem, + total_vesting_shares: r.total_vesting_shares, + hbd_print_rate: r.hbd_print_rate || r.sbd_print_rate, + hbd_interest_rate: r.hbd_interest_rate, + head_block_number: r.head_block_number, + vesting_reward_percent: r.vesting_reward_percent, + virtual_supply: r.virtual_supply + }; + }); + +export const getAccountHistory = ( + username: string, + filters: any[] | any, + start: number = -1, + limit: number = 20 +): Promise => { + return filters + ? client.call("condenser_api", "get_account_history", [username, start, limit, ...filters]) + : client.call("condenser_api", "get_account_history", [username, start, limit]); +}; export const getFeedHistory = (): Promise => client.database.call("get_feed_history"); -export const getRewardFund = (): Promise => client.database.call("get_reward_fund", ["post"]); +export const getRewardFund = (): Promise => + client.database.call("get_reward_fund", ["post"]); -export const getDynamicProps = async (): Promise => { - const globalDynamic = await getDynamicGlobalProperties(); - const feedHistory = await getFeedHistory(); - const rewardFund = await getRewardFund(); - - const hivePerMVests = - (parseAsset(globalDynamic.total_vesting_fund_hive).amount / parseAsset(globalDynamic.total_vesting_shares).amount) * - 1e6; - const base = parseAsset(feedHistory.current_median_history.base).amount; - const quote = parseAsset(feedHistory.current_median_history.quote).amount; - const fundRecentClaims = parseFloat(rewardFund.recent_claims); - const fundRewardBalance = parseAsset(rewardFund.reward_balance).amount; - const hbdPrintRate = globalDynamic.hbd_print_rate; - const hbdInterestRate = globalDynamic.hbd_interest_rate; - const headBlock = globalDynamic.head_block_number; - const totalVestingFund = parseAsset(globalDynamic.total_vesting_fund_hive).amount; - const totalVestingShares = parseAsset(globalDynamic.total_vesting_shares).amount; - const virtualSupply = parseAsset(globalDynamic.virtual_supply).amount; - const vestingRewardPercent = globalDynamic.vesting_reward_percent; +export const getChainProps = (): Promise => + client.database.call("get_chain_properties"); - return { - hivePerMVests, - base, - quote, - fundRecentClaims, - fundRewardBalance, - hbdPrintRate, - hbdInterestRate, - headBlock, - totalVestingFund, - totalVestingShares, - virtualSupply, - vestingRewardPercent - }; +export const getDynamicProps = async (): Promise => { + const globalDynamic = await getDynamicGlobalProperties(); + const feedHistory = await getFeedHistory(); + const chainProps = await getChainProps(); + const rewardFund = await getRewardFund(); + + const hivePerMVests = + (parseAsset(globalDynamic.total_vesting_fund_hive).amount / + parseAsset(globalDynamic.total_vesting_shares).amount) * + 1e6; + const base = parseAsset(feedHistory.current_median_history.base).amount; + const quote = parseAsset(feedHistory.current_median_history.quote).amount; + const fundRecentClaims = parseFloat(rewardFund.recent_claims); + const fundRewardBalance = parseAsset(rewardFund.reward_balance).amount; + const hbdPrintRate = globalDynamic.hbd_print_rate; + const hbdInterestRate = globalDynamic.hbd_interest_rate; + const headBlock = globalDynamic.head_block_number; + const totalVestingFund = parseAsset(globalDynamic.total_vesting_fund_hive).amount; + const totalVestingShares = parseAsset(globalDynamic.total_vesting_shares).amount; + const virtualSupply = parseAsset(globalDynamic.virtual_supply).amount; + const vestingRewardPercent = globalDynamic.vesting_reward_percent; + const accountCreationFee = chainProps.account_creation_fee; + + return { + hivePerMVests, + base, + quote, + fundRecentClaims, + fundRewardBalance, + hbdPrintRate, + hbdInterestRate, + headBlock, + totalVestingFund, + totalVestingShares, + virtualSupply, + vestingRewardPercent, + accountCreationFee + }; }; export const getVestingDelegations = ( - username: string, - from: string = "", - limit: number = 50 -): Promise => client.database.call("get_vesting_delegations", [username, from, limit]); + username: string, + from: string = "", + limit: number = 50 +): Promise => + client.database.call("get_vesting_delegations", [username, from, limit]); + +export const getOutgoingRc = async ( + from: string, + to: string = "", + limit: number = 50 +): Promise => { + const data = await client.call("rc_api", "list_rc_direct_delegations", { + start: [from, to], + limit: limit + }); + return data; +}; + +export const getIncomingRc = async (user: string): Promise => { + const data = await fetch(`https://ecency.com/private-api/received-rc/${user}`) + .then((res: any) => res.json()) + .then((r: any) => r); + return data; +}; export interface Witness { - total_missed: number; - url: string; - props: { - account_creation_fee: string; - account_subsidy_budget: number; - maximum_block_size: number; - }, - hbd_exchange_rate: { - base: string; - }, - available_witness_account_subsidies: number; - running_version: string; - owner: string; - signing_key:string, - last_hbd_exchange_update:string + total_missed: number; + url: string; + props: { + account_creation_fee: string; + account_subsidy_budget: number; + maximum_block_size: number; + }; + hbd_exchange_rate: { + base: string; + }; + available_witness_account_subsidies: number; + running_version: string; + owner: string; + signing_key: string; + last_hbd_exchange_update: string; } -export const getWitnessesByVote = ( - from: string = "", - limit: number = 50 -): Promise => client.call("condenser_api", "get_witnesses_by_vote", [from, limit]); - +export const getWitnessesByVote = (from: string, limit: number): Promise => + client.call("condenser_api", "get_witnesses_by_vote", [from, limit]); export interface Proposal { - creator: string; - daily_pay: { - amount: string - nai: string - precision: number - }; - end_date: string; - id: number; - permlink: string; - proposal_id: number; - receiver: string; - start_date: string; - status: string; - subject: string; - total_votes: string; + creator: string; + daily_pay: { + amount: string; + nai: string; + precision: number; + }; + end_date: string; + id: number; + permlink: string; + proposal_id: number; + receiver: string; + start_date: string; + status: string; + subject: string; + total_votes: string; } -export const getProposals = (): Promise => client.call("database_api", "list_proposals", { - start: [-1], - limit: 200, - order: 'by_total_votes', - order_direction: 'descending', - status: 'all' -}).then(r => r.proposals); +export const getProposals = (): Promise => + client + .call("database_api", "list_proposals", { + start: [-1], + limit: 200, + order: "by_total_votes", + order_direction: "descending", + status: "all" + }) + .then((r) => r.proposals); export interface ProposalVote { - id: number; - proposal: Proposal; - voter: string; + id: number; + proposal: Proposal; + voter: string; } -export const getProposalVotes = (proposalId: number, voter: string = "", limit: number = 300): Promise => - client.call('condenser_api', 'list_proposal_votes', [ - [proposalId, voter], - limit, - 'by_proposal_voter' - ]) - .then(r => r.filter((x: ProposalVote) => x.proposal.proposal_id === proposalId)) - .then(r => r.map((x: ProposalVote) => ({id: x.id, voter: x.voter}))) - +export const getProposalVotes = ( + proposalId: number, + voter: string, + limit: number +): Promise => + client + .call("condenser_api", "list_proposal_votes", [[proposalId, voter], limit, "by_proposal_voter"]) + .then((r) => r.filter((x: ProposalVote) => x.proposal.proposal_id === proposalId)) + .then((r) => r.map((x: ProposalVote) => ({ id: x.id, voter: x.voter }))); export interface WithdrawRoute { - auto_vest: boolean; - from_account: string; - id: number; - percent: number; - to_account: string; + auto_vest: boolean; + from_account: string; + id: number; + percent: number; + to_account: string; } export const getWithdrawRoutes = (account: string): Promise => - client.database.call("get_withdraw_routes", [account, "outgoing"]); + client.database.call("get_withdraw_routes", [account, "outgoing"]); export const votingPower = (account: FullAccount): number => { - // @ts-ignore "Account" is compatible with dhive's "ExtendedAccount" - const calc = account && client.rc.calculateVPMana(account); - const {percentage} = calc; + // @ts-ignore "Account" is compatible with dhive's "ExtendedAccount" + const calc = account && client.rc.calculateVPMana(account); + const { percentage } = calc; - return percentage / 100; + return percentage / 100; }; export const powerRechargeTime = (power: number) => { - const missingPower = 100 - power - return missingPower * 100 * 432000 / 10000; -} + const missingPower = 100 - power; + return (missingPower * 100 * 432000) / 10000; +}; -export const votingValue = (account: FullAccount, dynamicProps: DynamicProps, votingPower: number, weight: number = 10000): number => { - const {fundRecentClaims, fundRewardBalance, base, quote} = dynamicProps; +export const votingValue = ( + account: FullAccount, + dynamicProps: DynamicProps, + votingPower: number, + weight: number = 10000 +): number => { + const { fundRecentClaims, fundRewardBalance, base, quote } = dynamicProps; - const total_vests = - parseAsset(account.vesting_shares).amount + - parseAsset(account.received_vesting_shares).amount - - parseAsset(account.delegated_vesting_shares).amount; + const total_vests = + parseAsset(account.vesting_shares).amount + + parseAsset(account.received_vesting_shares).amount - + parseAsset(account.delegated_vesting_shares).amount; - const rShares = vestsToRshares(total_vests, votingPower, weight); + const rShares = vestsToRshares(total_vests, votingPower, weight); - return rShares / fundRecentClaims * fundRewardBalance * (base / quote); -} + return (rShares / fundRecentClaims) * fundRewardBalance * (base / quote); +}; -const HIVE_VOTING_MANA_REGENERATION_SECONDS = 5*60*60*24; //5 days +const HIVE_VOTING_MANA_REGENERATION_SECONDS = 5 * 60 * 60 * 24; //5 days export const downVotingPower = (account: FullAccount): number => { - const totalShares = parseFloat(account.vesting_shares) + parseFloat(account.received_vesting_shares) - parseFloat(account.delegated_vesting_shares) - parseFloat(account.vesting_withdraw_rate); - const elapsed = Math.floor(Date.now() / 1000) - account.downvote_manabar.last_update_time; - const maxMana = totalShares * 1000000 / 4; - - let currentMana = parseFloat(account.downvote_manabar.current_mana.toString()) + elapsed * maxMana / HIVE_VOTING_MANA_REGENERATION_SECONDS; - - if (currentMana > maxMana) { - currentMana = maxMana; - } - const currentManaPerc = currentMana * 100 / maxMana; - - if (isNaN(currentManaPerc)) { - return 0; - } - - if (currentManaPerc > 100) { - return 100; - } - return currentManaPerc; + const totalShares = + parseFloat(account.vesting_shares) + + parseFloat(account.received_vesting_shares) - + parseFloat(account.delegated_vesting_shares) - + parseFloat(account.vesting_withdraw_rate); + const elapsed = Math.floor(Date.now() / 1000) - account.downvote_manabar.last_update_time; + const maxMana = (totalShares * 1000000) / 4; + + let currentMana = + parseFloat(account.downvote_manabar.current_mana.toString()) + + (elapsed * maxMana) / HIVE_VOTING_MANA_REGENERATION_SECONDS; + + if (currentMana > maxMana) { + currentMana = maxMana; + } + const currentManaPerc = (currentMana * 100) / maxMana; + + if (isNaN(currentManaPerc)) { + return 0; + } + + if (currentManaPerc > 100) { + return 100; + } + return currentManaPerc; }; export const rcPower = (account: RCAccount): number => { - const calc = client.rc.calculateRCMana(account); - const {percentage} = calc; - return percentage / 100; + const calc = client.rc.calculateRCMana(account); + const { percentage } = calc; + return percentage / 100; }; export interface ConversionRequest { - amount: string; - conversion_date: string; - id: number; - owner: string; - requestid: number; + amount: string; + conversion_date: string; + id: number; + owner: string; + requestid: number; +} + +export interface CollateralizedConversionRequest { + collateral_amount: string; + conversion_date: string; + converted_amount: string; + id: number; + owner: string; + requestid: number; } export const getConversionRequests = (account: string): Promise => - client.database.call("get_conversion_requests", [account]); + client.database.call("get_conversion_requests", [account]); + +export const getCollateralizedConversionRequests = ( + account: string +): Promise => + client.database.call("get_collateralized_conversion_requests", [account]); export interface SavingsWithdrawRequest { - id: number; - from: string; - to: string; - memo: string; - request_id: number; - amount: string; - complete: string; -} + id: number; + from: string; + to: string; + memo: string; + request_id: number; + amount: string; + complete: string; +} export const getSavingsWithdrawFrom = (account: string): Promise => - client.database.call("get_savings_withdraw_from", [account]); + client.database.call("get_savings_withdraw_from", [account]); export interface BlogEntry { - blog: string, - entry_id: number, - author: string, - permlink: string, - reblogged_on: string + blog: string; + entry_id: number; + post_id?: number; + num?: number; + author: string; + permlink: string; + reblogged_on: string; + created?: string; } export const getBlogEntries = (username: string, limit: number = dataLimit): Promise => - client.call('condenser_api', 'get_blog_entries', [ - username, - 0, - limit - ]); + client.call("condenser_api", "get_blog_entries", [username, 0, limit]); + +export const findAccountRecoveryRequest = (account: string): Promise => + client.call("database_api", "find_change_recovery_account_requests", { accounts: [account] }); + +// @source https://ecency.com/hive-139531/@andablackwidow/rc-stats-in-1-27 +export type RcOperation = + | "comment_operation" + | "vote_operation" + | "transfer_operation" + | "custom_json_operation"; + +export interface RcOperationStats { + count: number; // number of such operations executed during last day + avg_cost_rc: number; // average RC cost of single operation + resource_cost: { + // average RC cost split between various resources + history_rc: number; + tokens_rc: number; + market_rc: number; + state_rc: number; + exec_rc: number; + }; + resource_cost_share: { + // share of resource cost in average final cost (expressed in basis points) + history_bp: number; + tokens_bp: number; + market_bp: number; + state_bp: number; + exec_bp: number; + }; + resource_usage: { + // average consumption of resources per operation + history_bytes: number; // - size of transaction in bytes + tokens: string; // - number of tokens (always 0 or 1 (with exception of multiop) - tokens are internally expressed with 4 digit precision + market_bytes: number; // - size of transaction in bytes when it belongs to market category or 0 otherwise + state_hbytes: number; // - hour-bytes of state + exec_ns: number; // - nanoseconds of execution time + }; +} + +export const getRcOperationStats = (): Promise => client.call("rc_api", "get_rc_stats", {}); + +export const getContentReplies = (author: string, permlink: string): Promise => + client.call("condenser_api", "get_content_replies", { author, permlink }); diff --git a/src/common/api/misc.ts b/src/common/api/misc.ts index 7abb7b869ad..5b13768e72d 100644 --- a/src/common/api/misc.ts +++ b/src/common/api/misc.ts @@ -1,41 +1,73 @@ -import axios from 'axios'; +import axios from "axios"; import defaults from "../constants/defaults.json"; -import {apiBase} from "./helper"; +import { apiBase } from "./helper"; export const getEmojiData = () => fetch(apiBase("/emoji.json")).then((response) => response.json()); -export const uploadImage = async (file: File, token: string): Promise<{ - url: string +export const uploadImage = async ( + file: File, + token: string +): Promise<{ + url: string; }> => { - const fData = new FormData(); - fData.append('file', file); + const fData = new FormData(); + fData.append("file", file); - const postUrl = `${defaults.imageServer}/hs/${token}`; + const postUrl = `${defaults.imageServer}/hs/${token}`; - return axios.post(postUrl, fData, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }).then(r => r.data); + return axios + .post(postUrl, fData, { + headers: { + "Content-Type": "multipart/form-data" + } + }) + .then((r) => r.data); }; -export const getMarketData = (coin: string, vsCurrency: string, fromTs: string, toTs: string): Promise<{ prices?: [number, number] }> => { - const u = `https://api.coingecko.com/api/v3/coins/${coin}/market_chart/range?vs_currency=${vsCurrency}&from=${fromTs}&to=${toTs}` - return axios.get(u).then(r => r.data); -} +export const getMarketData = ( + coin: string, + vsCurrency: string, + fromTs: string, + toTs: string +): Promise<{ prices?: [number, number] }> => { + const u = `https://api.coingecko.com/api/v3/coins/${coin}/market_chart/range?vs_currency=${vsCurrency}&from=${fromTs}&to=${toTs}`; + return axios.get(u).then((r) => r.data); +}; export const getCurrencyRate = (cur: string): Promise => { - if (cur === "hbd") { - return new Promise((resolve) => resolve(1)); - } + if (cur === "hbd") { + return new Promise((resolve) => resolve(1)); + } - const u = `https://api.coingecko.com/api/v3/simple/price?ids=hive_dollar&vs_currencies=${cur}`; - return axios.get(u).then(r => r.data).then(r => r.hive_dollar[cur]); -} + const u = `https://api.coingecko.com/api/v3/simple/price?ids=hive_dollar&vs_currencies=${cur}`; + return axios + .get(u) + .then((r) => r.data) + .then((r) => r.hive_dollar[cur]); +}; export const geLatestDesktopTag = (): Promise => - axios.get("https://api.github.com/repos/ecency/ecency-vision/releases/latest") - .then(r => r.data) - .then(r => r.tag_name); + axios + .get("https://api.github.com/repos/ecency/ecency-vision/releases/latest") + .then((r) => r.data) + .then((r) => r.tag_name); + +export const GIPHY_API_KEY = "DQ7mV4VsZ749GcCBZEunztICJ5nA4Vef"; +export const GIPHY_API = `https://api.giphy.com/v1/gifs/trending?api_key=${GIPHY_API_KEY}&limit=10&offset=0`; +export const GIPHY_SEARCH_API = `https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&limit=40&offset=0&q=`; + +export const fetchGif = async (query: string | null, limit: string, offset: string) => { + let gifs; + if (query) { + gifs = await axios( + `https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&limit=${limit}&offset=${offset}&q=${query}` + ); + } else { + gifs = await axios( + `https://api.giphy.com/v1/gifs/trending?api_key=${GIPHY_API_KEY}&limit=${limit}&offset=${offset}` + ); + } + return gifs; +}; diff --git a/src/common/api/mutations.ts b/src/common/api/mutations.ts new file mode 100644 index 00000000000..e5f0e06c8e8 --- /dev/null +++ b/src/common/api/mutations.ts @@ -0,0 +1,15 @@ +import { useMutation } from "@tanstack/react-query"; +import { usrActivity } from "./private-api"; + +interface Params { + bl?: string | number; + tx?: string | number; +} + +export function useUserActivity(username: string | undefined, ty: number) { + return useMutation(["user-activity", username, ty], async (params: Params | undefined) => { + if (username) { + await usrActivity(username, ty, params?.bl, params?.tx); + } + }); +} diff --git a/src/common/api/notifications-ws-api.ts b/src/common/api/notifications-ws-api.ts new file mode 100644 index 00000000000..81bf1771383 --- /dev/null +++ b/src/common/api/notifications-ws-api.ts @@ -0,0 +1,190 @@ +import { WsNotification } from "../store/notifications/types"; +import { _t } from "../i18n"; +import { requestNotificationPermission } from "../util/request-notification-permission"; +import { ActiveUser } from "../store/active-user/types"; +import defaults from "../constants/defaults.json"; +import { NotifyTypes } from "../enums"; +import { playNotificationSound } from "../util/play-notification-sound"; + +declare var window: Window & { + nws?: WebSocket; +}; + +export class NotificationsWebSocket { + private activeUser: ActiveUser | null = null; + private isElectron = false; + private hasNotifications = false; + private hasUiNotifications = false; + private onSuccessCallbacks: Function[] = []; + private enabledNotifyTypes: NotifyTypes[] = []; + private toggleUiProp: Function = () => {}; + private isConnected = false; + + private static getBody(data: WsNotification) { + const { source } = data; + switch (data.type) { + case "vote": + return _t("notification.voted", { source }); + case "mention": + return data.extra.is_post === 1 + ? _t("notification.mention-post", { source }) + : _t("notification.mention-comment", { source }); + case "favorites": + return _t("notification.favorite", { source }); + case "bookmarks": + return _t("notification.bookmark", { source }); + case "follow": + return _t("notification.followed", { source }); + case "reply": + return _t("notification.replied", { source }); + case "reblog": + return _t("notification.reblogged", { source }); + case "transfer": + return _t("notification.transfer", { source, amount: data.extra.amount }); + case "delegations": + return _t("notification.delegations", { source, amount: data.extra.amount }); + default: + return ""; + } + } + + private async playSound() { + if (!("Notification" in window)) { + return; + } + const permission = await requestNotificationPermission(); + if (permission !== "granted") return; + + playNotificationSound(this.isElectron); + } + + private async onMessageReceive(evt: MessageEvent) { + const logo = this.isElectron ? "./img/logo-circle.svg" : require("../img/logo-circle.svg"); + + const data = JSON.parse(evt.data); + const msg = NotificationsWebSocket.getBody(data); + + const messageNotifyType = this.getNotificationType(data.type); + const allowedToNotify = + messageNotifyType && this.enabledNotifyTypes.length > 0 + ? this.enabledNotifyTypes.includes(messageNotifyType) + : true; + + if (msg) { + this.onSuccessCallbacks.forEach((cb) => cb()); + if (!this.hasNotifications || !allowedToNotify) { + return; + } + + await this.playSound(); + + new Notification(_t("notification.popup-title"), { body: msg, icon: logo }).onclick = () => { + if (!this.hasUiNotifications) { + this.toggleUiProp("notifications"); + } + }; + } + } + + public async connect() { + if (this.isConnected) { + return; + } + + if (!this.activeUser) { + this.disconnect(); + return; + } + + if (window.nws !== undefined) { + return; + } + + if ("Notification" in window) { + await requestNotificationPermission(); + } + + window.nws = new WebSocket(`${defaults.nwsServer}/ws?user=${this.activeUser.username}`); + window.nws.onopen = () => { + console.log("nws connected"); + this.isConnected = true; + }; + window.nws.onmessage = (e) => this.onMessageReceive(e); + window.nws.onclose = (evt: CloseEvent) => { + console.log("nws disconnected"); + + window.nws = undefined; + + if (!evt.wasClean) { + // Disconnected due connection error + console.log("nws trying to reconnect"); + + setTimeout(() => { + this.connect(); + }, 2000); + } + }; + } + + public disconnect() { + if (window.nws !== undefined && this.isConnected) { + window.nws.close(); + window.nws = undefined; + this.isConnected = false; + } + } + + public withActiveUser(activeUser: ActiveUser | null) { + this.activeUser = activeUser; + return this; + } + + public withElectron(isElectron: boolean) { + this.isElectron = isElectron; + return this; + } + + public withToggleUi(toggle: Function) { + this.toggleUiProp = toggle; + return this; + } + + public setHasNotifications(has: boolean) { + this.hasNotifications = has; + return this; + } + + public withCallbackOnMessage(cb: Function) { + this.onSuccessCallbacks.push(cb); + return this; + } + + public setHasUiNotifications(has: boolean) { + this.hasUiNotifications = has; + return this; + } + + public setEnabledNotificationsTypes(value: NotifyTypes[]) { + this.enabledNotifyTypes = value; + return this; + } + + public getNotificationType(value: string): NotifyTypes | null { + switch (value) { + case "vote": + return NotifyTypes.VOTE; + case "mention": + return NotifyTypes.MENTION; + case "follow": + return NotifyTypes.FOLLOW; + case "reply": + return NotifyTypes.COMMENT; + case "reblog": + return NotifyTypes.RE_BLOG; + case "transfer": + return NotifyTypes.TRANSFERS; + default: + return null; + } + } +} diff --git a/src/common/api/operations.ts b/src/common/api/operations.ts index b7688d0bc5a..922e861eca3 100644 --- a/src/common/api/operations.ts +++ b/src/common/api/operations.ts @@ -1,1091 +1,2281 @@ import hs from "hivesigner"; -import {PrivateKey, Operation, TransactionConfirmation, AccountUpdateOperation, CustomJsonOperation} from '@hiveio/dhive'; +import { + AccountUpdateOperation, + Authority, + CustomJsonOperation, + KeyRole, + Operation, + OperationName, + VirtualOperationName, + PrivateKey, + TransactionConfirmation +} from "@hiveio/dhive"; -import {Parameters} from 'hive-uri'; +import { Parameters } from "hive-uri"; -import {client as hiveClient} from "./hive"; +import { client as hiveClient } from "./hive"; -import {Account} from "../store/accounts/types"; +import { Account } from "../store/accounts/types"; -import {usrActivity} from "./private-api"; +import { usrActivity } from "./private-api"; -import {getAccessToken, getPostingKey} from "../helper/user-token"; +import { getAccessToken, getPostingKey } from "../helper/user-token"; import * as keychain from "../helper/keychain"; import parseAsset from "../helper/parse-asset"; -import {hotSign} from "../helper/hive-signer"; +import { hotSign } from "../helper/hive-signer"; -import {_t} from "../i18n"; +import { _t } from "../i18n"; import { TransactionType } from "../components/buy-sell-hive"; +import { ErrorTypes } from "../enums"; +import { formatNumber } from "../helper/format-number"; export interface MetaData { - links?: string[]; - image?: string[]; - thumbnails?: string[]; - users?: string[]; - tags?: string[]; - app?: string; - format?: string; - community?: string; + image?: string[]; + image_ratios?: any; + thumbnails?: string[]; + tags?: string[]; + app?: string; + format?: string; + community?: string; + description?: string; } export interface BeneficiaryRoute { - account: string; - weight: number; + account: string; + weight: number; } export interface CommentOptions { - allow_curation_rewards: boolean; - allow_votes: boolean; - author: string; - permlink: string; - max_accepted_payout: string; - percent_hbd: number; - extensions: Array<[0, { beneficiaries: BeneficiaryRoute[] }]>; + allow_curation_rewards: boolean; + allow_votes: boolean; + author: string; + permlink: string; + max_accepted_payout: string; + percent_hbd: number; + extensions: Array<[0, { beneficiaries: BeneficiaryRoute[] }]>; +} + +export enum OrderIdPrefix { + EMPTY = "", + SWAP = "9" } export type RewardType = "default" | "sp" | "dp"; -const handleChainError = (strErr: string) => { - if (/You may only post once every/.test(strErr)) { - return _t("chain-error.min-root-comment"); - } else if (/Your current vote on this comment is identical/.test(strErr)) { - return _t("chain-error.identical-vote"); - } else if (/Please wait to transact, or power up/.test(strErr)) { - return _t("chain-error.insufficient-resource"); - } else if (/Cannot delete a comment with net positive/.test(strErr)) { - return _t("chain-error.delete-comment-with-vote"); - } else if (/children == 0/.test(strErr)) { - return _t("chain-error.comment-children"); - } else if (/comment_cashout/.test(strErr)) { - return _t("chain-error.comment-cashout"); - } else if (/Votes evaluating for comment that is paid out is forbidden/.test(strErr)) { - return _t("chain-error.paid-out-post-forbidden"); - } - - return null; -} +const handleChainError = (strErr: string): [string | null, ErrorTypes] => { + if (/You may only post once every/.test(strErr)) { + return [_t("chain-error.min-root-comment"), ErrorTypes.COMMON]; + } else if (/Your current vote on this comment is identical/.test(strErr)) { + return [_t("chain-error.identical-vote"), ErrorTypes.INFO]; + } else if (/Please wait to transact, or power up/.test(strErr)) { + return [_t("chain-error.insufficient-resource"), ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS]; + } else if (/Cannot delete a comment with net positive/.test(strErr)) { + return [_t("chain-error.delete-comment-with-vote"), ErrorTypes.INFO]; + } else if (/children == 0/.test(strErr)) { + return [_t("chain-error.comment-children"), ErrorTypes.COMMON]; + } else if (/comment_cashout/.test(strErr)) { + return [_t("chain-error.comment-cashout"), ErrorTypes.COMMON]; + } else if (/Votes evaluating for comment that is paid out is forbidden/.test(strErr)) { + return [_t("chain-error.paid-out-post-forbidden"), ErrorTypes.COMMON]; + } else if (/Missing Active Authority/.test(strErr)) { + return [_t("chain-error.missing-authority"), ErrorTypes.INFO]; + } else if (/Missing Owner Authority/.test(strErr)) { + return [_t("chain-error.missing-owner-authority"), ErrorTypes.INFO]; + } + + return [null, ErrorTypes.COMMON]; +}; -export const formatError = (err: any): string => { +export const formatError = (err: any): [string, ErrorTypes] => { + let [chainErr, type] = handleChainError(err.toString()); + if (chainErr) { + return [chainErr, type]; + } - let chainErr = handleChainError(err.toString()); + if (err.error_description && typeof err.error_description === "string") { + let [chainErr, type] = handleChainError(err.error_description); if (chainErr) { - return chainErr; + return [chainErr, type]; } - if (err.error_description && typeof err.error_description === "string") { - let chainErr = handleChainError(err.error_description); - if (chainErr) { - return chainErr; - } + return [err.error_description.substring(0, 80), ErrorTypes.COMMON]; + } - return err.error_description.substring(0, 80); + if (err.message && typeof err.message === "string") { + let [chainErr, type] = handleChainError(err.message); + if (chainErr) { + return [chainErr, type]; } - if (err.message && typeof err.message === "string") { - let chainErr = handleChainError(err.message); - if (chainErr) { - return chainErr; - } - - return err.message.substring(0, 80); - } + return [err.message.substring(0, 80), ErrorTypes.COMMON]; + } - return ''; + return ["", ErrorTypes.COMMON]; }; -export const broadcastPostingJSON = (username: string, id: string, json: {}): Promise => { +export const broadcastPostingJSON = ( + username: string, + id: string, + json: {} +): Promise => { + // With posting private key + const postingKey = getPostingKey(username); + if (postingKey) { + const privateKey = PrivateKey.fromString(postingKey); + + const operation: CustomJsonOperation[1] = { + id, + required_auths: [], + required_posting_auths: [username], + json: JSON.stringify(json) + }; + + return hiveClient.broadcast.json(operation, privateKey); + } + + // With hivesigner access token - // With posting private key - const postingKey = getPostingKey(username); - if (postingKey) { - const privateKey = PrivateKey.fromString(postingKey); + let token = getAccessToken(username); + return token + ? new hs.Client({ + accessToken: token + }) + .customJson([], [username], id, JSON.stringify(json)) + .then((r: any) => r.result) + : Promise.resolve(0); +}; + +const broadcastPostingOperations = ( + username: string, + operations: Operation[] +): Promise => { + // With posting private key + const postingKey = getPostingKey(username); + if (postingKey) { + const privateKey = PrivateKey.fromString(postingKey); + + return hiveClient.broadcast.sendOperations(operations, privateKey); + } + + // With hivesigner access token + let token = getAccessToken(username); + return token + ? new hs.Client({ + accessToken: token + }) + .broadcast(operations) + .then((r: any) => r.result) + : Promise.resolve(0); +}; - const operation: CustomJsonOperation[1] = { - id, - required_auths: [], - required_posting_auths: [username], - json: JSON.stringify(json) - } +export const reblog = ( + username: string, + author: string, + permlink: string, + _delete: boolean = false +): Promise => { + const message = { + account: username, + author, + permlink + }; + + if (_delete) { + message["delete"] = "delete"; + } + + const json = ["reblog", message]; + + return broadcastPostingJSON(username, "follow", json).then((r: TransactionConfirmation) => { + usrActivity(username, 130, r.block_num, r.id).then(); + return r; + }); +}; - return hiveClient.broadcast.json(operation, privateKey); +export const comment = ( + username: string, + parentAuthor: string, + parentPermlink: string, + permlink: string, + title: string, + body: string, + jsonMetadata: MetaData, + options: CommentOptions | null, + point: boolean = false +): Promise => { + const params = { + parent_author: parentAuthor, + parent_permlink: parentPermlink, + author: username, + permlink, + title, + body, + json_metadata: JSON.stringify(jsonMetadata) + }; + + const opArray: Operation[] = [["comment", params]]; + + if (options) { + const e: Operation = ["comment_options", options]; + opArray.push(e); + } + + return broadcastPostingOperations(username, opArray).then((r) => { + if (point) { + const t = title ? 100 : 110; + usrActivity(username, t, r.block_num, r.id).then(); } + return r; + }); +}; - // With hivesigner access token +export const deleteComment = ( + username: string, + author: string, + permlink: string +): Promise => { + const params = { + author, + permlink + }; - let token = getAccessToken(username); - return token ? new hs.Client({ - accessToken: token, - }).customJson([], [username], id, JSON.stringify(json)) - .then((r: any) => r.result) : Promise.resolve(0); -} + const opArray: Operation[] = [["delete_comment", params]]; -const broadcastPostingOperations = (username: string, operations: Operation[]): Promise => { + return broadcastPostingOperations(username, opArray); +}; - // With posting private key - const postingKey = getPostingKey(username); - if (postingKey) { - const privateKey = PrivateKey.fromString(postingKey); +export const vote = ( + username: string, + author: string, + permlink: string, + weight: number +): Promise => { + const params = { + voter: username, + author, + permlink, + weight + }; + + const opArray: Operation[] = [["vote", params]]; + + return broadcastPostingOperations(username, opArray).then((r: TransactionConfirmation) => { + usrActivity(username, 120, r.block_num, r.id).then(); + return r; + }); +}; - return hiveClient.broadcast.sendOperations(operations, privateKey); +export const changeRecoveryAccount = ( + account_to_recover: string, + new_recovery_account: string, + extensions: [], + key: PrivateKey +): Promise => { + const op: Operation = [ + "change_recovery_account", + { + account_to_recover, + new_recovery_account, + extensions } + ]; + return hiveClient.broadcast.sendOperations([op], key); +}; - // With hivesigner access token - let token = getAccessToken(username); - return token ? new hs.Client({ - accessToken: token, - }).broadcast(operations) - .then((r: any) => r.result) : Promise.resolve(0); -} +export const changeRecoveryAccountHot = ( + account_to_recover: string, + new_recovery_account: string, + extensions: [] +) => { + const op: Operation = [ + "change_recovery_account", + { + account_to_recover, + new_recovery_account, + extensions + } + ]; -export const reblog = (username: string, author: string, permlink: string, _delete: boolean = false): Promise => { - const message = { - account: username, - author, - permlink - }; + const params: Parameters = { callback: `https://ecency.com/@${account_to_recover}/permissions` }; + return hs.sendOperation(op, params, () => {}); +}; - if (_delete) { - message["delete"] = "delete"; +export const changeRecoveryAccountKc = ( + account_to_recover: string, + new_recovery_account: string, + extensions: [] +) => { + const op: Operation = [ + "change_recovery_account", + { + account_to_recover, + new_recovery_account, + extensions } + ]; - const json = ["reblog", message]; + return keychain.broadcast(account_to_recover, [op], "Owner"); +}; + +export const follow = (follower: string, following: string): Promise => { + const json = [ + "follow", + { + follower, + following, + what: ["blog"] + } + ]; - return broadcastPostingJSON(username, "follow", json) - .then((r: TransactionConfirmation) => { - usrActivity(username, 130, r.block_num, r.id).then(); - return r; - }); + return broadcastPostingJSON(follower, "follow", json); }; -export const comment = ( - username: string, - parentAuthor: string, - parentPermlink: string, - permlink: string, - title: string, - body: string, - jsonMetadata: MetaData, - options: CommentOptions | null, - point: boolean = false -): Promise => { - const params = { - parent_author: parentAuthor, - parent_permlink: parentPermlink, - author: username, - permlink, - title, - body, - json_metadata: JSON.stringify(jsonMetadata), - }; +export const unFollow = (follower: string, following: string): Promise => { + const json = [ + "follow", + { + follower, + following, + what: [] + } + ]; - const opArray: Operation[] = [["comment", params]]; + return broadcastPostingJSON(follower, "follow", json); +}; - if (options) { - const e: Operation = ["comment_options", options]; - opArray.push(e); +export const ignore = (follower: string, following: string): Promise => { + const json = [ + "follow", + { + follower, + following, + what: ["ignore"] } + ]; - return broadcastPostingOperations(username, opArray) - .then((r) => { - if (point) { - const t = title ? 100 : 110; - usrActivity(username, t, r.block_num, r.id).then(); - } - return r; - }) + return broadcastPostingJSON(follower, "follow", json); }; -export const deleteComment = (username: string, author: string, permlink: string): Promise => { - const params = { - author, - permlink, - }; +export const claimRewardBalance = ( + username: string, + rewardHive: string, + rewardHbd: string, + rewardVests: string +): Promise => { + const params = { + account: username, + reward_hive: rewardHive, + reward_hbd: rewardHbd, + reward_vests: rewardVests + }; - const opArray: Operation[] = [["delete_comment", params]]; + const opArray: Operation[] = [["claim_reward_balance", params]]; - return broadcastPostingOperations(username, opArray); + return broadcastPostingOperations(username, opArray); }; -export const vote = (username: string, author: string, permlink: string, weight: number): Promise => { - const params = { - voter: username, - author, - permlink, - weight - } +export const transfer = ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + const args = { + from, + to, + amount, + memo + }; + + return hiveClient.broadcast.transfer(args, key); +}; - const opArray: Operation[] = [["vote", params]]; +export const transferHot = (from: string, to: string, amount: string, memo: string) => { + const op: Operation = [ + "transfer", + { + from, + to, + amount, + memo + } + ]; - return broadcastPostingOperations(username, opArray) - .then((r: TransactionConfirmation) => { - usrActivity(username, 120, r.block_num, r.id).then(); - return r; - }); + const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + return hs.sendOperation(op, params, () => {}); }; -export const follow = (follower: string, following: string): Promise => { - const json = ["follow", { - follower, - following, - what: ["blog"] - }]; +export const transferKc = (from: string, to: string, amount: string, memo: string) => { + const asset = parseAsset(amount); + return keychain.transfer(from, to, asset.amount.toFixed(3).toString(), memo, asset.symbol, true); +}; - return broadcastPostingJSON(follower, "follow", json); -} +export const transferPoint = ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + const json = JSON.stringify({ + sender: from, + receiver: to, + amount, + memo + }); + + const op = { + id: "ecency_point_transfer", + json, + required_auths: [from], + required_posting_auths: [] + }; + + return hiveClient.broadcast.json(op, key); +}; -export const unFollow = (follower: string, following: string): Promise => { - const json = ["follow", { - follower, - following, - what: [] - }]; +export const transferPointHot = (from: string, to: string, amount: string, memo: string) => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: "ecency_point_transfer", + json: JSON.stringify({ + sender: from, + receiver: to, + amount, + memo + }) + }; + + hotSign("custom-json", params, `@${from}/points`); +}; - return broadcastPostingJSON(follower, "follow", json); -} +export const transferPointKc = (from: string, to: string, amount: string, memo: string) => { + const json = JSON.stringify({ + sender: from, + receiver: to, + amount, + memo + }); + + return keychain.customJson(from, "ecency_point_transfer", "Active", json, "Point Transfer"); +}; -export const ignore = (follower: string, following: string): Promise => { - const json = ["follow", { - follower, - following, - what: ["ignore"] - }]; +export const transferToSavings = ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + const op: Operation = [ + "transfer_to_savings", + { + from, + to, + amount, + memo + } + ]; - return broadcastPostingJSON(follower, "follow", json); -} + return hiveClient.broadcast.sendOperations([op], key); +}; -export const claimRewardBalance = (username: string, rewardHive: string, rewardHbd: string, rewardVests: string): Promise => { - const params = { - account: username, - reward_hive: rewardHive, - reward_hbd: rewardHbd, - reward_vests: rewardVests +export const transferToSavingsHot = (from: string, to: string, amount: string, memo: string) => { + const op: Operation = [ + "transfer_to_savings", + { + from, + to, + amount, + memo } + ]; - const opArray: Operation[] = [['claim_reward_balance', params]]; - - return broadcastPostingOperations(username, opArray); -} + const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + return hs.sendOperation(op, params, () => {}); +}; -export const transfer = (from: string, key: PrivateKey, to: string, amount: string, memo: string): Promise => { - const args = { - from, - to, - amount, - memo - }; +export const transferToSavingsKc = (from: string, to: string, amount: string, memo: string) => { + const op: Operation = [ + "transfer_to_savings", + { + from, + to, + amount, + memo + } + ]; - return hiveClient.broadcast.transfer(args, key); -} + return keychain.broadcast(from, [op], "Active"); +}; -export const transferHot = (from: string, to: string, amount: string, memo: string) => { +export const limitOrderCreate = ( + owner: string, + key: PrivateKey, + amount_to_sell: any, + min_to_receive: any, + orderType: TransactionType, + idPrefix = OrderIdPrefix.EMPTY +): Promise => { + let expiration: any = new Date(Date.now()); + expiration.setDate(expiration.getDate() + 27); + expiration = expiration.toISOString().split(".")[0]; + + const op: Operation = [ + "limit_order_create", + { + orderid: Number( + `${idPrefix}${Math.floor(Date.now() / 1000) + .toString() + .slice(2)}` + ), + owner: owner, + amount_to_sell: `${ + orderType === TransactionType.Buy + ? formatNumber(amount_to_sell, 3) + : formatNumber(min_to_receive, 3) + } ${orderType === TransactionType.Buy ? "HBD" : "HIVE"}`, + min_to_receive: `${ + orderType === TransactionType.Buy + ? formatNumber(min_to_receive, 3) + : formatNumber(amount_to_sell, 3) + } ${orderType === TransactionType.Buy ? "HIVE" : "HBD"}`, + fill_or_kill: false, + expiration: expiration + } + ]; - const op: Operation = ['transfer', { - from, - to, - amount, - memo - }]; + return hiveClient.broadcast.sendOperations([op], key); +}; - const params: Parameters = {callback: `https://ecency.com/@${from}/wallet`}; - return hs.sendOperation(op, params, () => { - }); -} +export const limitOrderCancel = ( + owner: string, + key: PrivateKey, + orderid: number +): Promise => { + const op: Operation = [ + "limit_order_cancel", + { + owner: owner, + orderid: orderid + } + ]; -export const transferKc = (from: string, to: string, amount: string, memo: string) => { - const asset = parseAsset(amount); - return keychain.transfer(from, to, asset.amount.toFixed(3).toString(), memo, asset.symbol, true); -} + return hiveClient.broadcast.sendOperations([op], key); +}; -export const transferPoint = (from: string, key: PrivateKey, to: string, amount: string, memo: string): Promise => { - const json = JSON.stringify({ - sender: from, - receiver: to, - amount, - memo - }); +export const limitOrderCreateHot = ( + owner: string, + amount_to_sell: any, + min_to_receive: any, + orderType: TransactionType, + idPrefix = OrderIdPrefix.EMPTY +) => { + let expiration: any = new Date(); + expiration.setDate(expiration.getDate() + 27); + expiration = expiration.toISOString().split(".")[0]; + const op: Operation = [ + "limit_order_create", + { + orderid: Number( + `${idPrefix}${Math.floor(Date.now() / 1000) + .toString() + .slice(2)}` + ), + owner: owner, + amount_to_sell: `${ + orderType === TransactionType.Buy + ? formatNumber(amount_to_sell, 3) + : formatNumber(min_to_receive, 3) + } ${orderType === TransactionType.Buy ? "HBD" : "HIVE"}`, + min_to_receive: `${ + orderType === TransactionType.Buy + ? formatNumber(min_to_receive, 3) + : formatNumber(amount_to_sell, 3) + } ${orderType === TransactionType.Buy ? "HIVE" : "HBD"}`, + fill_or_kill: false, + expiration: expiration + } + ]; - const op = { - id: 'ecency_point_transfer', - json, - required_auths: [from], - required_posting_auths: [] - }; + const params: Parameters = { + callback: `https://ecency.com/market${idPrefix === OrderIdPrefix.SWAP ? "#swap" : ""}` + }; + return hs.sendOperation(op, params, () => {}); +}; - return hiveClient.broadcast.json(op, key); -} +export const limitOrderCancelHot = (owner: string, orderid: number) => { + const op: Operation = [ + "limit_order_cancel", + { + orderid: orderid, + owner: owner + } + ]; -export const transferPointHot = (from: string, to: string, amount: string, memo: string) => { - const params = { - authority: "active", - required_auths: `["${from}"]`, - required_posting_auths: "[]", - id: "ecency_point_transfer", - json: JSON.stringify({ - sender: from, - receiver: to, - amount, - memo - }), - } - - hotSign("custom-json", params, `@${from}/points`); -} + const params: Parameters = { callback: `https://ecency.com/market` }; + return hs.sendOperation(op, params, () => {}); +}; -export const transferPointKc = (from: string, to: string, amount: string, memo: string) => { - const json = JSON.stringify({ - sender: from, - receiver: to, - amount, - memo - }); +export const limitOrderCreateKc = ( + owner: string, + amount_to_sell: any, + min_to_receive: any, + orderType: TransactionType, + idPrefix: OrderIdPrefix = OrderIdPrefix.EMPTY +) => { + let expiration: any = new Date(); + expiration.setDate(expiration.getDate() + 27); + expiration = expiration.toISOString().split(".")[0]; + const op: Operation = [ + "limit_order_create", + { + orderid: Number( + `${idPrefix}${Math.floor(Date.now() / 1000) + .toString() + .slice(2)}` + ), + owner: owner, + amount_to_sell: `${ + orderType === TransactionType.Buy + ? formatNumber(amount_to_sell, 3) + : formatNumber(min_to_receive, 3) + } ${orderType === TransactionType.Buy ? "HBD" : "HIVE"}`, + min_to_receive: `${ + orderType === TransactionType.Buy + ? formatNumber(min_to_receive, 3) + : formatNumber(amount_to_sell, 3) + } ${orderType === TransactionType.Buy ? "HIVE" : "HBD"}`, + fill_or_kill: false, + expiration: expiration + } + ]; - return keychain.customJson(from, "ecency_point_transfer", "Active", json, "Point Transfer") -} + return keychain.broadcast(owner, [op], "Active"); +}; -export const transferToSavings = (from: string, key: PrivateKey, to: string, amount: string, memo: string): Promise => { +export const limitOrderCancelKc = (owner: string, orderid: any) => { + const op: Operation = [ + "limit_order_cancel", + { + orderid: orderid, + owner: owner + } + ]; - const op: Operation = [ - 'transfer_to_savings', - { - from, - to, - amount, - memo - } - ] + return keychain.broadcast(owner, [op], "Active"); +}; - return hiveClient.broadcast.sendOperations([op], key); -} +export const convert = ( + owner: string, + key: PrivateKey, + amount: string +): Promise => { + const op: Operation = [ + "convert", + { + owner, + amount, + requestid: new Date().getTime() >>> 0 + } + ]; -export const transferToSavingsHot = (from: string, to: string, amount: string, memo: string) => { + return hiveClient.broadcast.sendOperations([op], key); +}; - const op: Operation = ['transfer_to_savings', { - from, - to, - amount, - memo - }]; +export const convertHot = (owner: string, amount: string) => { + const op: Operation = [ + "convert", + { + owner, + amount, + requestid: new Date().getTime() >>> 0 + } + ]; - const params: Parameters = {callback: `https://ecency.com/@${from}/wallet`}; - return hs.sendOperation(op, params, () => { - }); -} + const params: Parameters = { callback: `https://ecency.com/@${owner}/wallet` }; + return hs.sendOperation(op, params, () => {}); +}; -export const transferToSavingsKc = (from: string, to: string, amount: string, memo: string) => { - const op: Operation = [ - 'transfer_to_savings', - { - from, - to, - amount, - memo - } - ] - - return keychain.broadcast(from, [op], "Active"); -} +export const convertKc = (owner: string, amount: string) => { + const op: Operation = [ + "convert", + { + owner, + amount, + requestid: new Date().getTime() >>> 0 + } + ]; -export const limitOrderCreate = (owner: string, key: PrivateKey, amount_to_sell: any, min_to_receive: any, orderType: TransactionType): Promise => { - let expiration:any = new Date(Date.now()); - expiration.setDate(expiration.getDate() + 27); - expiration = expiration.toISOString().split(".")[0]; - - const op: Operation = [ - 'limit_order_create', - { - "orderid": Math.floor(Date.now() / 1000), - "owner": owner, - "amount_to_sell": `${orderType === TransactionType.Buy ? amount_to_sell.toFixed(3) : min_to_receive.toFixed(3)} ${orderType === TransactionType.Buy ? 'HBD' : "HIVE"}`, - "min_to_receive": `${orderType === TransactionType.Buy ? min_to_receive.toFixed(3): amount_to_sell.toFixed(3)} ${orderType === TransactionType.Buy ? 'HIVE' : "HBD"}`, - "fill_or_kill": false, - "expiration": expiration - } - ] - - return hiveClient.broadcast.sendOperations([op], key); -} + return keychain.broadcast(owner, [op], "Active"); +}; -export const limitOrderCancel = (owner: string, key: PrivateKey, orderid:number, ): Promise => { +export const transferFromSavings = ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + const op: Operation = [ + "transfer_from_savings", + { + from, + to, + amount, + memo, + request_id: new Date().getTime() >>> 0 + } + ]; - const op: Operation = [ - 'limit_order_cancel', - { - "owner": owner, - "orderid": orderid, - } - ] + return hiveClient.broadcast.sendOperations([op], key); +}; - return hiveClient.broadcast.sendOperations([op], key); -} +export const transferFromSavingsHot = (from: string, to: string, amount: string, memo: string) => { + const op: Operation = [ + "transfer_from_savings", + { + from, + to, + amount, + memo, + request_id: new Date().getTime() >>> 0 + } + ]; -export const limitOrderCreateHot = (owner:string, amount_to_sell:any, min_to_receive:any, orderType: TransactionType) => { - let expiration:any = new Date(); - expiration.setDate(expiration.getDate() + 27); - expiration = expiration.toISOString().split(".")[0] - const op: Operation = [ - 'limit_order_create', - { - "orderid": Math.floor(Date.now() / 1000), - "owner": owner, - "amount_to_sell": `${orderType === TransactionType.Buy ? amount_to_sell.toFixed(3) : min_to_receive.toFixed(3)} ${orderType === TransactionType.Buy ? 'HBD' : "HIVE"}`, - "min_to_receive": `${orderType === TransactionType.Buy ? min_to_receive.toFixed(3): amount_to_sell.toFixed(3)} ${orderType === TransactionType.Buy ? 'HIVE' : "HBD"}`, - "fill_or_kill": false, - "expiration": expiration - } - ] - - const params: Parameters = {callback: `https://ecency.com/market`}; - return hs.sendOperation(op, params, () => {}); -} + const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + return hs.sendOperation(op, params, () => {}); +}; -export const limitOrderCancelHot = (owner:string, orderid:number) => { - const op: Operation = [ - 'limit_order_cancel', - { - "orderid": orderid, - "owner": owner, - } - ] - - const params: Parameters = {callback: `https://ecency.com/market`}; - return hs.sendOperation(op, params, () => {}); -} +export const transferFromSavingsKc = (from: string, to: string, amount: string, memo: string) => { + const op: Operation = [ + "transfer_from_savings", + { + from, + to, + amount, + memo, + request_id: new Date().getTime() >>> 0 + } + ]; -export const limitOrderCreateKc = (owner:string, amount_to_sell:any, min_to_receive:any, orderType: TransactionType) => { - let expiration:any = new Date(); - expiration.setDate(expiration.getDate() + 27); - expiration = expiration.toISOString().split(".")[0] - const op: Operation = [ - 'limit_order_create', - { - "orderid": Math.floor(Date.now() / 1000), - "owner": owner, - "amount_to_sell": `${orderType === TransactionType.Buy ? amount_to_sell.toFixed(3) : min_to_receive.toFixed(3)} ${orderType === TransactionType.Buy ? 'HBD' : "HIVE"}`, - "min_to_receive": `${orderType === TransactionType.Buy ? min_to_receive.toFixed(3): amount_to_sell.toFixed(3)} ${orderType === TransactionType.Buy ? 'HIVE' : "HBD"}`, - "fill_or_kill": false, - "expiration": expiration - } - ] - - return keychain.broadcast(owner, [op], "Active"); -} + return keychain.broadcast(from, [op], "Active"); +}; -export const limitOrderCancelKc = (owner:string, orderid:any) => { - const op: Operation = [ - 'limit_order_cancel', - { - "orderid": orderid, - "owner": owner, - } - ] +export const claimInterest = ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + const rid = new Date().getTime() >>> 0; + const op: Operation = [ + "transfer_from_savings", + { + from, + to, + amount, + memo, + request_id: rid + } + ]; + const cop: Operation = [ + "cancel_transfer_from_savings", + { + from, + request_id: rid + } + ]; - return keychain.broadcast(owner, [op], "Active"); -} + return hiveClient.broadcast.sendOperations([op, cop], key); +}; -export const convert = (owner: string, key: PrivateKey, amount: string): Promise => { - const op: Operation = [ - 'convert', - { - owner, - amount, - requestid: new Date().getTime() >>> 0 - } - ] - - return hiveClient.broadcast.sendOperations([op], key); -} +export const claimInterestHot = (from: string, to: string, amount: string, memo: string) => { + const rid = new Date().getTime() >>> 0; + const op: Operation = [ + "transfer_from_savings", + { + from, + to, + amount, + memo, + request_id: rid + } + ]; + const cop: Operation = [ + "cancel_transfer_from_savings", + { + from, + request_id: rid + } + ]; -export const convertHot = (owner: string, amount: string) => { + const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + return hs.sendOperations([op, cop], params, () => {}); +}; - const op: Operation = ['convert', { - owner, - amount, - requestid: new Date().getTime() >>> 0 - }]; +export const claimInterestKc = (from: string, to: string, amount: string, memo: string) => { + const rid = new Date().getTime() >>> 0; + const op: Operation = [ + "transfer_from_savings", + { + from, + to, + amount, + memo, + request_id: rid + } + ]; + const cop: Operation = [ + "cancel_transfer_from_savings", + { + from, + request_id: rid + } + ]; - const params: Parameters = {callback: `https://ecency.com/@${owner}/wallet`}; - return hs.sendOperation(op, params, () => { - }); -} + return keychain.broadcast(from, [op, cop], "Active"); +}; -export const convertKc = (owner: string, amount: string) => { - const op: Operation = [ - 'convert', - { - owner, - amount, - requestid: new Date().getTime() >>> 0 - } - ] - - return keychain.broadcast(owner, [op], "Active"); -} +export const transferToVesting = ( + from: string, + key: PrivateKey, + to: string, + amount: string +): Promise => { + const op: Operation = [ + "transfer_to_vesting", + { + from, + to, + amount + } + ]; -export const transferFromSavings = (from: string, key: PrivateKey, to: string, amount: string, memo: string): Promise => { - const op: Operation = [ - 'transfer_from_savings', - { - from, - to, - amount, - memo, - request_id: new Date().getTime() >>> 0 - } - ] - - return hiveClient.broadcast.sendOperations([op], key); -} + return hiveClient.broadcast.sendOperations([op], key); +}; -export const transferFromSavingsHot = (from: string, to: string, amount: string, memo: string) => { +export const transferToVestingHot = (from: string, to: string, amount: string) => { + const op: Operation = [ + "transfer_to_vesting", + { + from, + to, + amount + } + ]; - const op: Operation = ['transfer_from_savings', { - from, - to, - amount, - memo, - request_id: new Date().getTime() >>> 0 - }]; - - const params: Parameters = {callback: `https://ecency.com/@${from}/wallet`}; - return hs.sendOperation(op, params, () => { - }); -} + const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + return hs.sendOperation(op, params, () => {}); +}; -export const transferFromSavingsKc = (from: string, to: string, amount: string, memo: string) => { - const op: Operation = [ - 'transfer_from_savings', - { - from, - to, - amount, - memo, - request_id: new Date().getTime() >>> 0 - } - ] - - return keychain.broadcast(from, [op], "Active"); -} +export const transferToVestingKc = (from: string, to: string, amount: string) => { + const op: Operation = [ + "transfer_to_vesting", + { + from, + to, + amount + } + ]; -export const transferToVesting = (from: string, key: PrivateKey, to: string, amount: string): Promise => { - const op: Operation = [ - 'transfer_to_vesting', - { - from, - to, - amount - } - ] - - return hiveClient.broadcast.sendOperations([op], key); -} + return keychain.broadcast(from, [op], "Active"); +}; -export const transferToVestingHot = (from: string, to: string, amount: string) => { +export const delegateVestingShares = ( + delegator: string, + key: PrivateKey, + delegatee: string, + vestingShares: string +): Promise => { + const op: Operation = [ + "delegate_vesting_shares", + { + delegator, + delegatee, + vesting_shares: vestingShares + } + ]; - const op: Operation = ['transfer_to_vesting', { - from, - to, - amount - }]; + return hiveClient.broadcast.sendOperations([op], key); +}; - const params: Parameters = {callback: `https://ecency.com/@${from}/wallet`}; - return hs.sendOperation(op, params, () => { - }); -} +export const delegateVestingSharesHot = ( + delegator: string, + delegatee: string, + vestingShares: string +) => { + const op: Operation = [ + "delegate_vesting_shares", + { + delegator, + delegatee, + vesting_shares: vestingShares + } + ]; -export const transferToVestingKc = (from: string, to: string, amount: string) => { - const op: Operation = [ - 'transfer_to_vesting', - { - from, - to, - amount - } - ] - - return keychain.broadcast(from, [op], "Active"); -} + const params: Parameters = { callback: `https://ecency.com/@${delegator}/wallet` }; + return hs.sendOperation(op, params, () => {}); +}; -export const delegateVestingShares = (delegator: string, key: PrivateKey, delegatee: string, vestingShares: string): Promise => { - const op: Operation = [ - 'delegate_vesting_shares', - { - delegator, - delegatee, - vesting_shares: vestingShares - } - ] - - return hiveClient.broadcast.sendOperations([op], key); -} +export const delegateVestingSharesKc = ( + delegator: string, + delegatee: string, + vestingShares: string +) => { + const op: Operation = [ + "delegate_vesting_shares", + { + delegator, + delegatee, + vesting_shares: vestingShares + } + ]; -export const delegateVestingSharesHot = (delegator: string, delegatee: string, vestingShares: string) => { - const op: Operation = ['delegate_vesting_shares', { - delegator, - delegatee, - vesting_shares: vestingShares - }]; + return keychain.broadcast(delegator, [op], "Active"); +}; - const params: Parameters = {callback: `https://ecency.com/@${delegator}/wallet`}; - return hs.sendOperation(op, params, () => { - }); -} +export const delegateRC = ( + delegator: string, + delegatees: string, + max_rc: string | number +): Promise => { + const json = [ + "delegate_rc", + { + from: delegator, + delegatees: delegatees.includes(",") ? delegatees.split(",") : [delegatees], + max_rc: max_rc + } + ]; -export const delegateVestingSharesKc = (delegator: string, delegatee: string, vestingShares: string) => { - const op: Operation = [ - 'delegate_vesting_shares', - { - delegator, - delegatee, - vesting_shares: vestingShares - } - ] - - return keychain.broadcast(delegator, [op], "Active"); -} + return broadcastPostingJSON(delegator, "rc", json); +}; -export const withdrawVesting = (account: string, key: PrivateKey, vestingShares: string): Promise => { - const op: Operation = [ - 'withdraw_vesting', - { - account, - vesting_shares: vestingShares - } - ] +export const withdrawVesting = ( + account: string, + key: PrivateKey, + vestingShares: string +): Promise => { + const op: Operation = [ + "withdraw_vesting", + { + account, + vesting_shares: vestingShares + } + ]; - return hiveClient.broadcast.sendOperations([op], key); -} + return hiveClient.broadcast.sendOperations([op], key); +}; export const withdrawVestingHot = (account: string, vestingShares: string) => { - const op: Operation = ['withdraw_vesting', { - account, - vesting_shares: vestingShares - }]; - - const params: Parameters = {callback: `https://ecency.com/@${account}/wallet`}; - return hs.sendOperation(op, params, () => { - }); -} + const op: Operation = [ + "withdraw_vesting", + { + account, + vesting_shares: vestingShares + } + ]; + + const params: Parameters = { callback: `https://ecency.com/@${account}/wallet` }; + return hs.sendOperation(op, params, () => {}); +}; export const withdrawVestingKc = (account: string, vestingShares: string) => { - const op: Operation = [ - 'withdraw_vesting', - { - account, - vesting_shares: vestingShares - } - ] - - return keychain.broadcast(account, [op], "Active"); -} + const op: Operation = [ + "withdraw_vesting", + { + account, + vesting_shares: vestingShares + } + ]; -export const setWithdrawVestingRoute = (from: string, key: PrivateKey, to: string, percent: number, autoVest: boolean): Promise => { - const op: Operation = [ - 'set_withdraw_vesting_route', - { - from_account: from, - to_account: to, - percent, - auto_vest: autoVest - } - ] - - return hiveClient.broadcast.sendOperations([op], key); -} + return keychain.broadcast(account, [op], "Active"); +}; -export const setWithdrawVestingRouteHot = (from: string, to: string, percent: number, autoVest: boolean) => { - const op: Operation = ['set_withdraw_vesting_route', { - from_account: from, - to_account: to, - percent, - auto_vest: autoVest - }]; - - const params: Parameters = {callback: `https://ecency.com/@${from}/wallet`}; - return hs.sendOperation(op, params, () => { - }); -} +export const setWithdrawVestingRoute = ( + from: string, + key: PrivateKey, + to: string, + percent: number, + autoVest: boolean +): Promise => { + const op: Operation = [ + "set_withdraw_vesting_route", + { + from_account: from, + to_account: to, + percent, + auto_vest: autoVest + } + ]; -export const setWithdrawVestingRouteKc = (from: string, to: string, percent: number, autoVest: boolean) => { - const op: Operation = [ - 'set_withdraw_vesting_route', - { - from_account: from, - to_account: to, - percent, - auto_vest: autoVest - } - ] - - return keychain.broadcast(from, [op], "Active"); -} + return hiveClient.broadcast.sendOperations([op], key); +}; -export const witnessVote = (account: string, key: PrivateKey, witness: string, approve: boolean): Promise => { - const op: Operation = [ - 'account_witness_vote', - { - account, - witness, - approve - } - ] - - return hiveClient.broadcast.sendOperations([op], key); -} +export const setWithdrawVestingRouteHot = ( + from: string, + to: string, + percent: number, + autoVest: boolean +) => { + const op: Operation = [ + "set_withdraw_vesting_route", + { + from_account: from, + to_account: to, + percent, + auto_vest: autoVest + } + ]; -export const witnessVoteHot = (account: string, witness: string, approve: boolean) => { - const params = { - account, - witness, - approve, + const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + return hs.sendOperation(op, params, () => {}); +}; + +export const setWithdrawVestingRouteKc = ( + from: string, + to: string, + percent: number, + autoVest: boolean +) => { + const op: Operation = [ + "set_withdraw_vesting_route", + { + from_account: from, + to_account: to, + percent, + auto_vest: autoVest } + ]; - hotSign("account-witness-vote", params, "witnesses"); -} + return keychain.broadcast(from, [op], "Active"); +}; + +export const witnessVote = ( + account: string, + key: PrivateKey, + witness: string, + approve: boolean +): Promise => { + const op: Operation = [ + "account_witness_vote", + { + account, + witness, + approve + } + ]; + + return hiveClient.broadcast.sendOperations([op], key); +}; + +export const witnessVoteHot = (account: string, witness: string, approve: boolean) => { + const params = { + account, + witness, + approve + }; + + hotSign("account-witness-vote", params, "witnesses"); +}; export const witnessVoteKc = (account: string, witness: string, approve: boolean) => { - return keychain.witnessVote(account, witness, approve); -} + return keychain.witnessVote(account, witness, approve); +}; -export const witnessProxy = (account: string, key: PrivateKey, proxy: string): Promise => { - const op: Operation = [ - 'account_witness_proxy', - { - account, - proxy - } - ] +export const witnessProxy = ( + account: string, + key: PrivateKey, + proxy: string +): Promise => { + const op: Operation = [ + "account_witness_proxy", + { + account, + proxy + } + ]; - return hiveClient.broadcast.sendOperations([op], key); -} + return hiveClient.broadcast.sendOperations([op], key); +}; export const witnessProxyHot = (account: string, proxy: string) => { - const params = { - account, - proxy - } + const params = { + account, + proxy + }; - hotSign("account-witness-proxy", params, "witnesses"); -} + hotSign("account-witness-proxy", params, "witnesses"); +}; export const witnessProxyKc = (account: string, witness: string) => { - return keychain.witnessProxy(account, witness); -} - -export const proposalVote = (account: string, key: PrivateKey, proposal: number, approve: boolean): Promise => { - const op: Operation = [ - 'update_proposal_votes', - { - voter: account, - proposal_ids: [proposal], - approve, - extensions: [] - } - ] - - return hiveClient.broadcast.sendOperations([op], key); -} + return keychain.witnessProxy(account, witness); +}; -export const proposalVoteHot = (account: string, proposal: number, approve: boolean) => { - const params = { - account, - proposal_ids: JSON.stringify( - [proposal] - ), - approve, +export const proposalVote = ( + account: string, + key: PrivateKey, + proposal: number, + approve: boolean +): Promise => { + const op: Operation = [ + "update_proposal_votes", + { + voter: account, + proposal_ids: [proposal], + approve, + extensions: [] } + ]; - hotSign("update-proposal-votes", params, "proposals"); -} - -export const proposalVoteKc = (account: string, proposal: number, approve: boolean) => { - const op: Operation = [ - 'update_proposal_votes', - { - voter: account, - proposal_ids: [proposal], - approve, - extensions: [] - } - ] - - return keychain.broadcast(account, [op], "Active"); + return hiveClient.broadcast.sendOperations([op], key); +}; -} +export const proposalVoteHot = (account: string, proposal: number, approve: boolean) => { + const params = { + account, + proposal_ids: JSON.stringify([proposal]), + approve + }; -export const subscribe = (username: string, community: string): Promise => { - const json = [ - 'subscribe', {community} - ]; + hotSign("update-proposal-votes", params, "proposals"); +}; - return broadcastPostingJSON(username, "community", json); -} +export const proposalVoteKc = (account: string, proposal: number, approve: boolean) => { + const op: Operation = [ + "update_proposal_votes", + { + voter: account, + proposal_ids: [proposal], + approve, + extensions: [] + } + ]; -export const unSubscribe = (username: string, community: string): Promise => { - const json = [ - 'unsubscribe', {community} - ] + return keychain.broadcast(account, [op], "Active"); +}; - return broadcastPostingJSON(username, "community", json); -} +export const subscribe = ( + username: string, + community: string +): Promise => { + const json = ["subscribe", { community }]; -export const promote = (key: PrivateKey, user: string, author: string, permlink: string, duration: number): Promise => { + return broadcastPostingJSON(username, "community", json); +}; - const json = JSON.stringify({ - user, - author, - permlink, - duration - }); +export const unSubscribe = ( + username: string, + community: string +): Promise => { + const json = ["unsubscribe", { community }]; - const op = { - id: 'ecency_promote', - json, - required_auths: [user], - required_posting_auths: [] - }; + return broadcastPostingJSON(username, "community", json); +}; - return hiveClient.broadcast.json(op, key); -} +export const promote = ( + key: PrivateKey, + user: string, + author: string, + permlink: string, + duration: number +): Promise => { + const json = JSON.stringify({ + user, + author, + permlink, + duration + }); + + const op = { + id: "ecency_promote", + json, + required_auths: [user], + required_posting_auths: [] + }; + + return hiveClient.broadcast.json(op, key); +}; export const promoteHot = (user: string, author: string, permlink: string, duration: number) => { - const params = { - authority: "active", - required_auths: `["${user}"]`, - required_posting_auths: "[]", - id: "ecency_promote", - json: JSON.stringify({ - user, - author, - permlink, - duration - }) - } - - hotSign("custom-json", params, `@${user}/points`); -} + const params = { + authority: "active", + required_auths: `["${user}"]`, + required_posting_auths: "[]", + id: "ecency_promote", + json: JSON.stringify({ + user, + author, + permlink, + duration + }) + }; + + hotSign("custom-json", params, `@${user}/points`); +}; export const promoteKc = (user: string, author: string, permlink: string, duration: number) => { - const json = JSON.stringify({ - user, - author, - permlink, - duration - }); - - return keychain.customJson(user, "ecency_promote", "Active", json, "Promote"); -} - -export const boost = (key: PrivateKey, user: string, author: string, permlink: string, amount: string): Promise => { - const json = JSON.stringify({ - user, - author, - permlink, - amount - }); - - const op = { - id: 'ecency_boost', - json, - required_auths: [user], - required_posting_auths: [] - }; + const json = JSON.stringify({ + user, + author, + permlink, + duration + }); + + return keychain.customJson(user, "ecency_promote", "Active", json, "Promote"); +}; - return hiveClient.broadcast.json(op, key); -} +export const boost = ( + key: PrivateKey, + user: string, + author: string, + permlink: string, + amount: string +): Promise => { + const json = JSON.stringify({ + user, + author, + permlink, + amount + }); + + const op = { + id: "ecency_boost", + json, + required_auths: [user], + required_posting_auths: [] + }; + + return hiveClient.broadcast.json(op, key); +}; export const boostHot = (user: string, author: string, permlink: string, amount: string) => { - const params = { - authority: "active", - required_auths: `["${user}"]`, - required_posting_auths: "[]", - id: "ecency_boost", - json: JSON.stringify({ - user, - author, - permlink, - amount - }) - } - - hotSign("custom-json", params, `@${user}/points`); -} + const params = { + authority: "active", + required_auths: `["${user}"]`, + required_posting_auths: "[]", + id: "ecency_boost", + json: JSON.stringify({ + user, + author, + permlink, + amount + }) + }; + + hotSign("custom-json", params, `@${user}/points`); +}; export const boostKc = (user: string, author: string, permlink: string, amount: string) => { - const json = JSON.stringify({ - user, - author, - permlink, - amount - }); - - return keychain.customJson(user, "ecency_boost", "Active", json, "Boost"); -} + const json = JSON.stringify({ + user, + author, + permlink, + amount + }); + + return keychain.customJson(user, "ecency_boost", "Active", json, "Boost"); +}; -export const communityRewardsRegister = (key: PrivateKey, name: string): Promise => { - const json = JSON.stringify({ - name, - }); +export const communityRewardsRegister = ( + key: PrivateKey, + name: string +): Promise => { + const json = JSON.stringify({ + name + }); + + const op = { + id: "ecency_registration", + json, + required_auths: [name], + required_posting_auths: [] + }; + + return hiveClient.broadcast.json(op, key); +}; - const op = { - id: 'ecency_registration', - json, - required_auths: [name], - required_posting_auths: [] - }; +export const communityRewardsRegisterHot = (name: string) => { + const params = { + authority: "active", + required_auths: `["${name}"]`, + required_posting_auths: "[]", + id: "ecency_registration", + json: JSON.stringify({ + name + }) + }; + + hotSign("custom-json", params, `created/${name}`); +}; - return hiveClient.broadcast.json(op, key); -} +export const communityRewardsRegisterKc = (name: string) => { + const json = JSON.stringify({ + name + }); -export const communityRewardsRegisterHot = (name: string) => { - const params = { - authority: "active", - required_auths: `["${name}"]`, - required_posting_auths: "[]", - id: "ecency_registration", - json: JSON.stringify({ - name - }) + return keychain.customJson(name, "ecency_registration", "Active", json, "Community Registration"); +}; + +export const updateProfile = ( + account: Account, + newProfile: any +): Promise => { + const params = { + account: account.name, + json_metadata: "", + posting_json_metadata: JSON.stringify({ profile: { ...newProfile, version: 2 } }), + extensions: [] + }; + + const opArray: Operation[] = [["account_update2", params]]; + + return broadcastPostingOperations(account.name, opArray); +}; + +export const grantPostingPermission = (key: PrivateKey, account: Account, pAccount: string) => { + if (!account.__loaded) { + throw "posting|memo_key|json_metadata required with account instance"; + } + + const newPosting = Object.assign( + {}, + { ...account.posting }, + { + account_auths: [ + ...account.posting.account_auths, + [pAccount, account.posting.weight_threshold] + ] } + ); + + // important! + newPosting.account_auths.sort((a, b) => (a[0] > b[0] ? 1 : -1)); + + return hiveClient.broadcast.updateAccount( + { + account: account.name, + posting: newPosting, + active: undefined, + memo_key: account.memo_key, + json_metadata: account.json_metadata + }, + key + ); +}; - hotSign("custom-json", params, `created/${name}`); -} +export const revokePostingPermission = (key: PrivateKey, account: Account, pAccount: string) => { + if (!account.__loaded) { + throw "posting|memo_key|json_metadata required with account instance"; + } + + const newPosting = Object.assign( + {}, + { ...account.posting }, + { + account_auths: account.posting.account_auths.filter((x) => x[0] !== pAccount) + } + ); + + return hiveClient.broadcast.updateAccount( + { + account: account.name, + posting: newPosting, + memo_key: account.memo_key, + json_metadata: account.json_metadata + }, + key + ); +}; -export const communityRewardsRegisterKc = (name: string) => { - const json = JSON.stringify({ - name - }); +export const setUserRole = ( + username: string, + community: string, + account: string, + role: string +): Promise => { + const json = ["setRole", { community, account, role }]; - return keychain.customJson(name, "ecency_registration", "Active", json, "Community Registration"); -} + return broadcastPostingJSON(username, "community", json); +}; + +export const updateCommunity = ( + username: string, + community: string, + props: { + title: string; + about: string; + lang: string; + description: string; + flag_text: string; + is_nsfw: boolean; + } +): Promise => { + const json = ["updateProps", { community, props }]; + + return broadcastPostingJSON(username, "community", json); +}; + +export const pinPost = ( + username: string, + community: string, + account: string, + permlink: string, + pin: boolean +): Promise => { + const json = [pin ? "pinPost" : "unpinPost", { community, account, permlink }]; + + return broadcastPostingJSON(username, "community", json); +}; + +export const mutePost = ( + username: string, + community: string, + account: string, + permlink: string, + notes: string, + mute: boolean +): Promise => { + const json = [mute ? "mutePost" : "unmutePost", { community, account, permlink, notes }]; + + return broadcastPostingJSON(username, "community", json); +}; + +export const hiveNotifySetLastRead = (username: string): Promise => { + const now = new Date().toISOString(); + const date = now.split(".")[0]; + + const params = { + id: "notify", + required_auths: [], + required_posting_auths: [username], + json: JSON.stringify(["setLastRead", { date }]) + }; + const params1 = { + id: "ecency_notify", + required_auths: [], + required_posting_auths: [username], + json: JSON.stringify(["setLastRead", { date }]) + }; + + const opArray: Operation[] = [ + ["custom_json", params], + ["custom_json", params1] + ]; + + return broadcastPostingOperations(username, opArray); +}; + +export const updatePassword = ( + update: AccountUpdateOperation[1], + ownerKey: PrivateKey +): Promise => hiveClient.broadcast.updateAccount(update, ownerKey); + +// HE Operations +export const transferHiveEngineKc = ( + from: string, + to: string, + symbol: string, + amount: string, + memo: string +) => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "transfer", + contractPayload: { + symbol, + to, + quantity: amount.toString(), + memo + } + }); + + return keychain.customJson(from, "ssc-mainnet-hive", "Active", json, "Transfer"); +}; +export const delegateHiveEngineKc = (from: string, to: string, symbol: string, amount: string) => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "delegate", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }); + + return keychain.customJson(from, "ssc-mainnet-hive", "Active", json, "Transfer"); +}; +export const undelegateHiveEngineKc = ( + from: string, + to: string, + symbol: string, + amount: string +) => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "undelegate", + contractPayload: { + symbol, + from: to, + quantity: amount.toString() + } + }); + + return keychain.customJson(from, "ssc-mainnet-hive", "Active", json, "Transfer"); +}; +export const stakeHiveEngineKc = (from: string, to: string, symbol: string, amount: string) => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "stake", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }); + + return keychain.customJson(from, "ssc-mainnet-hive", "Active", json, "Transfer"); +}; +export const unstakeHiveEngineKc = (from: string, to: string, symbol: string, amount: string) => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "unstake", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }); + + return keychain.customJson(from, "ssc-mainnet-hive", "Active", json, "Transfer"); +}; + +// HE Hive Signer Operations +export const transferHiveEngineHs = ( + from: string, + to: string, + symbol: string, + amount: string, + memo: string +): any => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: "ssc-mainnet-hive", + json: JSON.stringify({ + contractName: "tokens", + contractAction: "transfer", + contractPayload: { + symbol, + to, + quantity: amount.toString(), + memo + } + }) + }; + + return hotSign("custom-json", params, `@${from}/engine`); +}; + +export const delegateHiveEngineHs = ( + from: string, + to: string, + symbol: string, + amount: string +): any => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: "ssc-mainnet-hive", + json: JSON.stringify({ + contractName: "tokens", + contractAction: "delegate", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }) + }; + + return hotSign("custom-json", params, `@${from}/engine`); +}; + +export const undelegateHiveEngineHs = ( + from: string, + to: string, + symbol: string, + amount: string +): any => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: "ssc-mainnet-hive", + json: JSON.stringify({ + contractName: "tokens", + contractAction: "undelegate", + contractPayload: { + symbol, + from: to, + quantity: amount.toString() + } + }) + }; + + return hotSign("custom-json", params, `@${from}/engine`); +}; -export const updateProfile = (account: Account, newProfile: { - name: string, - about: string, - website: string, - location: string, - cover_image: string, - profile_image: string, -}): Promise => { - const params = { - account: account.name, - json_metadata: '', - posting_json_metadata: JSON.stringify({profile: {...newProfile, version: 2}}), - extensions: [] +export const stakeHiveEngineHs = ( + from: string, + to: string, + symbol: string, + amount: string +): any => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: "ssc-mainnet-hive", + json: JSON.stringify({ + contractName: "tokens", + contractAction: "stake", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }) + }; + + return hotSign("custom-json", params, `@${from}/engine`); +}; + +export const unstakeHiveEngineHs = ( + from: string, + to: string, + symbol: string, + amount: string +): any => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: "ssc-mainnet-hive", + json: JSON.stringify({ + contractName: "tokens", + contractAction: "unstake", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }) + }; + + return hotSign("custom-json", params, `@${from}/engine`); +}; + +//HE Key Operations +export const transferHiveEngineKey = async ( + from: string, + key: PrivateKey, + symbol: string, + to: string, + amount: string, + memo: string +): Promise => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "transfer", + contractPayload: { + symbol, + to, + quantity: amount.toString(), + memo + } + }); + + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [from], + required_posting_auths: [] + }; + + const result = await hiveClient.broadcast.json(op, key); + + return result; +}; + +export const delegateHiveEngineKey = async ( + from: string, + key: PrivateKey, + symbol: string, + to: string, + amount: string +): Promise => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "delegate", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }); + + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [from], + required_posting_auths: [] + }; + + const result = await hiveClient.broadcast.json(op, key); + return result; +}; + +export const undelegateHiveEngineKey = async ( + from: string, + key: PrivateKey, + symbol: string, + to: string, + amount: string +): Promise => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "undelegate", + contractPayload: { + symbol, + from: to, + quantity: amount.toString() + } + }); + + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [from], + required_posting_auths: [] + }; + + const result = await hiveClient.broadcast.json(op, key); + return result; +}; + +export const stakeHiveEngineKey = async ( + from: string, + key: PrivateKey, + symbol: string, + to: string, + amount: string +): Promise => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "stake", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }); + + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [from], + required_posting_auths: [] + }; + + const result = await hiveClient.broadcast.json(op, key); + return result; +}; + +export const unstakeHiveEngineKey = async ( + from: string, + key: PrivateKey, + symbol: string, + to: string, + amount: string +): Promise => { + const json = JSON.stringify({ + contractName: "tokens", + contractAction: "stake", + contractPayload: { + symbol, + to, + quantity: amount.toString() + } + }); + + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [from], + required_posting_auths: [] + }; + + const result = await hiveClient.broadcast.json(op, key); + return result; +}; + +export const Revoke = ( + account: string, + weight_threshold: number, + account_auths: [string, number][], + key_auths: any[], + memo_key: string, + key: PrivateKey +): Promise => { + const newPosting = { + weight_threshold, + account_auths, + key_auths + }; + + const op: Operation = [ + "account_update", + { + account, + posting: newPosting, + memo_key, + json_metadata: "" + } + ]; + return hiveClient.broadcast.sendOperations([op], key); +}; + +export const RevokeHot = ( + account: string, + weight_threshold: number, + account_auths: [string, number][], + key_auths: any[], + memo_key: string +) => { + const newPosting = { + weight_threshold, + account_auths, + key_auths + }; + const op: Operation = [ + "account_update", + { + account, + posting: newPosting, + memo_key, + json_metadata: "" + } + ]; + + const params: Parameters = { callback: `https://ecency.com/@${account}/permissions` }; + return hs.sendOperation(op, params, () => {}); +}; + +export const RevokeKc = ( + account: string, + weight_threshold: number, + account_auths: [string, number][], + key_auths: any[], + memo_key: string +) => { + const newPosting = { + weight_threshold, + account_auths, + key_auths + }; + const op: Operation = [ + "account_update", + { + account, + posting: newPosting, + memo_key, + json_metadata: "" + } + ]; + return keychain.broadcast(account, [op], "Active"); +}; + +// Create account with hive keychain +export const createAccountKc = async (data: any, creator_account: string) => { + try { + const { username, pub_keys, fee } = data; + + const account = { + name: username, + ...pub_keys, + active: false }; - const opArray: Operation[] = [["account_update2", params]]; + const op_name: OperationName = "account_create"; - return broadcastPostingOperations(account.name, opArray); -} + const owner = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.ownerPublicKey, 1]] + }; + const active = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.activePublicKey, 1]] + }; + const posting = { + weight_threshold: 1, + account_auths: [["ecency.app", 1]], + key_auths: [[account.postingPublicKey, 1]] + }; + const ops: Array = []; + const params: any = { + creator: creator_account, + new_account_name: account.name, + owner, + active, + posting, + memo_key: account.memoPublicKey, + json_metadata: "", + extensions: [], + fee + }; -export const grantPostingPermission = (key: PrivateKey, account: Account, pAccount: string) => { - if (!account.__loaded) { - throw "posting|memo_key|json_metadata required with account instance"; + const operation: Operation = [op_name, params]; + ops.push(operation); + try { + // For Keychain + const newAccount = await keychain.broadcast(creator_account, [operation], "Active"); + return newAccount; + } catch (err: any) { + console.log(err); + return err.jse_info.name; } + } catch (err) { + return err; + } +}; - const newPosting = Object.assign( - {}, - {...account.posting}, - { - account_auths: [ - ...account.posting.account_auths, - [pAccount, account.posting.weight_threshold] - ] - } - ); +// Create account with hive Hs +export const createAccountHs = async (data: any, creator_account: string, hash: string) => { + try { + const { username, pub_keys, fee } = data; + + const account = { + name: username, + ...pub_keys, + active: false + }; + + const op_name: OperationName = "account_create"; + + const owner = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.ownerPublicKey, 1]] + }; + const active = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.activePublicKey, 1]] + }; + const posting = { + weight_threshold: 1, + account_auths: [["ecency.app", 1]], + key_auths: [[account.postingPublicKey, 1]] + }; - // important! - newPosting.account_auths.sort((a, b) => (a[0] > b[0] ? 1 : -1)); + const params: any = { + creator: creator_account, + new_account_name: account.name, + owner, + active, + posting, + memo_key: account.memoPublicKey, + json_metadata: "", + extensions: [], + fee + }; - return hiveClient.broadcast.updateAccount({ - account: account.name, - posting: newPosting, - active: undefined, - memo_key: account.memo_key, - json_metadata: account.json_metadata - }, key); + const operation: Operation = [op_name, params]; + + try { + // For Hive Signer + const params: Parameters = { + callback: `https://ecency.com/onboard-friend/confirming/${hash}?tid={{id}}` + }; + const newAccount = hs.sendOperation(operation, params, () => {}); + return newAccount; + } catch (err: any) { + console.log(err); + return err.jse_info.name; + } + } catch (err) { + return err; + } }; -export const revokePostingPermission = (key: PrivateKey, account: Account, pAccount: string) => { - if (!account.__loaded) { - throw "posting|memo_key|json_metadata required with account instance"; - } - - const newPosting = Object.assign( - {}, - {...account.posting}, - { - account_auths: account.posting.account_auths.filter(x => x[0] !== pAccount) - } - ); - - return hiveClient.broadcast.updateAccount( - { - account: account.name, - posting: newPosting, - memo_key: account.memo_key, - json_metadata: account.json_metadata - }, - key - ); -}; - -export const setUserRole = (username: string, community: string, account: string, role: string): Promise => { - const json = [ - 'setRole', {community, account, role} - ] - - return broadcastPostingJSON(username, "community", json); -} +// Create account with hive key +export const createAccountKey = async ( + data: any, + creator_account: string, + creator_key: PrivateKey +) => { + try { + const { username, pub_keys, fee } = data; + + const account = { + name: username, + ...pub_keys, + active: false + }; -export const updateCommunity = (username: string, community: string, props: { title: string, about: string, lang: string, description: string, flag_text: string, is_nsfw: boolean }): Promise => { - const json = [ - 'updateProps', {community, props} - ]; + let tokens: any = await hiveClient.database.getAccounts([creator_account]); + tokens = tokens[0]?.pending_claimed_accounts; - return broadcastPostingJSON(username, "community", json); -} + let op_name: OperationName = "account_create"; -export const pinPost = (username: string, community: string, account: string, permlink: string, pin: boolean): Promise => { - const json = [ - pin ? 'pinPost' : 'unpinPost', {community, account, permlink} - ] + const owner = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.ownerPublicKey, 1]] + }; + const active = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.activePublicKey, 1]] + }; + const posting = { + weight_threshold: 1, + account_auths: [["ecency.app", 1]], + key_auths: [[account.postingPublicKey, 1]] + }; + const ops: Array = []; + const params: any = { + creator: creator_account, + new_account_name: account?.name, + owner, + active, + posting, + memo_key: account.memoPublicKey, + json_metadata: "", + extensions: [], + fee + }; - return broadcastPostingJSON(username, "community", json); -} + const operation: Operation = [op_name, params]; + ops.push(operation); + + try { + // With Private Key + const newAccount = await hiveClient.broadcast.sendOperations(ops, creator_key); + return newAccount; + } catch (err: any) { + console.log(err.message); + return err.jse_info.name; + } + } catch (err) { + console.log(err); + return err; + } +}; -export const mutePost = (username: string, community: string, account: string, permlink: string, notes: string, mute: boolean): Promise => { - const json = [ - mute ? 'mutePost' : 'unmutePost', {community, account, permlink, notes} - ]; +// Create account with credit Kc +export const createAccountWithCreditKc = async (data: any, creator_account: string) => { + try { + const { username, pub_keys } = data; - return broadcastPostingJSON(username, "community", json); -} + const account = { + name: username, + ...pub_keys, + active: false + }; -export const hiveNotifySetLastRead = (username: string): Promise => { - const now = new Date().toISOString(); - const date = now.split(".")[0]; + let tokens: any = await hiveClient.database.getAccounts([creator_account]); + tokens = tokens[0]?.pending_claimed_accounts; + + let fee = null; + let op_name: OperationName = "create_claimed_account"; - const params = { - id: "notify", - required_auths: [], - required_posting_auths: [username], - json: JSON.stringify(['setLastRead', {date}]) + const owner = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.ownerPublicKey, 1]] + }; + const active = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.activePublicKey, 1]] + }; + const posting = { + weight_threshold: 1, + account_auths: [["ecency.app", 1]], + key_auths: [[account.postingPublicKey, 1]] + }; + const ops: Array = []; + const params: any = { + creator: creator_account, + new_account_name: account.name, + owner, + active, + posting, + memo_key: account.memoPublicKey, + json_metadata: "", + extensions: [] + }; + + if (fee) params.fee = fee; + const operation: Operation = [op_name, params]; + ops.push(operation); + try { + // For Keychain + const newAccount = await keychain.broadcast(creator_account, [operation], "Active"); + return newAccount; + } catch (err: any) { + return err.jse_info.name; } - const params1 = { - id: "ecency_notify", - required_auths: [], - required_posting_auths: [username], - json: JSON.stringify(['setLastRead', {date}]) + } catch (err) { + console.log(err); + return err; + } +}; + +// Create account with credit Hs +export const createAccountWithCreditHs = async ( + data: any, + creator_account: string, + hash: string +) => { + try { + const { username, pub_keys } = data; + + const account = { + name: username, + ...pub_keys, + active: false + }; + + let tokens: any = await hiveClient.database.getAccounts([creator_account]); + tokens = tokens[0]?.pending_claimed_accounts; + + let fee = null; + let op_name: OperationName = "create_claimed_account"; + + const owner = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.ownerPublicKey, 1]] + }; + const active = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.activePublicKey, 1]] + }; + const posting = { + weight_threshold: 1, + account_auths: [["ecency.app", 1]], + key_auths: [[account.postingPublicKey, 1]] + }; + + const params: any = { + creator: creator_account, + new_account_name: account.name, + owner, + active, + posting, + memo_key: account.memoPublicKey, + json_metadata: "", + extensions: [] + }; + + if (fee) params.fee = fee; + const operation: Operation = [op_name, params]; + + try { + // For Hive Signer + const params: Parameters = { + callback: `https://ecency.com/onboard-friend/confirming/${hash}?tid={{id}}` + }; + console.log(params); + const newAccount = hs.sendOperation(operation, params, () => {}); + return newAccount; + } catch (err: any) { + console.log(err); + return err.jse_info.name; } + } catch (err) { + return err; + } +}; - const opArray: Operation[] = [['custom_json', params], ['custom_json', params1]]; +// Create account with credit key +export const createAccountWithCreditKey = async ( + data: any, + creator_account: string, + creator_key: PrivateKey +) => { + try { + const { username, pub_keys } = data; + + const account = { + name: username, + ...pub_keys, + active: false + }; - return broadcastPostingOperations(username, opArray); -} + let tokens: any = await hiveClient.database.getAccounts([creator_account]); + tokens = tokens[0]?.pending_claimed_accounts; + + let fee = null; + let op_name: OperationName = "create_claimed_account"; -export const updatePassword = (update: AccountUpdateOperation[1], ownerKey: PrivateKey): Promise => hiveClient.broadcast.updateAccount(update, ownerKey) + const owner = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.ownerPublicKey, 1]] + }; + const active = { + weight_threshold: 1, + account_auths: [], + key_auths: [[account.activePublicKey, 1]] + }; + const posting = { + weight_threshold: 1, + account_auths: [["ecency.app", 1]], + key_auths: [[account.postingPublicKey, 1]] + }; + const ops: Array = []; + const params: any = { + creator: creator_account, + new_account_name: account?.name, + owner, + active, + posting, + memo_key: account.memoPublicKey, + json_metadata: "", + extensions: [] + }; + + if (fee) params.fee = fee; + const operation: Operation = [op_name, params]; + ops.push(operation); + + try { + // With Private Key + const newAccount = await hiveClient.broadcast.sendOperations(ops, creator_key); + return newAccount; + } catch (err: any) { + console.log(err.message); + return err.jse_info.name; + } + } catch (err) { + return err; + } +}; diff --git a/src/common/api/private-api.ts b/src/common/api/private-api.ts index 9218a5d6112..84cc539eb51 100644 --- a/src/common/api/private-api.ts +++ b/src/common/api/private-api.ts @@ -1,391 +1,640 @@ import axios from "axios"; -import {PointTransaction} from "../store/points/types"; -import {ApiNotification, NotificationFilter} from "../store/notifications/types"; -import {Entry} from "../store/entries/types"; - -import {getAccessToken} from "../helper/user-token"; - -import {apiBase} from "./helper"; - -import {AppWindow} from "../../client/window"; +import { PointTransaction } from "../store/points/types"; +import { + ApiNotification, + ApiNotificationSetting, + NotificationFilter +} from "../store/notifications/types"; +import { Entry } from "../store/entries/types"; + +import { getAccessToken } from "../helper/user-token"; + +import { apiBase } from "./helper"; +import { AppWindow } from "../../client/window"; +import isElectron from "../util/is-electron"; +import { NotifyTypes } from "../enums"; +import { BeneficiaryRoute, MetaData, RewardType } from "./operations"; declare var window: AppWindow; export interface ReceivedVestingShare { - delegatee: string; - delegator: string; - timestamp: string; - vesting_shares: string; + delegatee: string; + delegator: string; + timestamp: string; + vesting_shares: string; } export const getReceivedVestingShares = (username: string): Promise => - axios.get(apiBase(`/private-api/received-vesting/${username}`)).then((resp) => resp.data.list); - + axios.get(apiBase(`/private-api/received-vesting/${username}`)).then((resp) => resp.data.list); export interface RewardedCommunity { - start_date: string; - total_rewards: string; - name: string; + start_date: string; + total_rewards: string; + name: string; } export const getRewardedCommunities = (): Promise => - axios.get(apiBase(`/private-api/rewarded-communities`)).then((resp) => resp.data); + axios.get(apiBase(`/private-api/rewarded-communities`)).then((resp) => resp.data); export interface LeaderBoardItem { - _id: string; - count: number; - points: string + _id: string; + count: number; + points: string; } export type LeaderBoardDuration = "day" | "week" | "month"; export const getLeaderboard = (duration: LeaderBoardDuration): Promise => { - return axios.get(apiBase(`/private-api/leaderboard/${duration}`)).then(resp => resp.data); + return axios.get(apiBase(`/private-api/leaderboard/${duration}`)).then((resp) => resp.data); }; export interface CurationItem { - efficiency: number; - account: string; - vests: number; - votes: number; - uniques: number; + efficiency: number; + account: string; + vests: number; + votes: number; + uniques: number; } export type CurationDuration = "day" | "week" | "month"; export const getCuration = (duration: CurationDuration): Promise => { - return axios.get(apiBase(`/private-api/curation/${duration}`)).then(resp => resp.data); + return axios.get(apiBase(`/private-api/curation/${duration}`)).then((resp) => resp.data); }; export const signUp = (username: string, email: string, referral: string): Promise => - axios - .post(apiBase(`/private-api/account-create`), { - username: username, - email: email, - referral: referral - }) - .then(resp => { - return resp; - }); + axios + .post(apiBase(`/private-api/account-create`), { + username: username, + email: email, + referral: referral + }) + .then((resp) => { + return resp; + }); export const subscribeEmail = (email: string): Promise => - axios - .post(apiBase(`/private-api/subscribe`), { - email: email - }) - .then(resp => { - return resp; - }); - -export const usrActivity = (username: string, ty: number, bl: string | number = '', tx: string | number = '') => { - if (!window.usePrivate) { - return new Promise((resolve) => resolve(null)); - } - - const params: { - code: string | undefined; - ty: number; - bl?: string | number; - tx?: string | number; - } = {code: getAccessToken(username), ty}; - - if (bl) params.bl = bl; - if (tx) params.tx = tx; + axios + .post(apiBase(`/private-api/subscribe`), { + email: email + }) + .then((resp) => { + return resp; + }); - return axios.post(apiBase(`/private-api/usr-activity`), params); +export const usrActivity = ( + username: string, + ty: number, + bl: string | number = "", + tx: string | number = "" +) => { + if (!window.usePrivate) { + return new Promise((resolve) => resolve(null)); + } + + const params: { + code: string | undefined; + ty: number; + bl?: string | number; + tx?: string | number; + } = { code: getAccessToken(username), ty }; + + if (bl) params.bl = bl; + if (tx) params.tx = tx; + + return axios.post(apiBase(`/private-api/usr-activity`), params); }; -export const getNotifications = (username: string, filter: NotificationFilter | null, since: string | null = null): Promise => { - - const data: { code: string | undefined; filter?: string, since?: string } = {code: getAccessToken(username)}; - - if (filter) { - data.filter = filter; - } +export const getNotifications = ( + username: string, + filter: NotificationFilter | null, + since: string | null = null, + user: string | null = null +): Promise => { + const data: { code: string | undefined; filter?: string; since?: string; user?: string } = { + code: getAccessToken(username) + }; + + if (filter) { + data.filter = filter; + } + + if (since) { + data.since = since; + } + + if (user) { + data.user = user; + } + + return axios.post(apiBase(`/private-api/notifications`), data).then((resp) => resp.data); +}; - if (since) { - data.since = since; - } +export const saveNotificationSetting = ( + username: string, + system: string, + allows_notify: number, + notify_types: number[], + token: string +): Promise => { + const data = { + code: getAccessToken(username), + username, + token, + system, + allows_notify, + notify_types + }; + return axios.post(apiBase(`/private-api/register-device`), data).then((resp) => resp.data); +}; - return axios.post(apiBase(`/private-api/notifications`), data).then(resp => resp.data); +export const getNotificationSetting = ( + username: string, + token: string +): Promise => { + const data = { code: getAccessToken(username), username, token }; + return axios.post(apiBase(`/private-api/detail-device`), data).then((resp) => resp.data); }; -export const getCurrencyTokenRate = (currency:string, token:string): Promise => - axios.get(apiBase(`/private-api/market-data/${currency==="hbd" ? "usd" : currency}/${token}`)).then((resp:any) => resp.data) +export const getCurrencyTokenRate = (currency: string, token: string): Promise => + axios + .get(apiBase(`/private-api/market-data/${currency === "hbd" ? "usd" : currency}/${token}`)) + .then((resp: any) => resp.data); + +export const getCurrencyRates = (): Promise<{ + [currency: string]: { + quotes: { + [currency: string]: { + last_updated: string; + percent_change: number; + price: number; + }; + }; + }; +}> => axios.get(apiBase("/private-api/market-data/latest")).then((resp: any) => resp.data); export const getUnreadNotificationCount = (username: string): Promise => { - const data = {code: getAccessToken(username)}; + const data = { code: getAccessToken(username) }; - return data.code ? axios - .post(apiBase(`/private-api/notifications/unread`), data) - .then(resp => resp.data.count) : Promise.resolve(0); -} + return data.code + ? axios.post(apiBase(`/private-api/notifications/unread`), data).then((resp) => resp.data.count) + : Promise.resolve(0); +}; export const markNotifications = (username: string, id: string | null = null) => { - const data: { code: string | undefined; id?: string } = {code: getAccessToken(username)} - if (id) { - data.id = id; - } + const data: { code: string | undefined; id?: string } = { code: getAccessToken(username) }; + if (id) { + data.id = id; + } - return axios.post(apiBase(`/private-api/notifications/mark`), data); + return axios.post(apiBase(`/private-api/notifications/mark`), data); }; export interface UserImage { - created: string - timestamp: number - url: string - _id: string + created: string; + timestamp: number; + url: string; + _id: string; } export const getImages = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/images`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/images`), data).then((resp) => resp.data); +}; export const deleteImage = (username: string, imageID: string): Promise => { - const data = {code: getAccessToken(username), id: imageID}; - return axios.post(apiBase(`/private-api/images-delete`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username), id: imageID }; + return axios.post(apiBase(`/private-api/images-delete`), data).then((resp) => resp.data); +}; export const addImage = (username: string, url: string): Promise => { - const data = {code: getAccessToken(username), url: url}; - return axios.post(apiBase(`/private-api/images-add`), data).then(resp => resp.data); + const data = { code: getAccessToken(username), url: url }; + return axios.post(apiBase(`/private-api/images-add`), data).then((resp) => resp.data); +}; + +export interface DraftMetadata extends MetaData { + beneficiaries: BeneficiaryRoute[]; + rewardType: RewardType; } export interface Draft { - body: string - created: string - post_type: string - tags: string - timestamp: number - title: string - _id: string + body: string; + created: string; + post_type: string; + tags: string; + timestamp: number; + title: string; + _id: string; + meta?: DraftMetadata; } export const getDrafts = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/drafts`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/drafts`), data).then((resp) => resp.data); +}; -export const addDraft = (username: string, title: string, body: string, tags: string): Promise<{ drafts: Draft[] }> => { - const data = {code: getAccessToken(username), title, body, tags}; - return axios.post(apiBase(`/private-api/drafts-add`), data).then(resp => resp.data); -} +export const addDraft = ( + username: string, + title: string, + body: string, + tags: string, + meta: DraftMetadata +): Promise<{ drafts: Draft[] }> => { + const data = { code: getAccessToken(username), title, body, tags, meta }; + return axios.post(apiBase(`/private-api/drafts-add`), data).then((resp) => resp.data); +}; -export const updateDraft = (username: string, draftId: string, title: string, body: string, tags: string): Promise => { - const data = {code: getAccessToken(username), id: draftId, title, body, tags}; - return axios.post(apiBase(`/private-api/drafts-update`), data).then(resp => resp.data); -} +export const updateDraft = ( + username: string, + draftId: string, + title: string, + body: string, + tags: string, + meta: DraftMetadata +): Promise => { + const data = { code: getAccessToken(username), id: draftId, title, body, tags, meta }; + return axios.post(apiBase(`/private-api/drafts-update`), data).then((resp) => resp.data); +}; export const deleteDraft = (username: string, draftId: string): Promise => { - const data = {code: getAccessToken(username), id: draftId}; - return axios.post(apiBase(`/private-api/drafts-delete`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username), id: draftId }; + return axios.post(apiBase(`/private-api/drafts-delete`), data).then((resp) => resp.data); +}; export interface Schedule { - _id: string; - username: string; - permlink: string; - title: string; - body: string; - tags: string[]; - tags_arr: string; - schedule: string; - original_schedule: string; - reblog: boolean; - status: 1 | 2 | 3 | 4; - message: string | null + _id: string; + username: string; + permlink: string; + title: string; + body: string; + tags: string[]; + tags_arr: string; + schedule: string; + original_schedule: string; + reblog: boolean; + status: 1 | 2 | 3 | 4; + message: string | null; } export const getSchedules = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/schedules`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/schedules`), data).then((resp) => resp.data); +}; -export const addSchedule = (username: string, permlink: string, title: string, body: string, meta: {}, options: {}, schedule: string, reblog: boolean): Promise => { - const data = {code: getAccessToken(username), permlink, title, body, meta, options, schedule, reblog} - return axios.post(apiBase(`/private-api/schedules-add`), data).then(resp => resp.data); -} +export const addSchedule = ( + username: string, + permlink: string, + title: string, + body: string, + meta: {}, + options: {}, + schedule: string, + reblog: boolean +): Promise => { + const data = { + code: getAccessToken(username), + permlink, + title, + body, + meta, + options, + schedule, + reblog + }; + return axios.post(apiBase(`/private-api/schedules-add`), data).then((resp) => resp.data); +}; export const deleteSchedule = (username: string, id: string): Promise => { - const data = {code: getAccessToken(username), id}; - return axios.post(apiBase(`/private-api/schedules-delete`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username), id }; + return axios.post(apiBase(`/private-api/schedules-delete`), data).then((resp) => resp.data); +}; export const moveSchedule = (username: string, id: string): Promise => { - const data = {code: getAccessToken(username), id}; - return axios.post(apiBase(`/private-api/schedules-move`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username), id }; + return axios.post(apiBase(`/private-api/schedules-move`), data).then((resp) => resp.data); +}; export interface Bookmark { - _id: string, - author: string, - permlink: string, - timestamp: number, - created: string + _id: string; + author: string; + permlink: string; + timestamp: number; + created: string; } export const getBookmarks = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/bookmarks`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/bookmarks`), data).then((resp) => resp.data); +}; -export const addBookmark = (username: string, author: string, permlink: string): Promise<{ bookmarks: Bookmark[] }> => { - const data = {code: getAccessToken(username), author, permlink}; - return axios.post(apiBase(`/private-api/bookmarks-add`), data).then(resp => resp.data); -} +export const addBookmark = ( + username: string, + author: string, + permlink: string +): Promise<{ bookmarks: Bookmark[] }> => { + const data = { code: getAccessToken(username), author, permlink }; + return axios.post(apiBase(`/private-api/bookmarks-add`), data).then((resp) => resp.data); +}; export const deleteBookmark = (username: string, bookmarkId: string): Promise => { - const data = {code: getAccessToken(username), id: bookmarkId}; - return axios.post(apiBase(`/private-api/bookmarks-delete`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username), id: bookmarkId }; + return axios.post(apiBase(`/private-api/bookmarks-delete`), data).then((resp) => resp.data); +}; export interface Favorite { - _id: string, - account: string, - timestamp: number, + _id: string; + account: string; + timestamp: number; } export const getFavorites = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/favorites`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/favorites`), data).then((resp) => resp.data); +}; export const checkFavorite = (username: string, account: string): Promise => { - const data = {code: getAccessToken(username), account}; - return axios.post(apiBase(`/private-api/favorites-check`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username), account }; + return axios.post(apiBase(`/private-api/favorites-check`), data).then((resp) => resp.data); +}; -export const addFavorite = (username: string, account: string): Promise<{ favorites: Favorite[] }> => { - const data = {code: getAccessToken(username), account}; - return axios.post(apiBase(`/private-api/favorites-add`), data).then(resp => resp.data); -} +export const addFavorite = ( + username: string, + account: string +): Promise<{ favorites: Favorite[] }> => { + const data = { code: getAccessToken(username), account }; + return axios.post(apiBase(`/private-api/favorites-add`), data).then((resp) => resp.data); +}; export const deleteFavorite = (username: string, account: string): Promise => { - const data = {code: getAccessToken(username), account}; - return axios.post(apiBase(`/private-api/favorites-delete`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username), account }; + return axios.post(apiBase(`/private-api/favorites-delete`), data).then((resp) => resp.data); +}; export interface Fragment { - id: string; - title: string; - body: string; - created: string; - modified: string; + id: string; + title: string; + body: string; + created: string; + modified: string; } export const getFragments = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/fragments`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/fragments`), data).then((resp) => resp.data); +}; -export const addFragment = (username: string, title: string, body: string): Promise<{ fragments: Fragment[] }> => { - const data = {code: getAccessToken(username), title, body}; - return axios.post(apiBase(`/private-api/fragments-add`), data).then(resp => resp.data); -} +export const addFragment = ( + username: string, + title: string, + body: string +): Promise<{ fragments: Fragment[] }> => { + const data = { code: getAccessToken(username), title, body }; + return axios.post(apiBase(`/private-api/fragments-add`), data).then((resp) => resp.data); +}; -export const updateFragment = (username: string, fragmentId: string, title: string, body: string): Promise => { - const data = {code: getAccessToken(username), id: fragmentId, title, body}; - return axios.post(apiBase(`/private-api/fragments-update`), data).then(resp => resp.data); -} +export const updateFragment = ( + username: string, + fragmentId: string, + title: string, + body: string +): Promise => { + const data = { code: getAccessToken(username), id: fragmentId, title, body }; + return axios.post(apiBase(`/private-api/fragments-update`), data).then((resp) => resp.data); +}; export const deleteFragment = (username: string, fragmentId: string): Promise => { - const data = {code: getAccessToken(username), id: fragmentId}; - return axios.post(apiBase(`/private-api/fragments-delete`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username), id: fragmentId }; + return axios.post(apiBase(`/private-api/fragments-delete`), data).then((resp) => resp.data); +}; -export const getPoints = (username: string): Promise<{ - points: string; - unclaimed_points: string; +export const getPoints = async ( + username: string, + usePrivate?: boolean +): Promise<{ + points: string; + unclaimed_points: string; }> => { - if (window.usePrivate) { - const data = {username}; - return axios.post(apiBase(`/private-api/points`), data).then(resp => resp.data); - } - - return new Promise((resolve) => { - resolve({ - points: "0.000", - unclaimed_points: "0.000" - }) - }); -} - -export const getPointTransactions = (username: string, type?: number): Promise => { - if (window.usePrivate) { - const data = {username, type}; - return axios.post(apiBase(`/private-api/point-list`), data).then(resp => resp.data); - } + if (usePrivate ?? window.usePrivate) { + const data = { username }; + return axios.post(apiBase(`/private-api/points`), data).then((resp) => resp.data); + } + return { + points: "0.000", + unclaimed_points: "0.000" + }; +}; - return new Promise((resolve) => { - resolve([]); - }); -} +export const getPointTransactions = ( + username: string, + type?: number +): Promise => { + if (window.usePrivate) { + const data = { username, type }; + return axios.post(apiBase(`/private-api/point-list`), data).then((resp) => resp.data); + } + + return new Promise((resolve) => { + resolve([]); + }); +}; export const claimPoints = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/points-claim`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/points-claim`), data).then((resp) => resp.data); +}; -export const calcPoints = (username: string, amount: string): Promise<{ usd: number, estm: number }> => { - const data = {code: getAccessToken(username), amount}; - return axios.post(apiBase(`/private-api/points-calc`), data).then(resp => resp.data); -} +export const calcPoints = ( + username: string, + amount: string +): Promise<{ usd: number; estm: number }> => { + const data = { code: getAccessToken(username), amount }; + return axios.post(apiBase(`/private-api/points-calc`), data).then((resp) => resp.data); +}; export interface PromotePrice { - duration: number, - price: number + duration: number; + price: number; } export const getPromotePrice = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/promote-price`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/promote-price`), data).then((resp) => resp.data); +}; -export const getPromotedPost = (username: string, author: string, permlink: string): Promise<{ author: string, permlink: string } | ''> => { - const data = {code: getAccessToken(username), author, permlink}; - return axios.post(apiBase(`/private-api/promoted-post`), data).then(resp => resp.data); -} +export const getPromotedPost = ( + username: string, + author: string, + permlink: string +): Promise<{ author: string; permlink: string } | ""> => { + const data = { code: getAccessToken(username), author, permlink }; + return axios.post(apiBase(`/private-api/promoted-post`), data).then((resp) => resp.data); +}; export const getBoostOptions = (username: string): Promise => { - const data = {code: getAccessToken(username)}; - return axios.post(apiBase(`/private-api/boost-options`), data).then(resp => resp.data); -} + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/boost-options`), data).then((resp) => resp.data); +}; -export const getBoostedPost = (username: string, author: string, permlink: string): Promise<{ author: string, permlink: string } | ''> => { - const data = {code: getAccessToken(username), author, permlink}; - return axios.post(apiBase(`/private-api/boosted-post`), data).then(resp => resp.data); -} +export const getBoostedPost = ( + username: string, + author: string, + permlink: string +): Promise<{ author: string; permlink: string } | ""> => { + const data = { code: getAccessToken(username), author, permlink }; + return axios.post(apiBase(`/private-api/boosted-post`), data).then((resp) => resp.data); +}; export interface CommentHistoryListItem { - title: string; - body: string; - tags: string[]; - timestamp: string; - v: number; + title: string; + body: string; + tags: string[]; + timestamp: string; + v: number; } interface CommentHistory { - meta: { - count: number; - }, - list: CommentHistoryListItem[]; + meta: { + count: number; + }; + list: CommentHistoryListItem[]; } -export const commentHistory = (author: string, permlink: string, onlyMeta: boolean = false): Promise => { - const data = {author, permlink, onlyMeta: onlyMeta ? '1' : ''}; - return axios.post(apiBase(`/private-api/comment-history`), data).then(resp => resp.data); -} +export const commentHistory = ( + author: string, + permlink: string, + onlyMeta: boolean = false +): Promise => { + const data = { author, permlink, onlyMeta: onlyMeta ? "1" : "" }; + return axios.post(apiBase(`/private-api/comment-history`), data).then((resp) => resp.data); +}; export const getPromotedEntries = (): Promise => { - if (window.usePrivate) { - return axios.get(apiBase(`/private-api/promoted-entries`)).then((resp) => resp.data); + if (window.usePrivate) { + return axios.get(apiBase(`/private-api/promoted-entries`)).then((resp) => resp.data); + } + + return new Promise((resolve) => resolve([])); +}; + +export const saveNotificationsSettings = ( + username: string, + notifyTypes: NotifyTypes[], + isEnabled: boolean, + token: string +) => { + return saveNotificationSetting( + username, + isElectron() ? "desktop" : "web", + Number(isEnabled), + notifyTypes as number[], + token + ); +}; + +export interface ReferralItem { + id: number; + username: string; + referrer: string; + created: string; + rewarded: number; + v: number; +} + +export interface ReferralItems { + data: ReferralItem[]; +} + +export const getReferrals = (username: any, maxId: any): Promise => { + return axios.get(apiBase(`/private-api/referrals/${username}`), { + params: { + max_id: maxId } + }); +}; - return new Promise(resolve => resolve([])); +export interface ReferralStat { + total: number; + rewarded: number; } +export const getReferralsStats = async (username: any): Promise => { + try { + const res = await axios.get(apiBase(`/private-api/referrals/${username}/stats`)); + if (!res.data) { + throw new Error("No Referrals for this user!"); + } + const convertReferralStat = (rawData: any) => { + return { + total: rawData.total || 0, + rewarded: rawData.rewarded || 0 + } as ReferralStat; + }; + return convertReferralStat(res.data); + } catch (error) { + console.warn(error); + throw error; + } +}; +export interface Announcement { + id: number; + title: string; + description: string; + button_text: string; + button_link: string; + path: string | Array; + auth: boolean; +} + +export const getAnnouncementsData = async (): Promise => { + try { + const res = await axios.get(apiBase(`/private-api/announcements`)); + if (!res.data) { + return []; + } + return res.data; + } catch (error) { + console.warn(error); + throw error; + } +}; +export interface Recoveries { + username: string; + email: string; + publicKeys: Record; +} +export interface GetRecoveriesEmailResponse extends Recoveries { + _id: string; +} +export const getRecoveries = (username: string): Promise => { + const data = { code: getAccessToken(username) }; + return axios.post(apiBase(`/private-api/recoveries`), data).then((resp) => resp.data); +}; + +export const addRecoveries = ( + username: string, + email: string, + publicKeys: Object +): Promise<{ recoveries: Recoveries }> => { + const data = { code: getAccessToken(username), email, publicKeys }; + return axios.post(apiBase(`/private-api/recoveries-add`), data).then((resp) => resp.data); +}; + +export const deleteRecoveries = (username: string, recoveryId: string): Promise => { + const data = { code: getAccessToken(username), id: recoveryId }; + return axios.post(apiBase(`/private-api/recoveries-delete`), data).then((resp) => resp.data); +}; + +export const onboardEmail = (username: string, email: string, friend: string): Promise => { + const dataBody = { + username, + email, + friend + }; + return axios + .post(apiBase(`/private-api/account-create-friend`), dataBody) + .then((resp) => resp.data); +}; diff --git a/src/common/api/queries.ts b/src/common/api/queries.ts new file mode 100644 index 00000000000..545c3d54c02 --- /dev/null +++ b/src/common/api/queries.ts @@ -0,0 +1,37 @@ +import { useQuery } from "@tanstack/react-query"; +import { QueryIdentifiers } from "../core"; +import { getPoints, getPointTransactions } from "./private-api"; +import { useMappedStore } from "../store/use-mapped-store"; + +const DEFAULT = { + points: "0.000", + uPoints: "0.000", + transactions: [] +}; + +export function usePointsQuery(username: string, filter = 0) { + const { global } = useMappedStore(); + + return useQuery( + [QueryIdentifiers.POINTS, username, filter], + async () => { + const name = username.replace("@", ""); + + try { + const points = await getPoints(name, global.usePrivate); + const transactions = await getPointTransactions(name, filter); + return { + points: points.points, + uPoints: points.unclaimed_points, + transactions + }; + } catch (e) { + return DEFAULT; + } + }, + { + initialData: DEFAULT, + retryDelay: 30000 + } + ); +} diff --git a/src/common/api/search-api.ts b/src/common/api/search-api.ts index b188f98b510..f42c3c0ea68 100644 --- a/src/common/api/search-api.ts +++ b/src/common/api/search-api.ts @@ -1,88 +1,118 @@ import axios from "axios"; +import { EntryVote } from "../store/entries/types"; import { dataLimit } from "./bridge"; -import {apiBase} from "./helper"; +import { apiBase } from "./helper"; export interface SearchResult { - id: number; - title: string; - title_marked: string | null; - category: string; - author: string; - permlink: string; - author_rep: number | string; - children: number; - body: string; - body_marked: string | null; - img_url: string; - created_at: string; - payout: number; - total_votes: number; - up_votes: number; - tags: string[]; - depth: number; - app: string; + id: number; + title: string; + title_marked: string | null; + category: string; + author: string; + permlink: string; + author_rep: number | string; + author_reputation?: number | string; + children: number; + body: string; + body_marked: string | null; + img_url: string; + created_at: string; + created?: string; + payout: number; + total_votes: number; + up_votes: number; + tags: string[]; + json_metadata?: any; + depth: number; + app: string; + active_votes?: EntryVote[]; + pending_payout_value?: string; } export interface SearchResponse { - hits: number; - results: SearchResult[]; - scroll_id?: string; - took: number; + hits: number; + results: SearchResult[]; + scroll_id?: string; + took: number; } -export const search = (q: string, sort: string, hideLow: string, since?: string, scroll_id?: string): Promise => { - const data: { q: string, sort: string, hide_low: string, since?: string, scroll_id?: string } = {q, sort, hide_low: hideLow}; +export const search = ( + q: string, + sort: string, + hideLow: string, + since?: string, + scroll_id?: string, + votes?: number +): Promise => { + const data: { + q: string; + sort: string; + hide_low: string; + since?: string; + scroll_id?: string; + votes?: number; + } = { q, sort, hide_low: hideLow }; - if (since) data.since = since; - if (scroll_id) data.scroll_id = scroll_id; + if (since) data.since = since; + if (scroll_id) data.scroll_id = scroll_id; + if (votes) data.votes = votes; - return axios.post(apiBase(`/search-api/search`), data).then(resp => resp.data); -} + return axios.post(apiBase(`/search-api/search`), data).then((resp) => resp.data); +}; export interface FriendSearchResult { - name: string; - full_name: string; - reputation: number + name: string; + full_name: string; + lastSeen: string; + reputation: number; } export const searchFollower = (following: string, q: string): Promise => { - const data = {following, q}; + const data = { following, q }; - return axios.post(apiBase(`/search-api/search-follower`), data).then(resp => resp.data); -} + return axios.post(apiBase(`/search-api/search-follower`), data).then((resp) => resp.data); +}; export const searchFollowing = (follower: string, q: string): Promise => { - const data = {follower, q}; + const data = { follower, q }; - return axios.post(apiBase(`/search-api/search-following`), data).then(resp => resp.data); -} + return axios.post(apiBase(`/search-api/search-following`), data).then((resp) => resp.data); +}; export interface AccountSearchResult { - name: string; - full_name: string; - about: string; - reputation: number + name: string; + full_name: string; + about: string; + reputation: number; } -export const searchAccount = (q: string = "", limit: number = dataLimit, random: number = 1): Promise => { - const data = {q, limit, random}; +export const searchAccount = ( + q: string = "", + limit: number = dataLimit, + random: number = 1 +): Promise => { + const data = { q, limit, random }; - return axios.post(apiBase(`/search-api/search-account`), data).then(resp => resp.data); -} + return axios.post(apiBase(`/search-api/search-account`), data).then((resp) => resp.data); +}; export interface TagSearchResult { - tag: string; - repeat: number; + tag: string; + repeat: number; } -export const searchTag = (q: string = "", limit: number = dataLimit, random: number = 0): Promise => { - const data = {q, limit, random}; +export const searchTag = ( + q: string = "", + limit: number = dataLimit, + random: number = 0 +): Promise => { + const data = { q, limit, random }; - return axios.post(apiBase(`/search-api/search-tag`), data).then(resp => resp.data); -} + return axios.post(apiBase(`/search-api/search-tag`), data).then((resp) => resp.data); +}; export const searchPath = (username: string, q: string): Promise => { - const data = {q}; - return axios.post(apiBase(`/search-api/search-path`), data).then(resp => resp.data); -} + const data = { q }; + return axios.post(apiBase(`/search-api/search-path`), data).then((resp) => resp.data); +}; diff --git a/src/common/api/spk-api.ts b/src/common/api/spk-api.ts new file mode 100644 index 00000000000..337cc2a8412 --- /dev/null +++ b/src/common/api/spk-api.ts @@ -0,0 +1,371 @@ +import axios from "axios"; +import * as sdk from "hivesigner"; +import { PrivateKey, TransactionConfirmation } from "@hiveio/dhive"; +import { client as hiveClient } from "./hive"; +import * as keychain from "../helper/keychain"; +import { broadcastPostingJSON } from "./operations"; +import { hotSign } from "../helper/hive-signer"; + +const spkNodes = [ + "https://spk.good-karma.xyz", + "https://spkinstant.hivehoneycomb.com", + "https://spknode.blocktrades.us", + "https://spk.tcmd-spkcc.com", + "https://spktoken.dlux.io" +]; + +const spkNode = "https://spk.good-karma.xyz"; //spkNodes[Math.floor(Math.random()*spkNodes.length)]; + +export interface SpkApiWallet { + balance: number; + claim: number; + drop: { + availible: { + amount: number; + precision: number; + token: string; + }; + last_claim: number; + total_claims: number; + }; + poweredUp: number; + granted: { + t: number; + [key: string]: number; + }; + granting: { + t: number; + [key: string]: number; + }; + heldCollateral: number; + contracts: unknown[]; + up: unknown; + down: unknown; + power_downs: { [key: string]: string }; + gov_downs: unknown; + gov: number; + spk: number; + spk_block: number; + tick: string; // double in string + node: string; + head_block: number; + behind: number; + VERSION: string; // v + pow: number; +} + +export interface SpkMarkets { + head_block: number; + markets: { + node: { + [key: string]: { + lastGood: number; + report: { + block: number; + }; + }; + }; + }; +} + +export interface Market { + name: string; + status: string; +} + +export interface Markets { + list: Market[]; + raw: any; +} + +export interface HivePrice { + hive: { + usd: number; + }; +} + +export function rewardSpk(data: SpkApiWallet, sstats: any) { + let r = 0, + a = 0, + b = 0, + c = 0, + t = 0, + diff = data.head_block - data.spk_block; + if (!data.spk_block) { + return 0; + } else if (diff < 28800) { + return 0; + } else { + t = diff / 28800; + a = data.gov ? simpleInterest(data.gov, t, sstats.spk_rate_lgov) : 0; + b = data.pow ? simpleInterest(data.pow, t, sstats.spk_rate_lpow) : 0; + c = simpleInterest( + (data.granted.t > 0 ? data.granted.t : 0) + + (data.granting.t && data.granting.t > 0 ? data.granting.t : 0), + t, + sstats.spk_rate_ldel + ); + const i = a + b + c; + if (i) { + return i; + } else { + return 0; + } + } + function simpleInterest(p: number, t: number, r: number) { + const amount = p * (1 + r / 365); + const interest = amount - p; + return interest * t; + } +} + +export const getSpkWallet = async (username: string): Promise => { + const resp = await axios.get(`${spkNode}/@${username}`); + return resp.data; +}; + +export const getMarkets = async (): Promise => { + const resp = await axios.get(`${spkNode}/markets`); + return { + list: Object.entries(resp.data.markets.node).map(([name, node]) => ({ + name, + status: + node.lastGood >= resp.data.head_block - 1200 + ? "🟩" + : node.lastGood > resp.data.head_block - 28800 + ? "🟨" + : "🟥" + })), + raw: resp.data + }; +}; + +export const getHivePrice = async (): Promise => { + try { + const resp = await axios.get("https://api.coingecko.com/api/v3/simple/price", { + params: { + ids: "hive", + vs_currencies: "usd" + } + }); + return resp.data; + } catch (e) { + return { hive: { usd: 0 } }; + } +}; + +const sendSpkGeneralByHs = ( + id: string, + from: string, + to: string, + amount: string | number, + memo?: string +) => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id, + json: JSON.stringify({ + to, + amount: +amount * 1000, + ...(typeof memo === "string" ? { memo } : {}) + }) + }; + hotSign("custom-json", params, `@${from}/spk`); +}; + +const transferSpkGeneralByKey = async ( + id: string, + from: string, + key: PrivateKey, + to: string, + amount: string | number, + memo?: string +): Promise => { + const json = JSON.stringify({ + to, + amount: +amount * 1000, + ...(typeof memo === "string" ? { memo } : {}) + }); + + const op = { + id, + json, + required_auths: [from], + required_posting_auths: [] + }; + + return await hiveClient.broadcast.json(op, key); +}; + +const transferSpkGeneralByKc = ( + id: string, + from: string, + to: string, + amount: string | number, + memo?: string +) => { + const json = JSON.stringify({ + to, + amount: +amount * 1000, + ...(typeof memo === "string" ? { memo } : {}) + }); + return keychain.customJson( + from, + id, + "Active", + json, + `${ + id === "spkcc_spk_send" + ? "Transfer SPK" + : id === "spkcc_power_grant" + ? "Delegate LARYNX" + : "Transfer LARYNX" + }` + ); +}; + +export const sendSpkByHs = (from: string, to: string, amount: string, memo?: string) => { + return sendSpkGeneralByHs("spkcc_spk_send", from, to, amount, memo || ""); +}; + +export const sendLarynxByHs = (from: string, to: string, amount: string, memo?: string) => { + return sendSpkGeneralByHs("spkcc_send", from, to, amount, memo || ""); +}; + +export const transferSpkByKey = async ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + return transferSpkGeneralByKey("spkcc_spk_send", from, key, to, amount, memo || ""); +}; + +export const transferLarynxByKey = async ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + return transferSpkGeneralByKey("spkcc_send", from, key, to, amount, memo || ""); +}; + +export const transferSpkByKc = (from: string, to: string, amount: string, memo: string) => { + return transferSpkGeneralByKc("spkcc_spk_send", from, to, amount, memo || ""); +}; + +export const transferLarynxByKc = async ( + from: string, + to: string, + amount: string, + memo: string +) => { + return transferSpkGeneralByKc("spkcc_send", from, to, amount, memo || ""); +}; + +export const delegateLarynxByKey = async ( + from: string, + key: PrivateKey, + to: string, + amount: string +) => { + return transferSpkGeneralByKey("spkcc_power_grant", from, key, to, +amount); +}; + +export const delegateLarynxByHs = async (from: string, to: string, amount: string) => { + return sendSpkGeneralByHs("spkcc_power_grant", from, to, +amount); +}; + +export const delegateLarynxByKc = async (from: string, to: string, amount: string) => { + return transferSpkGeneralByKc("spkcc_power_grant", from, to, +amount); +}; + +export const claimLarynxRewards = async (from: string): Promise => { + const json = { gov: false }; + + return broadcastPostingJSON(from, "spkcc_shares_claim", json); +}; + +export const claimAirdropLarynxRewards = async (from: string): Promise => { + const json = { claim: true }; + + return broadcastPostingJSON(from, "spkcc_claim", json); +}; + +export const powerLarynxByKey = async ( + mode: "up" | "down", + from: string, + key: PrivateKey, + amount: string +) => { + const json = JSON.stringify({ amount: +amount * 1000 }); + + const op = { + id: `spkcc_power_${mode}`, + json, + required_auths: [from], + required_posting_auths: [] + }; + + return await hiveClient.broadcast.json(op, key); +}; + +export const powerLarynxByHs = (mode: "up" | "down", from: string, amount: string) => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: `spkcc_power_${mode}`, + json: JSON.stringify({ amount: +amount * 1000 }) + }; + hotSign("custom-json", params, `@${from}/spk`); +}; + +export const powerLarynxByKc = async (mode: "up" | "down", from: string, amount: string) => { + const json = JSON.stringify({ amount: +amount * 1000 }); + return keychain.customJson(from, `spkcc_power_${mode}`, "Active", json, `Power ${mode} LARYNX`); +}; + +export const lockLarynxByKey = async ( + mode: "lock" | "unlock", + key: PrivateKey, + from: string, + amount: string +) => { + const json = JSON.stringify({ amount: +amount * 1000 }); + + const op = { + id: mode === "lock" ? "spkcc_gov_up" : "spkcc_gov_down", + json, + required_auths: [from], + required_posting_auths: [] + }; + + return await hiveClient.broadcast.json(op, key); +}; + +export const lockLarynxByHs = async (mode: "lock" | "unlock", from: string, amount: string) => { + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: mode === "lock" ? "spkcc_gov_up" : "spkcc_gov_down", + json: JSON.stringify({ amount: +amount * 1000 }) + }; + hotSign("custom-json", params, `@${from}/spk`); +}; + +export const lockLarynxByKc = async (mode: "lock" | "unlock", from: string, amount: string) => { + const json = JSON.stringify({ amount: +amount * 1000 }); + return keychain.customJson( + from, + mode === "lock" ? "spkcc_gov_up" : "spkcc_gov_down", + "Active", + json, + `${mode.toUpperCase()} LARYNX` + ); +}; diff --git a/src/common/api/threespeak.ts b/src/common/api/threespeak.ts new file mode 100644 index 00000000000..67349918ce5 --- /dev/null +++ b/src/common/api/threespeak.ts @@ -0,0 +1,195 @@ +import * as tus from "tus-js-client"; +import axios from "axios"; +import { getDecodedMemo } from "../helper/hive-signer"; + +const studioEndPoint = "https://studio.3speak.tv"; +const tusEndPoint = "https://uploads.3speak.tv/files/"; +const client = axios.create({}); + +export const threespeakAuth = async (username: string) => { + try { + let response = await client.get( + `${studioEndPoint}/mobile/login?username=${username}&hivesigner=true`, + { + withCredentials: false, + headers: { + "Content-Type": "application/json" + } + } + ); + const memo_string = response.data.memo; + let { memoDecoded } = await getDecodedMemo(username, memo_string); + + memoDecoded = memoDecoded.replace("#", ""); + const user = await getTokenValidated(memoDecoded, username); + return memoDecoded; + } catch (err) { + console.log(err); + throw err; + } +}; + +export const getTokenValidated = async (jwt: string, username: string) => { + try { + let response = await client.get( + `${studioEndPoint}/mobile/login?username=${username}&access_token=${jwt}`, + { + withCredentials: false, + headers: { + "Content-Type": "application/json" + } + } + ); + return response.data; + } catch (err) { + console.log(err); + throw err; + } +}; + +export const uploadVideoInfo = async ( + oFilename: string, + fileSize: number, + videoUrl: string, + thumbnailUrl: string, + username: string, + duration: string +) => { + const token = await threespeakAuth(username); + try { + const { data } = await axios.post( + `${studioEndPoint}/mobile/api/upload_info?app=ecency`, + { + filename: videoUrl, + oFilename: oFilename, + size: fileSize, + duration, + thumbnail: thumbnailUrl, + isReel: false, + owner: username + }, + { + withCredentials: false, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + } + } + ); + return data; + } catch (e) { + console.error(e); + throw e; + } +}; + +export const getAllVideoStatuses = async (username: string) => { + const token = await threespeakAuth(username); + try { + let response = await client.get(`${studioEndPoint}/mobile/api/my-videos`, { + withCredentials: false, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + } + }); + return response.data; + } catch (err) { + console.log(err); + throw err; + } +}; + +export const updateSpeakVideoInfo = async ( + username: string, + postBody: string, + videoId: string, + title: string, + tags: string[], + isNsfwC: boolean +) => { + const token = await threespeakAuth(username); + + const data = { + videoId: videoId, + title: title, + description: postBody, + isNsfwContent: isNsfwC, + tags_v2: tags + }; + + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }; + + axios + .post(`${studioEndPoint}/mobile/api/update_info`, data, { headers }) + .then((response) => {}) + .catch((error) => { + console.error("Error:", error); + }); +}; + +export const markAsPublished = async (username: string, videoId: string) => { + const token = await threespeakAuth(username); + const data = { + videoId + }; + + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }; + + axios + .post(`${studioEndPoint}/mobile/api/my-videos/iPublished`, data, { headers }) + .then((response) => {}) + .catch((error) => { + console.error("Error:", error); + }); +}; + +export const uploadFile = async (file: File, type: string, progressCallback: (percentage: number) => void) => { + + return new Promise<{ + fileUrl: string, + fileName: string, + fileSize: number + }>((resolve, reject) => { + + let vPercentage = 0; + let tPercentage = 0; + + const upload: any = new tus.Upload(file, { + endpoint: tusEndPoint, + retryDelays: [0, 3000, 5000, 10000, 20000], + metadata: { + filename: file.name, + filetype: file.type + }, + onError: function (error: Error) { + reject(error); + }, + onProgress: function (bytesUploaded: number, bytesTotal: number) { + if (type === "video") { + vPercentage = Number(((bytesUploaded / bytesTotal) * 100).toFixed(2)); + progressCallback(vPercentage); + } else { + tPercentage = Number(((bytesUploaded / bytesTotal) * 100).toFixed(2)); + progressCallback(tPercentage); + } + }, + onSuccess: function () { + let fileUrl = upload?.url.replace(tusEndPoint, ""); + const result = { + fileUrl, + fileName: upload.file?.name || '', + fileSize: upload.file?.size || 0, + }; + resolve(result); + } + }); + upload.start(); + }); +}; \ No newline at end of file diff --git a/src/common/app.tsx b/src/common/app.tsx index 3a985d23260..b17c63972ab 100644 --- a/src/common/app.tsx +++ b/src/common/app.tsx @@ -1,92 +1,175 @@ -import React, { useEffect } from "react"; -import {Route, Switch} from "react-router-dom"; - -import EntryIndexContainer from "./pages/entry-index"; -import ProfileContainer from "./pages/profile"; -import EntryContainer from "./pages/entry"; -import CommunitiesContainer, {CommunityCreateContainer, CommunityCreateHSContainer} from "./pages/communities"; -import CommunityContainer from "./pages/community"; -import DiscoverContainer from "./pages/discover"; -import {SearchPageContainer, SearchMorePageContainer} from "./pages/search"; -import WitnessesContainer from "./pages/witnesses"; -import {ProposalsIndexContainer, ProposalDetailContainer} from "./pages/proposals"; -import AuthContainer from "./pages/auth"; -import SubmitContainer from "./pages/submit"; -import MarketPage from "./pages/market"; -import SignUpContainer from "./pages/sign-up"; +import React, { useEffect, useState } from "react"; +import { Route, Switch } from "react-router-dom"; +import EntryIndexContainer from "./pages/index"; +import { EntryScreen } from "./pages/entry"; +import { SearchMorePageContainer, SearchPageContainer } from "./pages/search"; +import { ProposalDetailContainer, ProposalsIndexContainer } from "./pages/proposals"; import NotFound from "./components/404"; - import Tracker from "./tracker"; - import { - AboutPageContainer, - GuestPostPageContainer, - ContributePageContainer, - PrivacyPageContainer, - WhitePaperPageContainer, - TosPageContainer, - FaqPageContainer, - ContributorsPageContainer + AboutPage, + ContributePage, + ContributorsPage, + FaqPage, + GuestPostPage, + PrivacyPage, + TosPage, + WhitePaperPage } from "./pages/static"; - import routes from "./routes"; -import * as ls from './util/local-storage'; - +import * as ls from "./util/local-storage"; import i18n from "i18next"; import { pageMapDispatchToProps, pageMapStateToProps } from "./pages/common"; import { connect } from "react-redux"; +import loadable from "@loadable/component"; +import Announcement from "./components/announcement"; +import FloatingFAQ from "./components/floating-faq"; +import { useMappedStore } from "./store/use-mapped-store"; +import { EntriesCacheManager } from "./core"; + +import { UserActivityRecorder } from "./components/user-activity-recorder"; + +// Define lazy pages +const ProfileContainer = loadable(() => import("./pages/profile-functional")); +const ProfilePage = (props: any) => ; + +const CommunityContainer = loadable(() => import("./pages/community-functional")); +const CommunityPage = (props: any) => ; + +const DiscoverContainer = loadable(() => import("./pages/discover")); +const DiscoverPage = (props: any) => ; + +const WitnessesContainer = loadable(() => import("./pages/witnesses")); +const WitnessesPage = (props: any) => ; + +const AuthContainer = loadable(() => import("./pages/auth")); +const AuthPage = (props: any) => ; + +const SubmitContainer = loadable(() => import("./pages/submit")); +const SubmitPage = (props: any) => ; + +const OnboardContainer = loadable(() => import("./pages/onboard")); +const OnboardPage = (props: any) => ; + +const MarketContainer = loadable(() => import("./pages/market")); +const MarketPage = (props: any) => ; + +const SignUpContainer = loadable(() => import("./pages/sign-up")); +const SignUpPage = (props: any) => ; + +const CommunitiesContainer = loadable(() => import("./pages/communities")); +const CommunitiesPage = (props: any) => ; + +const CommunityCreateContainer = loadable(() => import("./pages/community-create")); +const CommunityCreatePage = (props: any) => ; + +const CommunityCreateHSContainer = loadable(() => import("./pages/community-create-hs")); +const CommunityCreateHSPage = (props: any) => ; + +const EntryAMPContainer = loadable(() => import("./pages/entry/index-amp")); +const EntryPage = (props: any) => { + const [isAmp, setIsAmp] = useState(props.location.search.includes("?amps")); + return isAmp ? : ; +}; + +const PurchaseContainer = loadable(() => import("./pages/purchase")); +const PurchasePage = (props: any) => ; + +const DecksPage = loadable(() => import("./pages/decks")); + +const App = (props: any) => { + const { global } = useMappedStore(); + + useEffect(() => { + let pathname = window.location.pathname; + if (pathname !== "/faq") { + const currentLang = ls.get("current-language"); + if (currentLang) { + props.setLang(currentLang); + i18n.changeLanguage(currentLang); + } + } + }, []); + + return ( + + {/*Excluded from production*/} + {/**/} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -const App = ({ setLang }: any) => { - useEffect(() => { - let pathname = window.location.pathname; - if(pathname !== '/faq'){ - const currentLang = ls.get("current-language"); - if(currentLang){ - setLang(currentLang); - i18n.changeLanguage(currentLang) - } - } - },[]); - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + + +
+ + ); }; export default connect(pageMapStateToProps, pageMapDispatchToProps)(App); diff --git a/src/common/components/404/_index.scss b/src/common/components/404/_index.scss index 8b9a1db4a26..1f1101b3db3 100644 --- a/src/common/components/404/_index.scss +++ b/src/common/components/404/_index.scss @@ -1,3 +1,5 @@ +@import "src/style/colors"; + .not-found-404 { position: absolute; left: 0; diff --git a/src/common/components/404/index.tsx b/src/common/components/404/index.tsx index 9297fdf88ee..ca2230e282b 100644 --- a/src/common/components/404/index.tsx +++ b/src/common/components/404/index.tsx @@ -1,85 +1,97 @@ -import React, {Component} from "react"; +import React, { Component } from "react"; -import {History} from "history"; +import { History } from "history"; -import {Link} from "react-router-dom"; +import { Link } from "react-router-dom"; import Meta from "../meta"; import { Global } from "../../store/global/types"; import isElectron from "../../util/is-electron"; const logoCircle = require("../../img/logo-circle.svg"); +import "./_index.scss"; interface Props { - history: History; - global: Global; + history: History; + global: Global; } interface State { - loaded: boolean + loaded: boolean; } export class NotFound extends Component { - state: State = { - loaded: false - } + state: State = { + loaded: false + }; - componentDidMount() { - this.setState({loaded: true}); - } + componentDidMount() { + this.setState({ loaded: true }); + } - goBack = () => { - const {history} = this.props; + goBack = () => { + const { history } = this.props; - history.goBack(); - }; + history.goBack(); + }; - render() { - const {loaded} = this.state; - if (!loaded) { - return '' - } - - const metaProps = { - title: "404", - }; - - const {history, global} = this.props; - - // @ts-ignore make ide happy. code compiles without error. - const entries = history.entries || {} - // @ts-ignore - const index = history.index || 0; - - const canGoBack = !!entries[index - 1]; - - return ( - <> - -
- Ecency -

This page doesn't exist.

-

- {canGoBack && { - e.preventDefault(); - this.goBack(); - }}>Back} - Home - New posts - Hot posts - Trending posts -

-
- - ); + render() { + const { loaded } = this.state; + if (!loaded) { + return ""; } + + const metaProps = { + title: "404" + }; + + const { history, global } = this.props; + + // @ts-ignore make ide happy. code compiles without error. + const entries = history.entries || {}; + // @ts-ignore + const index = history.index || 0; + + const canGoBack = !!entries[index - 1]; + + return ( + <> + +
+ Ecency +

This page doesn't exist.

+

+ {canGoBack && ( + { + e.preventDefault(); + this.goBack(); + }} + > + Back + + )} + Home + New posts + Hot posts + Trending posts +

+
+ + ); + } } export default (p: Props) => { - const props = { - history: p.history, - global: p.global - } + const props = { + history: p.history, + global: p.global + }; - return -} + return ; +}; diff --git a/src/common/components/add-image-mobile/_index.scss b/src/common/components/add-image-mobile/_index.scss index 2486246b228..6013c55b1a0 100644 --- a/src/common/components/add-image-mobile/_index.scss +++ b/src/common/components/add-image-mobile/_index.scss @@ -1,3 +1,8 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + .add-image-mobile-modal { .modal-body { padding-top: 0; diff --git a/src/common/components/add-image-mobile/index.spec.tsx b/src/common/components/add-image-mobile/index.spec.tsx index 5a7c56a3f45..b6dff9bfef2 100644 --- a/src/common/components/add-image-mobile/index.spec.tsx +++ b/src/common/components/add-image-mobile/index.spec.tsx @@ -2,85 +2,82 @@ import * as React from "react"; import renderer from "react-test-renderer"; -import {AddImage} from "./index"; +import { AddImage } from "./index"; -import {activeUserInstance, globalInstance, allOver} from "../../helper/test-helper"; +import { activeUserInstance, globalInstance, allOver } from "../../helper/test-helper"; -let TEST_MODE = 0 +let TEST_MODE = 0; jest.mock("../../api/private-api", () => ({ - getImages: () => - new Promise((resolve) => { - if (TEST_MODE === 0) { - resolve([{ - "created": "Sat Aug 08 2020 12:50:55 GMT+0200 (Central European Summer Time)", - "url": "https://images.ecency.com/DQmSoXUteHvx1evzu27Xn5xbf6Mrn29L9Swn2yH2h4keuSQ/test-3.jpg", - "_id": "5f2e838fbaede01c77aa13a1", - "timestamp": 1596883855848 - }, { - "created": "Sat Aug 08 2020 12:57:47 GMT+0200 (Central European Summer Time)", - "url": "https://images.ecency.com/DQmYFWxjApdbdFVYdG6RMGh1vHQdAsAek38ePF3UsRbUYJv/test-10.jpg", - "_id": "5f2e852bbaede01c77aa13a2", - "timestamp": 1596884267539 - }, { - "created": "Sat Aug 08 2020 12:57:53 GMT+0200 (Central European Summer Time)", - "url": "https://images.ecency.com/DQmRJdj2PZmKHz3zZ31CUC6fpHuQaxsQCvpEp15rX3RpTFG/test-9.jpg", - "_id": "5f2e8531baede01c77aa13a3", - "timestamp": 1596884273695 - }, { - "created": "Sat Aug 08 2020 12:58:15 GMT+0200 (Central European Summer Time)", - "url": "https://images.ecency.com/DQmce1GReq9pwLiEHLsf2hKhAmouZnNSgnH95udH4g2VzAH/test-8.jpg", - "_id": "5f2e8547baede01c77aa13a4", - "timestamp": 1596884295409 - }]) - } + getImages: () => + new Promise((resolve) => { + if (TEST_MODE === 0) { + resolve([ + { + created: "Sat Aug 08 2020 12:50:55 GMT+0200 (Central European Summer Time)", + url: "https://images.ecency.com/DQmSoXUteHvx1evzu27Xn5xbf6Mrn29L9Swn2yH2h4keuSQ/test-3.jpg", + _id: "5f2e838fbaede01c77aa13a1", + timestamp: 1596883855848 + }, + { + created: "Sat Aug 08 2020 12:57:47 GMT+0200 (Central European Summer Time)", + url: "https://images.ecency.com/DQmYFWxjApdbdFVYdG6RMGh1vHQdAsAek38ePF3UsRbUYJv/test-10.jpg", + _id: "5f2e852bbaede01c77aa13a2", + timestamp: 1596884267539 + }, + { + created: "Sat Aug 08 2020 12:57:53 GMT+0200 (Central European Summer Time)", + url: "https://images.ecency.com/DQmRJdj2PZmKHz3zZ31CUC6fpHuQaxsQCvpEp15rX3RpTFG/test-9.jpg", + _id: "5f2e8531baede01c77aa13a3", + timestamp: 1596884273695 + }, + { + created: "Sat Aug 08 2020 12:58:15 GMT+0200 (Central European Summer Time)", + url: "https://images.ecency.com/DQmce1GReq9pwLiEHLsf2hKhAmouZnNSgnH95udH4g2VzAH/test-8.jpg", + _id: "5f2e8547baede01c77aa13a4", + timestamp: 1596884295409 + } + ]); + } - if (TEST_MODE === 1) { - resolve([]); - } - - }), + if (TEST_MODE === 1) { + resolve([]); + } + }) })); const defProps = { - global: globalInstance, - activeUser: {...activeUserInstance}, - onHide: () => { - }, - onPick: () => { - }, - onGallery: () => { - }, - onUpload: () => { - } + global: globalInstance, + activeUser: { ...activeUserInstance }, + onHide: () => {}, + onPick: () => {}, + onGallery: () => {}, + onUpload: () => {} }; - it("(1) Default render.", async () => { - const component = renderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); + const component = renderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); }); it("(2) usePrivate = 1", async () => { - const props = { - ...defProps, - global: { - ...globalInstance, - usePrivate: false - } + const props = { + ...defProps, + global: { + ...globalInstance, + usePrivate: false } - const component = renderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); + }; + const component = renderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); }); it("(3) Empty gallery.", async () => { - TEST_MODE = 1; + TEST_MODE = 1; - const component = renderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); + const component = renderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); }); - - diff --git a/src/common/components/add-image-mobile/index.tsx b/src/common/components/add-image-mobile/index.tsx index 86544586134..1ba87cf2e17 100644 --- a/src/common/components/add-image-mobile/index.tsx +++ b/src/common/components/add-image-mobile/index.tsx @@ -1,155 +1,171 @@ -import React, {Component} from "react"; +import React, { Component } from "react"; -import {Button, Modal} from "react-bootstrap"; +import { Button, Modal } from "react-bootstrap"; -import {proxifyImageSrc, setProxyBase} from "@ecency/render-helper"; +import { proxifyImageSrc, setProxyBase } from "@ecency/render-helper"; -import {Global} from "../../store/global/types"; -import {ActiveUser} from "../../store/active-user/types"; +import { Global } from "../../store/global/types"; +import { ActiveUser } from "../../store/active-user/types"; import BaseComponent from "../base"; import LinearProgress from "../linear-progress"; -import {getImages, UserImage} from "../../api/private-api"; +import { getImages, UserImage } from "../../api/private-api"; -import {error} from "../feedback"; - -import {_t} from "../../i18n"; - -import _c from "../../util/fix-class-names" +import { error } from "../feedback"; +import { _t } from "../../i18n"; import defaults from "../../constants/defaults.json"; - +import "./_index.scss"; setProxyBase(defaults.imageServer); interface Props { - global: Global; - activeUser: ActiveUser | null; - onHide: () => void; - onPick: (url: string) => void; - onGallery: () => void; - onUpload: () => void; + global: Global; + activeUser: ActiveUser | null; + onHide: () => void; + onPick: (url: string) => void; + onGallery: () => void; + onUpload: () => void; } interface State { - loading: boolean, - items: UserImage[] + loading: boolean; + items: UserImage[]; } export class AddImage extends BaseComponent { - state: State = { - loading: true, - items: [] - } + state: State = { + loading: true, + items: [] + }; - componentDidMount() { - this.fetch(); - } - - fetch = () => { - const {activeUser, global} = this.props; - - if (!global.usePrivate) { - this.stateSet({loading: false}); - return; - } - - this.stateSet({loading: true}); - getImages(activeUser?.username!).then(items => { - this.stateSet({items: this.sort(items).slice(0, 3), loading: false}); - }).catch(() => { - this.stateSet({loading: false}); - error(_t('g.server-error')); - }) - } + componentDidMount() { + this.fetch(); + } - sort = (items: UserImage[]) => - items.sort((a, b) => { - return new Date(b.created).getTime() > new Date(a.created).getTime() ? 1 : -1; - }); + fetch = () => { + const { activeUser, global } = this.props; - upload = () => { - this.props.onUpload(); + if (!global.usePrivate) { + this.stateSet({ loading: false }); + return; } - gallery = () => { - this.props.onGallery() + this.stateSet({ loading: true }); + getImages(activeUser?.username!) + .then((items) => { + this.stateSet({ items: this.sort(items).slice(0, 3), loading: false }); + }) + .catch(() => { + this.stateSet({ loading: false }); + error(_t("g.server-error")); + }); + }; + + sort = (items: UserImage[]) => + items.sort((a, b) => { + return new Date(b.created).getTime() > new Date(a.created).getTime() ? 1 : -1; + }); + + upload = () => { + this.props.onUpload(); + }; + + gallery = () => { + this.props.onGallery(); + }; + + itemClicked = (item: UserImage) => { + this.props.onPick(item.url); + }; + + render() { + const { global } = this.props; + const { items, loading } = this.state; + + if (loading) { + return ( +
+ +
+ ); } - itemClicked = (item: UserImage) => { - this.props.onPick(item.url); - } + const btnGallery = global.usePrivate ? ( + + ) : null; + const btnUpload = ; - render() { - const {global} = this.props; - const {items, loading} = this.state; - - if (loading) { - return
- -
- } - - const btnGallery = global.usePrivate ? : null; - const btnUpload = ; - - if (items.length === 0) { - return
-
-
- {btnUpload} -
-
- } - - return
-
- {items.length > 0 && ( - <> -
- {_t('add-image-mobile.recent-title')} -
-
- {items.map(item => { - const src = proxifyImageSrc(item.url, 600, 500, global.canUseWebp ? 'webp' : 'match') - return
{ - this.itemClicked(item); - }}/> - })} -
- - )} -
-
- {btnGallery} - {btnUpload} -
+ if (items.length === 0) { + return ( +
+
+
{btnUpload}
+ ); } -} + return ( +
+
+ {items.length > 0 && ( + <> +
{_t("add-image-mobile.recent-title")}
+
+ {items.map((item) => { + const src = proxifyImageSrc( + item.url, + 600, + 500, + global.canUseWebp ? "webp" : "match" + ); + return ( +
{ + this.itemClicked(item); + }} + /> + ); + })} +
+ + )} +
+
+ {btnGallery} + {btnUpload} +
+
+ ); + } +} export default class AddImageDialog extends Component { - hide = () => { - const {onHide} = this.props; - onHide(); - } - - render() { - return ( - - - {_t('add-image-mobile.title')} - - - - - - ); - } + hide = () => { + const { onHide } = this.props; + onHide(); + }; + + render() { + return ( + + + {_t("add-image-mobile.title")} + + + + + + ); + } } diff --git a/src/common/components/add-image/index.spec.tsx b/src/common/components/add-image/index.spec.tsx index 811d449f162..e6dfa0f75ca 100644 --- a/src/common/components/add-image/index.spec.tsx +++ b/src/common/components/add-image/index.spec.tsx @@ -1,20 +1,15 @@ import React from "react"; -import {AddImage} from "./index"; +import { AddImage } from "./index"; import TestRenderer from "react-test-renderer"; it("(1) Default render", async () => { - const props = { - onHide: () => { - }, - onSubmit: () => { - } - }; + const props = { + onHide: () => {}, + onSubmit: () => {} + }; - const renderer = TestRenderer.create(); - expect(renderer.toJSON()).toMatchSnapshot(); + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); }); - - - diff --git a/src/common/components/add-image/index.tsx b/src/common/components/add-image/index.tsx index 13657d5e133..482eb31cbf1 100644 --- a/src/common/components/add-image/index.tsx +++ b/src/common/components/add-image/index.tsx @@ -1,102 +1,112 @@ -import React, {Component} from "react"; +import React, { Component } from "react"; -import {Button, Form, FormControl, Modal} from "react-bootstrap"; +import { Button, Form, FormControl, Modal } from "react-bootstrap"; -import {_t} from "../../i18n"; +import { _t } from "../../i18n"; import { handleInvalid, handleOnInput } from "../../util/input-util"; interface Props { - onHide: () => void; - onSubmit: (text: string, link: string) => void; + onHide: () => void; + onSubmit: (text: string, link: string) => void; } interface State { - text: string; - link: string; + text: string; + link: string; } export class AddImage extends Component { - state: State = { - text: "", - link: "" - } + state: State = { + text: "", + link: "" + }; - form = React.createRef(); + form = React.createRef(); - textChanged = (e: React.ChangeEvent): void => { - this.setState({text: e.target.value}); - } + textChanged = (e: React.ChangeEvent): void => { + this.setState({ text: e.target.value }); + }; - linkChanged = (e: React.ChangeEvent): void => { - this.setState({link: e.target.value}); - } + linkChanged = (e: React.ChangeEvent): void => { + this.setState({ link: e.target.value }); + }; - render() { - const {text, link} = this.state; + render() { + const { text, link } = this.state; - return
-
{ - e.preventDefault(); - e.stopPropagation(); + return ( +
+ { + e.preventDefault(); + e.stopPropagation(); - if (!this.form.current?.checkValidity()) { - return; - } + if (!this.form.current?.checkValidity()) { + return; + } - const {text, link} = this.state; - const {onSubmit} = this.props; - onSubmit(text, link); - }}> - - handleInvalid(e, 'add-image.', 'validation-text')} - onInput={handleOnInput} - /> - - - handleInvalid(e, 'add-image.', 'validation-image')} - onInput={handleOnInput} - /> - -
- -
- -
- } + const { text, link } = this.state; + const { onSubmit } = this.props; + onSubmit(text, link); + }} + > + + handleInvalid(e, "add-image.", "validation-text")} + onInput={handleOnInput} + /> + + + handleInvalid(e, "add-image.", "validation-image")} + onInput={handleOnInput} + /> + +
+ +
+ +
+ ); + } } - export default class AddImageDialog extends Component { - hide = () => { - const {onHide} = this.props; - onHide(); - } + hide = () => { + const { onHide } = this.props; + onHide(); + }; - render() { - return ( - - - {_t('add-image.title')} - - - - - - ); - } + render() { + return ( + + + {_t("add-image.title")} + + + + + + ); + } } diff --git a/src/common/components/add-link/index.spec.tsx b/src/common/components/add-link/index.spec.tsx index 9c3e667cfba..1d6e9981330 100644 --- a/src/common/components/add-link/index.spec.tsx +++ b/src/common/components/add-link/index.spec.tsx @@ -1,20 +1,15 @@ import React from "react"; -import {AddLink} from "./index"; +import { AddLink } from "./index"; import TestRenderer from "react-test-renderer"; it("(1) Default render", async () => { - const props = { - onHide: () => { - }, - onSubmit: () => { - } - }; + const props = { + onHide: () => {}, + onSubmit: () => {} + }; - const renderer = TestRenderer.create(); - expect(renderer.toJSON()).toMatchSnapshot(); + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); }); - - - diff --git a/src/common/components/add-link/index.tsx b/src/common/components/add-link/index.tsx index 32897fa0712..c509b315aa6 100644 --- a/src/common/components/add-link/index.tsx +++ b/src/common/components/add-link/index.tsx @@ -1,118 +1,128 @@ -import React, {Component} from "react"; +import React, { Component } from "react"; -import {Button, Form, FormControl, Modal} from "react-bootstrap"; +import { Button, Form, FormControl, Modal } from "react-bootstrap"; -import {_t} from "../../i18n"; +import { _t } from "../../i18n"; -import {readClipboard} from "../../util/clipboard"; +import { readClipboard } from "../../util/clipboard"; -import {parseUrl} from "../../util/misc"; +import { parseUrl } from "../../util/misc"; import { handleInvalid, handleOnInput } from "../../util/input-util"; interface Props { - onHide: () => void; - onSubmit: (text: string, link: string) => void; + onHide: () => void; + onSubmit: (text: string, link: string) => void; } interface State { - text: string; - link: string; + text: string; + link: string; } export class AddLink extends Component { - state: State = { - text: "", - link: "https://" - } - - componentDidMount(){ - this.handleClipboard() - } - - handleClipboard = async() => { - const clipboard = await readClipboard(); - - if (clipboard && (clipboard.startsWith("https://") || clipboard.startsWith("http://"))) { - this.setState({ link: clipboard }) - } - } - - form = React.createRef(); + state: State = { + text: "", + link: "https://" + }; - textChanged = (e: React.ChangeEvent): void => { - this.setState({text: e.target.value}); - } + componentDidMount() { + this.handleClipboard(); + } - linkChanged = (e: React.ChangeEvent): void => { - this.setState({link: e.target.value}); - } + handleClipboard = async () => { + const clipboard = await readClipboard(); - render() { - const {text, link} = this.state; - - return
-
{ - e.preventDefault(); - e.stopPropagation(); - - if (!this.form.current?.checkValidity()) { - return; - } - - const {text, link} = this.state; - const {onSubmit} = this.props; - onSubmit(text, link); - }}> - - handleInvalid(e, 'add-link.', 'validation-text')} - onInput={handleOnInput} - /> - - - handleInvalid(e, 'add-link.', 'validation-link')} - onInput={handleOnInput} - /> - -
- -
-
-
+ if (clipboard && (clipboard.startsWith("https://") || clipboard.startsWith("http://"))) { + this.setState({ link: clipboard }); } + }; + + form = React.createRef(); + + textChanged = (e: React.ChangeEvent): void => { + this.setState({ text: e.target.value }); + }; + + linkChanged = (e: React.ChangeEvent): void => { + this.setState({ link: e.target.value }); + }; + + render() { + const { text, link } = this.state; + + return ( +
+
{ + e.preventDefault(); + e.stopPropagation(); + + if (!this.form.current?.checkValidity()) { + return; + } + + const { text, link } = this.state; + const { onSubmit } = this.props; + onSubmit(text, link); + }} + > + + handleInvalid(e, "add-link.", "validation-text")} + onInput={handleOnInput} + /> + + + handleInvalid(e, "add-link.", "validation-link")} + onInput={handleOnInput} + /> + +
+ +
+
+
+ ); + } } - export default class AddLinkDialog extends Component { - hide = () => { - const {onHide} = this.props; - onHide(); - } - - render() { - return ( - - - {_t('add-link.title')} - - - - - - ); - } + hide = () => { + const { onHide } = this.props; + onHide(); + }; + + render() { + return ( + + + {_t("add-link.title")} + + + + + + ); + } } diff --git a/src/common/components/alink/index.tsx b/src/common/components/alink/index.tsx index 0df3da0d1ce..bdc1e715775 100644 --- a/src/common/components/alink/index.tsx +++ b/src/common/components/alink/index.tsx @@ -1,18 +1,7 @@ -import React from 'react'; -import { Link as LinkImport} from 'react-router-dom'; - -const Link = (oprops: any) => ( - /^https?:\/\//.test(oprops.to) - ? ( - - ) : ( - - ) - ); +import React from "react"; +import { Link as LinkImport } from "react-router-dom"; + +const Link = (oprops: any) => + /^https?:\/\//.test(oprops.to) ? : ; export default Link; diff --git a/src/common/components/announcement/__snapshots__/index.spec.tsx.snap b/src/common/components/announcement/__snapshots__/index.spec.tsx.snap new file mode 100644 index 00000000000..3ae4ed4fe46 --- /dev/null +++ b/src/common/components/announcement/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`(1) Default render 1`] = `null`; diff --git a/src/common/components/announcement/index.scss b/src/common/components/announcement/index.scss new file mode 100644 index 00000000000..2d000437f12 --- /dev/null +++ b/src/common/components/announcement/index.scss @@ -0,0 +1,106 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + +.announcement-container { + position: fixed; + z-index: 100; + right: 2rem; + bottom: 2rem; + width: 500px; + border-radius: 6px; + transition: .3s ease-in-out; + + @include media-breakpoint-down(sm) { + max-width: calc(360px - 0.5rem); + width: 100%; + right: 0.5rem; + bottom: 0.5rem; + } +} + +.feedback-announcement { + display: flex; + flex-direction: column; + width: 100%; + padding: 2.4rem; + border-radius: 6px; + margin-top: 5px; + overflow: auto; + box-shadow: 0 6px 20px rgb(0, 0, 0, 0.15); + position: relative; + + @include themify(day) { + background: $white; + } + + @include themify(night) { + background: $dark-two; + } +} + +.announcement-title { + font-size: 1.5rem; + font-weight: bold; + + color: $charcoal-grey; + + @include themify(day) { + color: $charcoal-grey; + } + + @include themify(night) { + color: darken($silver, 10); + } + + @media (max-width: 580px) { + font-size: 1.2rem; + } +} + +.announcement-message { + line-height: 1.5; + + @media (max-width: 580px) { + font-size: 0.9rem; + } + + p { + @include themify(day) { + color: darken($silver, 35); + } + + @include themify(night) { + color: darken($silver, 10); + } + + } +} + +.feedback-announcement { + .row { + display: flex; + + @media (max-width: 390px) { + flex-wrap: wrap; + } + + } + + .main { + display: flex; + flex-direction: row; + justify-content: space-between; + } + + .actions { + gap: 0.5rem; + } + + .close-btn { + position: absolute; + top: 7px; + right: 3px; + } +} \ No newline at end of file diff --git a/src/common/components/announcement/index.spec.tsx b/src/common/components/announcement/index.spec.tsx new file mode 100644 index 00000000000..5653e7ebae5 --- /dev/null +++ b/src/common/components/announcement/index.spec.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import Announcement from "./index"; +import renderer from "react-test-renderer"; +import { BrowserRouter } from "react-router-dom"; + +it("(1) Default render", () => { + const props = { + activeUser: null + }; + const component = renderer.create( + + + + ); + expect(component.toJSON()).toMatchSnapshot(); +}); diff --git a/src/common/components/announcement/index.tsx b/src/common/components/announcement/index.tsx new file mode 100644 index 00000000000..11fe216b173 --- /dev/null +++ b/src/common/components/announcement/index.tsx @@ -0,0 +1,229 @@ +import React, { useState } from "react"; +import { useEffect } from "react"; +import moment from "moment"; +import { Link } from "react-router-dom"; + +import * as ls from "../../util/local-storage"; +import { closeSvg } from "../../img/svg"; +import { getAnnouncementsData, Announcement as AnnouncementApiData } from "../../api/private-api"; +import { Announcement, LaterAnnouncement } from "./types"; +import { useLocation } from "react-router"; +import { Button } from "react-bootstrap"; +import { _t } from "../../i18n"; +import { ActiveUser } from "../../store/active-user/types"; +import "./index.scss"; + +interface Props { + activeUser: ActiveUser | null; +} + +const Announcement = ({ activeUser }: Props) => { + const routerLocation = useLocation(); + + const [allAnnouncements, setAllAnnouncements] = useState([]); + const [show, setShow] = useState(true); + const [list, setList] = useState([]); + const [superList, setSuperList] = useState([]); + const [bannerState, setBannerState] = useState(1); + const [index, setIndex] = useState(0); + const [currentAnnouncement, setCurrentAnnouncement] = useState([]); + + useEffect(() => { + getAnnouncementsData().then((data) => setAllAnnouncements(data)); + }, []); + + useEffect(() => { + getAnnouncements(); + }, [routerLocation, allAnnouncements, activeUser]); + + useEffect(() => { + setCurrentAnnouncement([list[bannerState - 1]]); + }, [superList]); + + useEffect(() => { + if (index < list.length) { + setCurrentAnnouncement([list[index]]); + } else { + setCurrentAnnouncement([list[0]]); + } + }, [list]); + + const getAnnouncements = () => { + const data = allAnnouncements + .filter((announcement) => (announcement.auth ? !!activeUser : true)) + .filter((announcement) => { + if (typeof announcement.path === "object") { + return announcement.path.some((aPath) => routerLocation.pathname.match(aPath)); + } + return routerLocation.pathname.match(announcement.path); + }); + + const dismissList: number[] = ls.get("dismiss_announcements"); + const laterList: LaterAnnouncement[] = ls.get("later_announcements_detail"); + const displayList: Announcement[] = []; + + data.forEach((announcement) => { + if (dismissList !== null && dismissList.includes(announcement.id)) { + return; + } + if (laterList) { + const filteredAnnouncement: LaterAnnouncement[] = laterList.filter( + (a) => a.id == announcement.id + ); + + if (filteredAnnouncement[0] !== undefined) { + let pastDateTime = filteredAnnouncement[0].dateTime; + const past = moment(pastDateTime); + const now = moment(new Date()); + const duration = moment.duration(now.diff(past)); + const hours = duration.asHours(); + + if (hours >= 24) { + let i = 0; + for (const item of laterList) { + if (item.id === announcement.id) { + laterList.splice(i, 1); + i++; + } + } + ls.set("later_announcements_detail", laterList); + displayList.push(announcement); + } + } else { + displayList.push(announcement); + } + } else { + displayList.push(announcement); + } + }); + + setList(displayList); + setSuperList(displayList); + }; + + const closeClick = () => { + setShow(false); + }; + + const upClick = () => { + if (bannerState < list.length) { + setCurrentAnnouncement([list[bannerState]]); + setBannerState(bannerState + 1); + } else { + setBannerState(1); + setCurrentAnnouncement([list[0]]); + } + }; + + const downClick = () => { + if (bannerState > 1) { + setCurrentAnnouncement([list[bannerState - 2]]); + setBannerState(bannerState - 1); + } else { + setBannerState(list.length); + setCurrentAnnouncement([list[list.length - 1]]); + } + }; + + const dismissClick = () => { + const clickedBanner = list[bannerState - 1]; + const index = list.findIndex((x) => x.id === clickedBanner.id); + setIndex(index); + const newList = list.filter((x) => x.id !== clickedBanner.id); + setList(newList); + const data = ls.get("dismiss_announcements"); + if (data === null) { + ls.set("dismiss_announcements", [list[bannerState - 1].id]); + } else { + const getCurrentData = ls.get("dismiss_announcements"); + for (let i = 0; i < getCurrentData.length; i++) { + if (getCurrentData[i].id === list[bannerState - 1].id) { + return; + } + } + getCurrentData.push(list[bannerState - 1].id); + ls.set("dismiss_announcements", getCurrentData); + } + }; + + const laterClick = () => { + const clickedBanner = list[bannerState - 1]; + const index = list.findIndex((x) => x.id === clickedBanner.id); + setIndex(index); + const newList = list.filter((x) => x.id !== clickedBanner.id); + setList(newList); + const DateTime = moment(new Date()); + const laterAnnouncementDetail = ls.get("later_announcements_detail"); + if (laterAnnouncementDetail === null) { + ls.set("later_announcements_detail", [{ id: list[bannerState - 1].id, dateTime: DateTime }]); + } else { + const getCurrentAnnouncementsDetail = ls.get("later_announcements_detail"); + for (let i = 0; i < getCurrentAnnouncementsDetail.length; i++) { + if (getCurrentAnnouncementsDetail[i].id === list[bannerState - 1].id) { + ls.set("later_announcements_detail", [ + { id: list[bannerState - 1].id, dateTime: DateTime } + ]); + } + } + getCurrentAnnouncementsDetail.push({ id: list[bannerState - 1].id, dateTime: DateTime }); + ls.set("later_announcements_detail", getCurrentAnnouncementsDetail); + } + }; + + return ( + <> + {show && currentAnnouncement.length > 0 ? ( + list.length > 0 && + currentAnnouncement.map((x, i) => { + return ( +
+
+
+
+
+
+

{x?.title}

+
+
+
+

{x?.description}

+
+
+ + + + + {list.length > 1 ? ( + + ) : ( + <> + )} +
+
+ +
+
+
+ ); + }) + ) : ( + <> + )} + + ); +}; + +export default Announcement; diff --git a/src/common/components/announcement/types/announcement.ts b/src/common/components/announcement/types/announcement.ts new file mode 100644 index 00000000000..c8c72abd054 --- /dev/null +++ b/src/common/components/announcement/types/announcement.ts @@ -0,0 +1,12 @@ +export interface Announcement { + id: number; + title: string; + description: string; + button_text: string; + button_link: string; +} + +export interface LaterAnnouncement { + id: number; + dateTime: Date; +} diff --git a/src/common/components/announcement/types/index.ts b/src/common/components/announcement/types/index.ts new file mode 100644 index 00000000000..265f06a0248 --- /dev/null +++ b/src/common/components/announcement/types/index.ts @@ -0,0 +1 @@ +export * from "./announcement"; diff --git a/src/common/components/author-info-card/_index.scss b/src/common/components/author-info-card/_index.scss index 4be4c7af128..1e173b295f0 100644 --- a/src/common/components/author-info-card/_index.scss +++ b/src/common/components/author-info-card/_index.scss @@ -1,3 +1,8 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + .avatar-fixed { position: fixed; display: flex; diff --git a/src/common/components/author-info-card/index.tsx b/src/common/components/author-info-card/index.tsx index 81b3b27e018..5a5baab93f3 100644 --- a/src/common/components/author-info-card/index.tsx +++ b/src/common/components/author-info-card/index.tsx @@ -11,7 +11,7 @@ import FollowControls from "../follow-controls"; import ProfileLink from "../profile-link"; import { Skeleton } from "../skeleton"; import UserAvatar from "../user-avatar"; - +import "./_index.scss"; interface MatchParams { category: string; permlink: string; @@ -23,48 +23,49 @@ interface Props extends PageProps { } const AuthorInfoCard = (props: Props) => { - const reputation = accountReputation(props?.entry?.author_reputation); const { username } = props?.match?.params; const author = username.replace("@", ""); const [authorInfo, setAuthorInfo] = useState({ name: "", - about: "", + about: "" }); - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState(false); let _isMounted = false; useEffect(() => { _isMounted = true; !props?.global?.isMobile && getAuthorInfo(); return () => { - _isMounted = false - } + _isMounted = false; + }; }, []); // For fetching authors about and display name information const getAuthorInfo = async () => { - setLoading(true) + setLoading(true); const _authorInfo = (await getAccountFull(author))?.profile; - _isMounted && setAuthorInfo({ - name: _authorInfo?.name || "", - about: _authorInfo?.about || _authorInfo?.location || "", - }); - setLoading(false) + _isMounted && + setAuthorInfo({ + name: _authorInfo?.name || "", + about: _authorInfo?.about || _authorInfo?.location || "" + }); + setLoading(false); }; - - return loading ? -
-
- - + + return loading ? ( +
+
+ + +
+ +
- - -
: ( + ) : (
@@ -73,13 +74,9 @@ const AuthorInfoCard = (props: Props) => { username: props?.entry?.author, children: (
- {UserAvatar({ - ...props, - username: props?.entry?.author, - size: "medium", - })} +
- ), + ) })}
@@ -90,17 +87,13 @@ const AuthorInfoCard = (props: Props) => { children: (
- {!isNaN(reputation) && ({reputation})}
- ), + ) })}
@@ -109,26 +102,25 @@ const AuthorInfoCard = (props: Props) => {
{authorInfo?.name}
{authorInfo?.about && null !== authorInfo?.about && ( -

{`${truncate( - authorInfo?.about, - 130 - )}`}

+

{`${truncate(authorInfo?.about, 130)}`}

)}
{props?.entry?.author && ( - + )} - {props?.global?.usePrivate && props?.entry?.author && ( - - )} + {props?.global?.usePrivate && + props?.entry?.author && + props?.entry?.author !== props.activeUser?.username && ( + + )} {props?.global?.usePrivate && BookmarkBtn({ ...props, - entry: props?.entry, + entry: props?.entry })}
diff --git a/src/common/components/available-credits/index.scss b/src/common/components/available-credits/index.scss new file mode 100644 index 00000000000..421f5fdde75 --- /dev/null +++ b/src/common/components/available-credits/index.scss @@ -0,0 +1,121 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + +.available-credits { + .available-credits-bar { + position: relative; + overflow: hidden; + + .progress { + background-color: $gray-200; + height: 8px; + width: 100%; + border-radius: 4px; + padding: 2px; + + @include themify(night) { + stroke: $gray-700; + } + + .indicator { + height: 4px; + border-radius: 2px; + background-color: $primary; + transition: stroke-dashoffset 1s ease-in-out; + + &.warning { + background-color: $yellow; + + @include themify(night) { + background-color: darken($yellow, 15); + } + } + + &.danger { + background-color: $danger; + + @include themify(night) { + background-color: darken($danger, 15); + } + } + } + } + } + + .btn-link { + font-family: $font-family-base; + font-weight: bold; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05rem; + } +} + +.available-credits-bar-popper { + opacity: 0; + visibility: hidden; + transition: .3s ease-in-out; + z-index: 202; + background: $gray-800; + color: $white; + font-weight: bold; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + border-radius: 0.25rem; + display: grid; + grid-template-columns: min-content min-content; + + @include media-breakpoint-down(md) { + grid-template-columns: 1fr; + } + + span, p { + white-space: nowrap; + } + + &.show { + opacity: 1; + visibility: visible; + } + + .arrow { + visibility: hidden; + + &, &::before { + position: absolute; + width: 8px; + height: 8px; + background: inherit; + } + + &::before { + visibility: visible; + content: ''; + transform: rotate(45deg); + } + } + + .opacity-75 { + opacity: 0.75; + } + + .opacity-5 { + opacity: 0.5; + } + + .extra-details { + border-left: 1px solid $gray-700; + + @include media-breakpoint-down(md) { + border-left: 0; + } + + .two-col { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 0.5rem; + } + } +} \ No newline at end of file diff --git a/src/common/components/available-credits/index.tsx b/src/common/components/available-credits/index.tsx new file mode 100644 index 00000000000..34d5c65de0d --- /dev/null +++ b/src/common/components/available-credits/index.tsx @@ -0,0 +1,192 @@ +import React, { useEffect, useState } from "react"; +import { usePopper } from "react-popper"; +import { + client, + findRcAccounts, + powerRechargeTime, + rcPower, + getRcOperationStats, + RcOperation +} from "../../api/hive"; +import { _t } from "../../i18n"; +import moment, { Moment } from "moment"; +import { rcFormatter } from "../../util/formatted-number"; +import { PurchaseQrDialog } from "../purchase-qr"; +import { ActiveUser } from "../../store/active-user/types"; +import { Location } from "history"; +import "./index.scss"; +import { useMounted } from "../../util/use-mounted"; +import { createPortal } from "react-dom"; + +interface Props { + username: string; + operation: RcOperation; + activeUser: ActiveUser | null; + location: Location; + className?: string; +} + +export const AvailableCredits = ({ username, className, activeUser, location }: Props) => { + const [rcpFixed, setRcpFixed] = useState(0); + const [rcp, setRcp] = useState(0); + const [rcpRechargeDate, setRcpRechargeDate] = useState(moment()); + const [delegated, setDelegated] = useState("0"); + const [receivedDelegation, setReceivedDelegation] = useState("0"); + const [commentAmount, setCommentAmount] = useState(0); + const [voteAmount, setVoteAmount] = useState(0); + const [transferAmount, setTransferAmount] = useState(0); + const [showPurchaseDialog, setShowPurchaseDialog] = useState(false); + + const [host, setHost] = useState(); + const [popperElement, setPopperElement] = useState(); + const [isShow, setIsShow] = useState(false); + + const popper = usePopper(host, popperElement); + + const isMounted = useMounted(); + + useEffect(() => { + fetchRc(); + }, [username]); + + const fetchRc = async () => { + try { + const response = await findRcAccounts(username); + const account = response[0]; + + setRcp(client.rc.calculateRCMana(account).current_mana); + setRcpFixed(Math.floor(+rcPower(account))); + setRcpRechargeDate(moment().add(powerRechargeTime(rcPower(account)), "seconds")); + + const outGoing = response.map((a: any) => a.delegated_rc); + const delegated = outGoing[0]; + const formatOutGoing: any = rcFormatter(delegated); + setDelegated(formatOutGoing); + + const inComing: any = response.map((a: any) => Number(a.received_delegated_rc)); + const formatIncoming = rcFormatter(inComing); + setReceivedDelegation(formatIncoming); + + const rcStats: any = await getRcOperationStats(); + const operationCosts = rcStats.rc_stats.ops; + const commentCost = operationCosts.comment_operation.avg_cost; + const transferCost = operationCosts.transfer_operation.avg_cost; + const voteCost = operationCosts.vote_operation.avg_cost; + + const availableResourceCredit: any = response.map((a: any) => a.rc_manabar.current_mana); + setCommentAmount(Math.ceil(Number(availableResourceCredit[0]) / commentCost)); + setVoteAmount(Math.ceil(Number(availableResourceCredit[0]) / voteCost)); + setTransferAmount(Math.ceil(Number(availableResourceCredit[0]) / transferCost)); + } catch (error) { + console.log(error); + } + }; + + const show = () => { + setIsShow(true); + popper.update?.(); + }; + + const hide = () => { + setIsShow(false); + popper.update?.(); + }; + + return isMounted ? ( + <> +
+
+
+
+
+
+ {commentAmount <= 5 ? ( +
setShowPurchaseDialog(true)}> + {_t("rc-info.boost")} +
+ ) : ( + <> + )} +
+ {createPortal( +
+
+
+ {_t("rc-info.resource-credits")} +
+ {rcFormatter(rcp)}({rcpFixed}%) +
+
+ {rcpFixed !== 100 && ( + + {_t("profile-info.recharge-time", { n: rcpRechargeDate.fromNow() })} + + )} +
+
+
+ +
{_t("rc-info.received-delegations")}
+ {receivedDelegation} +
+ +
{_t("rc-info.delegated")}
+ {delegated} +
+
+
+ +
+ {_t("rc-info.extra-details-heading")} +
+
+
{_t("rc-info.extra-details-post")}
+ {commentAmount} +
+
+
+
{_t("rc-info.extra-details-upvote")}
+ {voteAmount} +
+
+
{_t("rc-info.extra-details-transfer")}
+ {transferAmount} +
+
+
+
+
, + document.querySelector("#popper-container")!! + )} + setShowPurchaseDialog(v)} + activeUser={activeUser} + location={location} + /> + + ) : ( + <> + ); +}; diff --git a/src/common/components/base/index.tsx b/src/common/components/base/index.tsx index 1a34e4322eb..e01dc2c7354 100644 --- a/src/common/components/base/index.tsx +++ b/src/common/components/base/index.tsx @@ -1,15 +1,15 @@ -import {Component} from "react"; +import { Component } from "react"; export default class BaseComponent

extends Component { - _mounted: boolean = true; + _mounted: boolean = true; - componentWillUnmount() { - this._mounted = false; - } + componentWillUnmount() { + this._mounted = false; + } - stateSet = (state: Pick, callback?: () => void): void => { - if (this._mounted) { - this.setState(state, callback); - } - }; + stateSet = (state: Pick, callback?: () => void): void => { + if (this._mounted) { + this.setState(state, callback); + } + }; } diff --git a/src/common/components/beneficiary-editor/index.spec.tsx b/src/common/components/beneficiary-editor/index.spec.tsx index a8c835922e0..ab1e609cfb6 100644 --- a/src/common/components/beneficiary-editor/index.spec.tsx +++ b/src/common/components/beneficiary-editor/index.spec.tsx @@ -1,44 +1,44 @@ import React from "react"; -import BeneficiaryEditorDialog, {DialogBody} from "./index"; +import BeneficiaryEditorDialog, { DialogBody } from "./index"; import TestRenderer from "react-test-renderer"; const defProps = { - list: [{ - account: "foo", - weight: 1000 - }], - onAdd: () => { - }, - onDelete: () => { + list: [ + { + account: "foo", + weight: 1000 } -} + ], + onAdd: () => {}, + onDelete: () => {} +}; it("(1) Default render", () => { - const props = { - ...defProps, - list: [], - } - const renderer = TestRenderer.create(); - expect(renderer.toJSON()).toMatchSnapshot(); + const props = { + ...defProps, + list: [] + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); }); it("(2) Default render with author", () => { - const renderer = TestRenderer.create(); - expect(renderer.toJSON()).toMatchSnapshot(); + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); }); it("(3) DialogBody", () => { - const renderer = TestRenderer.create(); - expect(renderer.toJSON()).toMatchSnapshot(); + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); }); it("(4) DialogBody with author", () => { - const props = { - ...defProps, - author: "bar" - }; - const renderer = TestRenderer.create(); - expect(renderer.toJSON()).toMatchSnapshot(); + const props = { + ...defProps, + author: "bar" + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); }); diff --git a/src/common/components/beneficiary-editor/index.tsx b/src/common/components/beneficiary-editor/index.tsx index ab9b062ad66..8c9e82c6038 100644 --- a/src/common/components/beneficiary-editor/index.tsx +++ b/src/common/components/beneficiary-editor/index.tsx @@ -1,199 +1,241 @@ -import React, {Component} from "react"; +import React, { Component } from "react"; -import {Button, Modal, Form, InputGroup, FormControl} from "react-bootstrap"; +import { Button, Modal, Form, InputGroup, FormControl } from "react-bootstrap"; import BaseComponent from "../base"; -import {error} from "../feedback"; +import { error } from "../feedback"; -import {BeneficiaryRoute} from "../../api/operations"; +import { BeneficiaryRoute } from "../../api/operations"; -import {getAccount} from "../../api/hive"; +import { getAccount } from "../../api/hive"; -import {_t} from "../../i18n"; +import { _t } from "../../i18n"; -import {plusSvg, deleteForeverSvg, accountMultipleSvg} from "../../img/svg"; +import { plusSvg, deleteForeverSvg, accountMultipleSvg } from "../../img/svg"; import { handleInvalid, handleOnInput } from "../../util/input-util"; +import "./_index.scss"; interface Props { - author?: string; - list: BeneficiaryRoute[]; - onAdd: (item: BeneficiaryRoute) => void; - onDelete: (username: string) => void; + author?: string; + list: BeneficiaryRoute[]; + onAdd: (item: BeneficiaryRoute) => void; + onDelete: (username: string) => void; } interface DialogBodyState { - username: string, - percentage: string, - inProgress: boolean + username: string; + percentage: string; + inProgress: boolean; } export class DialogBody extends BaseComponent { - state: DialogBodyState = { - username: "", - percentage: "", - inProgress: false - } - - form = React.createRef(); - - usernameChanged = (e: React.ChangeEvent): void => { - const username = e.target.value.trim().toLowerCase(); - this.stateSet({username}); - } - - percentageChanged = (e: React.ChangeEvent): void => { - this.stateSet({percentage: e.target.value}); - } - - render() { - const {list, author} = this.props; - const {username, percentage, inProgress} = this.state; - - const used = list.reduce((a, b) => a + b.weight / 100, 0); - const available = 100 - used; - - return

{ - e.preventDefault(); - e.stopPropagation(); - - if (!this.form.current?.checkValidity()) { - return; - } - - const {onAdd, list} = this.props; - const {username, percentage} = this.state; - - if (list.find(x => x.account === username) !== undefined) { - error(_t("beneficiary-editor.user-exists-error", {n: username})); + state: DialogBodyState = { + username: "", + percentage: "", + inProgress: false + }; + + form = React.createRef(); + + usernameChanged = (e: React.ChangeEvent): void => { + const username = e.target.value.trim().toLowerCase(); + this.stateSet({ username }); + }; + + percentageChanged = (e: React.ChangeEvent): void => { + this.stateSet({ percentage: e.target.value }); + }; + + render() { + const { list, author } = this.props; + const { username, percentage, inProgress } = this.state; + + const used = list.reduce((a, b) => a + b.weight / 100, 0); + const available = 100 - used; + + return ( + { + e.preventDefault(); + e.stopPropagation(); + + if (!this.form.current?.checkValidity()) { + return; + } + + const { onAdd, list } = this.props; + const { username, percentage } = this.state; + + if (list.find((x) => x.account === username) !== undefined) { + error(_t("beneficiary-editor.user-exists-error", { n: username })); + return; + } + + this.stateSet({ inProgress: true }); + getAccount(username) + .then((r) => { + if (!r) { + error(_t("beneficiary-editor.user-error", { n: username })); return; - } - - this.stateSet({inProgress: true}); - getAccount(username).then((r) => { - if (!r) { - error(_t("beneficiary-editor.user-error", {n: username})); - return; - } - - onAdd({ - account: username, - weight: Number(percentage) * 100 - }); - - this.stateSet({username: "", percentage: ""}); - }).finally(() => this.stateSet({inProgress: false})); - }}> -
- - - - - - - - - {(author && available > 0) && ( - - - - - )} - - - - - - {list.map(x => { - return - - - - - })} - -
{_t("beneficiary-editor.username")}{_t("beneficiary-editor.reward")} -
{`@${author}`}{`${available}%`} -
- - - @ - - handleInvalid(e, 'beneficiary-editor.', 'validation-username')} - onInput={handleOnInput} - onChange={this.usernameChanged} - /> - - - - handleInvalid(e, 'beneficiary-editor.', 'validation-percentage')} - onInput={handleOnInput} - /> - - % - - -
{`@${x.account}`}{`${x.weight / 100}%`}
-
- ; - } + } + + onAdd({ + account: username, + weight: Number(percentage) * 100 + }); + + this.stateSet({ username: "", percentage: "" }); + }) + .finally(() => this.stateSet({ inProgress: false })); + }} + > +
+ + + + + + + + + {author && available > 0 && ( + + + + + )} + + + + + + {list.map((x) => { + return ( + + + + + + ); + })} + +
{_t("beneficiary-editor.username")}{_t("beneficiary-editor.reward")} +
{`@${author}`}{`${available}%`} +
+ + + @ + + + handleInvalid(e, "beneficiary-editor.", "validation-username") + } + onInput={handleOnInput} + onChange={this.usernameChanged} + /> + + + + + handleInvalid(e, "beneficiary-editor.", "validation-percentage") + } + onInput={handleOnInput} + /> + + % + + + + +
{`@${x.account}`}{`${x.weight / 100}%`} + +
+
+ + ); + } } interface State { - visible: boolean + visible: boolean; } export default class BeneficiaryEditorDialog extends Component { - state: State = { - visible: false - } - - toggle = () => { - const {visible} = this.state; - this.setState({visible: !visible}); - } - - render() { - const {list} = this.props; - const {visible} = this.state; - - const btnLabel = list.length > 0 ? _t("beneficiary-editor.btn-label-n", {n: list.length}) : _t("beneficiary-editor.btn-label"); - - return <> - - - {visible && ( - - - {_t("beneficiary-editor.title")} - - - - - - - - - )} - ; - } + state: State = { + visible: false + }; + + toggle = () => { + const { visible } = this.state; + this.setState({ visible: !visible }); + }; + + render() { + const { list } = this.props; + const { visible } = this.state; + + const btnLabel = + list.length > 0 + ? _t("beneficiary-editor.btn-label-n", { n: list.length }) + : _t("beneficiary-editor.btn-label"); + + return ( + <> + + + {visible && ( + + + {_t("beneficiary-editor.title")} + + + + + + + + + )} + + ); + } } diff --git a/src/common/components/bookmark-btn/__snapshots__/index.spec.tsx.snap b/src/common/components/bookmark-btn/__snapshots__/index.spec.tsx.snap index 9b5e3a03932..e86981e7664 100644 --- a/src/common/components/bookmark-btn/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/bookmark-btn/__snapshots__/index.spec.tsx.snap @@ -22,7 +22,7 @@ exports[`(1) No active user 1`] = ` exports[`(2) Not bookmarked 1`] = `
({ - getBookmarks: () => - new Promise((resolve) => { - if (TEST_MODE === 0) { - resolve([]); - } - - if (TEST_MODE === 1) { - resolve([ - { - _id: "314123", - author: "good-karma", - permlink: "awesome-hive", - } - ]); - } - }), + getBookmarks: () => + new Promise((resolve) => { + if (TEST_MODE === 0) { + resolve([]); + } + + if (TEST_MODE === 1) { + resolve([ + { + _id: "314123", + author: "good-karma", + permlink: "awesome-hive" + } + ]); + } + }) })); const defProps = { - entry: {...entryInstance1}, - activeUser: null, - users: [], - ui: UiInstance, - setActiveUser: () => { - }, - updateActiveUser: () => { - }, - deleteUser: () => { - }, - toggleUIProp: () => { - - } + entry: { ...entryInstance1 }, + activeUser: null, + users: [], + ui: UiInstance, + setActiveUser: () => {}, + updateActiveUser: () => {}, + deleteUser: () => {}, + toggleUIProp: () => {} }; it("(1) No active user", () => { - const props = {...defProps}; - const renderer = TestRenderer.create(); - expect(renderer.toJSON()).toMatchSnapshot(); + const props = { ...defProps }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); }); - it("(2) Not bookmarked", async () => { - const props = { - ...defProps, - activeUser: {...activeUserInstance} - }; - - const component = TestRenderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); + const props = { + ...defProps, + activeUser: { ...activeUserInstance } + }; + + const component = TestRenderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); }); - it("(3) Bookmarked", async () => { + TEST_MODE = 1; - TEST_MODE = 1; - - const props = { - ...defProps, - activeUser: {...activeUserInstance} - }; + const props = { + ...defProps, + activeUser: { ...activeUserInstance } + }; - const component = TestRenderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); + const component = TestRenderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); }); diff --git a/src/common/components/bookmark-btn/index.tsx b/src/common/components/bookmark-btn/index.tsx index 39e8a01c38f..3d9b043f934 100644 --- a/src/common/components/bookmark-btn/index.tsx +++ b/src/common/components/bookmark-btn/index.tsx @@ -1,150 +1,164 @@ import React from "react"; -import {Entry} from "../../store/entries/types"; -import {ActiveUser} from "../../store/active-user/types"; +import { Entry } from "../../store/entries/types"; +import { ActiveUser } from "../../store/active-user/types"; -import {getBookmarks, addBookmark, deleteBookmark} from "../../api/private-api"; +import { getBookmarks, addBookmark, deleteBookmark } from "../../api/private-api"; import BaseComponent from "../base"; import LoginRequired from "../login-required"; -import {User} from "../../store/users/types"; -import {ToggleType, UI} from "../../store/ui/types"; -import {Account} from "../../store/accounts/types"; +import { User } from "../../store/users/types"; +import { ToggleType, UI } from "../../store/ui/types"; +import { Account } from "../../store/accounts/types"; import Tooltip from "../tooltip"; -import {success, error} from "../feedback"; +import { success, error } from "../feedback"; -import {_t} from "../../i18n"; +import { _t } from "../../i18n"; import _c from "../../util/fix-class-names"; -import {bookmarkOutlineSvg, bookmarkSvg} from "../../img/svg"; +import { bookmarkOutlineSvg, bookmarkSvg } from "../../img/svg"; +import "./_index.scss"; export interface Props { - entry: Entry; - activeUser: ActiveUser | null; - users: User[]; - ui: UI; - setActiveUser: (username: string | null) => void; - updateActiveUser: (data?: Account) => void; - deleteUser: (username: string) => void; - toggleUIProp: (what: ToggleType) => void; + entry: Entry; + activeUser: ActiveUser | null; + users: User[]; + ui: UI; + setActiveUser: (username: string | null) => void; + updateActiveUser: (data?: Account) => void; + deleteUser: (username: string) => void; + toggleUIProp: (what: ToggleType) => void; } export interface State { - bookmarkId: string | null; - inProgress: boolean + bookmarkId: string | null; + inProgress: boolean; } export class BookmarkBtn extends BaseComponent { - state: State = { - bookmarkId: null, - inProgress: false + state: State = { + bookmarkId: null, + inProgress: false + }; + + componentDidMount() { + this.detect(); + } + + componentDidUpdate(prevProps: Readonly) { + const { activeUser, entry } = this.props; + if ( + // active user changed + activeUser?.username !== prevProps.activeUser?.username || + // or entry changed + !(entry.author === prevProps.entry.author && entry.permlink === prevProps.entry.permlink) + ) { + this.detect(); } + } - componentDidMount() { - this.detect(); + detect = () => { + const { entry, activeUser } = this.props; + if (!activeUser) { + this.stateSet({ bookmarked: false }); + return; } - componentDidUpdate(prevProps: Readonly) { - const {activeUser, entry} = this.props; - if ( - // active user changed - (activeUser?.username !== prevProps.activeUser?.username) || - // or entry changed - (!(entry.author === prevProps.entry.author && entry.permlink === prevProps.entry.permlink)) - ) { - this.detect(); + this.stateSet({ inProgress: true }); + getBookmarks(activeUser.username) + .then((r) => { + const bookmark = r.find((x) => x.author === entry.author && x.permlink == entry.permlink); + if (bookmark) { + this.stateSet({ bookmarkId: bookmark._id }); + } else { + this.stateSet({ bookmarkId: null }); } + }) + .finally(() => this.stateSet({ inProgress: false })); + }; + + add = () => { + const { activeUser, entry } = this.props; + this.stateSet({ inProgress: true }); + addBookmark(activeUser?.username!, entry.author, entry.permlink) + .then(() => { + this.detect(); + success(_t("bookmark-btn.added")); + }) + .catch(() => error(_t("g.server-error"))) + .finally(() => this.stateSet({ inProgress: false })); + }; + + delete = () => { + const { activeUser } = this.props; + const { bookmarkId } = this.state; + + if (!bookmarkId) { + return; } - detect = () => { - const {entry, activeUser} = this.props; - if (!activeUser) { - this.stateSet({bookmarked: false}); - return; - } - - this.stateSet({inProgress: true}); - getBookmarks(activeUser.username).then(r => { - const bookmark = r.find(x => x.author === entry.author && x.permlink == entry.permlink); - if (bookmark) { - this.stateSet({bookmarkId: bookmark._id}); - } else { - this.stateSet({bookmarkId: null}); - } - }).finally(() => this.stateSet({inProgress: false})); - } - - add = () => { - const {activeUser, entry} = this.props; - this.stateSet({inProgress: true}) - addBookmark(activeUser?.username!, entry.author, entry.permlink) - .then(() => { - this.detect(); - success(_t('bookmark-btn.added')); - }) - .catch(() => error(_t('g.server-error'))) - .finally(() => this.stateSet({inProgress: false})) + this.stateSet({ inProgress: true }); + deleteBookmark(activeUser?.username!, bookmarkId) + .then(() => { + this.detect(); + success(_t("bookmark-btn.deleted")); + }) + .catch(() => error(_t("g.server-error"))) + .finally(() => this.stateSet({ inProgress: false })); + }; + + render() { + const { activeUser } = this.props; + + if (!activeUser) { + return LoginRequired({ + ...this.props, + children: ( +
+ + {bookmarkOutlineSvg} + +
+ ) + }); } - delete = () => { - const {activeUser} = this.props; - const {bookmarkId} = this.state; - - if (!bookmarkId) { - return; - } - - this.stateSet({inProgress: true}); - deleteBookmark(activeUser?.username!, bookmarkId) - .then(() => { - this.detect(); - success(_t('bookmark-btn.deleted')); - }) - .catch(() => error(_t('g.server-error'))) - .finally(() => this.stateSet({inProgress: false})) + const { bookmarkId, inProgress } = this.state; + + if (bookmarkId) { + return ( +
+ + {bookmarkSvg} + +
+ ); } - render() { - const {activeUser} = this.props; - - if (!activeUser) { - return LoginRequired({ - ...this.props, - children:
- {bookmarkOutlineSvg} -
- }) - } - - const {bookmarkId, inProgress} = this.state; - - if (bookmarkId) { - return ( -
- {bookmarkSvg} -
- ); - } - - return ( -
- {bookmarkOutlineSvg} -
- ); - } + return ( +
+ + {bookmarkOutlineSvg} + +
+ ); + } } export default (p: Props) => { - const props: Props = { - entry: p.entry, - activeUser: p.activeUser, - users: p.users, - ui: p.ui, - setActiveUser: p.setActiveUser, - updateActiveUser: p.updateActiveUser, - deleteUser: p.deleteUser, - toggleUIProp: p.toggleUIProp - } - - return -} + const props: Props = { + entry: p.entry, + activeUser: p.activeUser, + users: p.users, + ui: p.ui, + setActiveUser: p.setActiveUser, + updateActiveUser: p.updateActiveUser, + deleteUser: p.deleteUser, + toggleUIProp: p.toggleUIProp + }; + + return ; +}; diff --git a/src/common/components/bookmarks/__snapshots__/index.spec.tsx.snap b/src/common/components/bookmarks/__snapshots__/index.spec.tsx.snap index beb491dfdab..ba88e49e14f 100644 --- a/src/common/components/bookmarks/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/bookmarks/__snapshots__/index.spec.tsx.snap @@ -12,170 +12,7 @@ exports[`(1) Bookmarks - No data. 1`] = `
`; -exports[`(2) Bookmarks - Test with data. 1`] = ` -
-`; +exports[`(2) Bookmarks - Test with data. 1`] = `null`; exports[`(3) Favorites - No data. 1`] = `
`; -exports[`(4) Favorites - Test with data. 1`] = ` - -`; +exports[`(4) Favorites - Test with data. 1`] = `null`; diff --git a/src/common/components/bookmarks/_index.scss b/src/common/components/bookmarks/_index.scss index 03235d53d90..5c3317a9110 100644 --- a/src/common/components/bookmarks/_index.scss +++ b/src/common/components/bookmarks/_index.scss @@ -1,3 +1,8 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + .bookmarks-modal { .dialog-menu { diff --git a/src/common/components/bookmarks/index.spec.tsx b/src/common/components/bookmarks/index.spec.tsx index c139291a45c..ea00bb7240e 100644 --- a/src/common/components/bookmarks/index.spec.tsx +++ b/src/common/components/bookmarks/index.spec.tsx @@ -1,141 +1,142 @@ -import React from 'react'; +import React from "react"; import renderer from "react-test-renderer"; -import {createBrowserHistory} from "history"; +import { createBrowserHistory } from "history"; -import {Bookmarks, Favorites} from './index'; +import { Bookmarks, Favorites } from "./index"; -import {globalInstance, activeUserInstance, allOver} from "../../helper/test-helper"; +import { globalInstance, activeUserInstance, allOver } from "../../helper/test-helper"; -let TEST_MODE = 0 +let TEST_MODE = 0; jest.mock("../../api/private-api", () => ({ - getBookmarks: () => - new Promise((resolve) => { - if (TEST_MODE === 0) { - resolve([]); - } - - if (TEST_MODE === 1) { - resolve([{ - "author": "tarazkp", - "permlink": "she-ll-be-apples", - "created": "Wed Aug 12 2020 15:31:29 GMT+0200 (Central European Summer Time)", - "_id": "5f33ef31baede01c77aa1809", - "timestamp": 1597239089185 - }, { - "author": "bluemoon", - "permlink": "on-an-island", - "created": "Wed Aug 12 2020 16:18:50 GMT+0200 (Central European Summer Time)", - "_id": "5f33fa4abaede01c77aa1825", - "timestamp": 1597241930103 - }, { - "author": "acidyo", - "permlink": "dissolution-f2p-crypto-futuristic-fps", - "created": "Wed Aug 12 2020 16:19:29 GMT+0200 (Central European Summer Time)", - "_id": "5f33fa71baede01c77aa1826", - "timestamp": 1597241969917 - }, { - "author": "johnvibes", - "permlink": "after-multiple-ft-hood-soldiers-murdered-2-more-soldiers-arrested-in-child-trafficking-sting", - "created": "Wed Aug 12 2020 16:20:37 GMT+0200 (Central European Summer Time)", - "_id": "5f33fab5baede01c77aa182c", - "timestamp": 1597242037781 - }, { - "author": "kommienezuspadt", - "permlink": "iexnncxb", - "created": "Wed Aug 12 2020 16:20:45 GMT+0200 (Central European Summer Time)", - "_id": "5f33fabdbaede01c77aa182d", - "timestamp": 1597242045183 - }]) - } - }), - - getFavorites: () => - new Promise((resolve) => { - if (TEST_MODE === 0) { - resolve([]); - } - - if (TEST_MODE === 1) { - resolve([ - { - "account": "kommienezuspadt", - "_id": "5f355a2cbaede01c77aa1954", - "timestamp": 1597332012551 - }, { - "account": "purepinay", - "_id": "5f35622bbaede01c77aa1966", - "timestamp": 1597334059308 - }]) - } - }) + getBookmarks: () => + new Promise((resolve) => { + if (TEST_MODE === 0) { + resolve([]); + } + + if (TEST_MODE === 1) { + resolve([ + { + author: "tarazkp", + permlink: "she-ll-be-apples", + created: "Wed Aug 12 2020 15:31:29 GMT+0200 (Central European Summer Time)", + _id: "5f33ef31baede01c77aa1809", + timestamp: 1597239089185 + }, + { + author: "bluemoon", + permlink: "on-an-island", + created: "Wed Aug 12 2020 16:18:50 GMT+0200 (Central European Summer Time)", + _id: "5f33fa4abaede01c77aa1825", + timestamp: 1597241930103 + }, + { + author: "acidyo", + permlink: "dissolution-f2p-crypto-futuristic-fps", + created: "Wed Aug 12 2020 16:19:29 GMT+0200 (Central European Summer Time)", + _id: "5f33fa71baede01c77aa1826", + timestamp: 1597241969917 + }, + { + author: "johnvibes", + permlink: + "after-multiple-ft-hood-soldiers-murdered-2-more-soldiers-arrested-in-child-trafficking-sting", + created: "Wed Aug 12 2020 16:20:37 GMT+0200 (Central European Summer Time)", + _id: "5f33fab5baede01c77aa182c", + timestamp: 1597242037781 + }, + { + author: "kommienezuspadt", + permlink: "iexnncxb", + created: "Wed Aug 12 2020 16:20:45 GMT+0200 (Central European Summer Time)", + _id: "5f33fabdbaede01c77aa182d", + timestamp: 1597242045183 + } + ]); + } + }), + + getFavorites: () => + new Promise((resolve) => { + if (TEST_MODE === 0) { + resolve([]); + } + + if (TEST_MODE === 1) { + resolve([ + { + account: "kommienezuspadt", + _id: "5f355a2cbaede01c77aa1954", + timestamp: 1597332012551 + }, + { + account: "purepinay", + _id: "5f35622bbaede01c77aa1966", + timestamp: 1597334059308 + } + ]); + } + }) })); -it('(1) Bookmarks - No data.', async () => { - const props = { - history: createBrowserHistory(), - global: globalInstance, - activeUser: {...activeUserInstance}, - onHide: () => { - } - }; - - const component = await renderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); +it("(1) Bookmarks - No data.", async () => { + const props = { + history: createBrowserHistory(), + global: globalInstance, + activeUser: { ...activeUserInstance }, + onHide: () => {} + }; + + const component = await renderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); }); -it('(2) Bookmarks - Test with data.', async () => { - TEST_MODE = 1; +it("(2) Bookmarks - Test with data.", async () => { + TEST_MODE = 1; - const props = { - history: createBrowserHistory(), - global: globalInstance, - activeUser: {...activeUserInstance}, - onHide: () => { - } - }; + const props = { + history: createBrowserHistory(), + global: globalInstance, + activeUser: { ...activeUserInstance }, + onHide: () => {} + }; - const component = await renderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); + const component = await renderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); }); -it('(3) Favorites - No data.', async () => { - TEST_MODE = 0; - - const props = { - history: createBrowserHistory(), - global: globalInstance, - activeUser: {...activeUserInstance}, - addAccount: () => { - }, - onHide: () => { - } - }; - - const component = await renderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); -}); +it("(3) Favorites - No data.", async () => { + TEST_MODE = 0; + + const props = { + history: createBrowserHistory(), + global: globalInstance, + activeUser: { ...activeUserInstance }, + addAccount: () => {}, + onHide: () => {} + }; + const component = await renderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); +}); -it('(4) Favorites - Test with data.', async () => { - TEST_MODE = 1; +it("(4) Favorites - Test with data.", async () => { + TEST_MODE = 1; - const props = { - history: createBrowserHistory(), - global: globalInstance, - activeUser: {...activeUserInstance}, - addAccount: () => { - }, - onHide: () => { - } - }; + const props = { + history: createBrowserHistory(), + global: globalInstance, + activeUser: { ...activeUserInstance }, + addAccount: () => {}, + onHide: () => {} + }; - const component = await renderer.create(); - await allOver(); - expect(component.toJSON()).toMatchSnapshot(); + const component = await renderer.create(); + await allOver(); + expect(component.toJSON()).toMatchSnapshot(); }); - diff --git a/src/common/components/bookmarks/index.tsx b/src/common/components/bookmarks/index.tsx index 1e2d077cd16..e475a6183d9 100644 --- a/src/common/components/bookmarks/index.tsx +++ b/src/common/components/bookmarks/index.tsx @@ -1,234 +1,262 @@ -import React, {Component} from "react"; -import {Modal} from "react-bootstrap"; +import React, { Component } from "react"; +import { Modal } from "react-bootstrap"; -import {History} from "history"; +import { History } from "history"; -import {Global} from "../../store/global/types"; -import {ActiveUser} from "../../store/active-user/types"; -import {Account} from "../../store/accounts/types"; +import { Global } from "../../store/global/types"; +import { ActiveUser } from "../../store/active-user/types"; +import { Account } from "../../store/accounts/types"; import BaseComponent from "../base"; import EntryLink from "../entry-link"; import ProfileLink from "../profile-link"; import UserAvatar from "../user-avatar"; import LinearProgress from "../linear-progress"; -import {error} from "../feedback"; +import { error } from "../feedback"; -import {getBookmarks, Bookmark, getFavorites, Favorite} from "../../api/private-api"; - -import {_t} from "../../i18n"; +import { getBookmarks, Bookmark, getFavorites, Favorite } from "../../api/private-api"; +import { _t } from "../../i18n"; +import { useMappedStore } from "../../store/use-mapped-store"; +import "./_index.scss"; interface BookmarksProps { - history: History; - global: Global; - activeUser: ActiveUser | null; - onHide: () => void; + history: History; + global: Global; + activeUser: ActiveUser | null; + onHide: () => void; } interface BookmarksState { - loading: boolean, - items: Bookmark[] + loading: boolean; + items: Bookmark[]; } export class Bookmarks extends BaseComponent { - state: BookmarksState = { - loading: true, - items: [] - } - - componentDidMount() { - this.fetch(); - } - - fetch = () => { - const {activeUser} = this.props; - - this.stateSet({loading: true}); - getBookmarks(activeUser?.username!).then(items => { - const sorted = items.sort((a, b) => b.timestamp > a.timestamp ? 1 : -1); - this.stateSet({items: sorted, loading: false}); - }).catch(() => { - this.stateSet({loading: false}); - error(_t('g.server-error')); - }) - } - - - render() { - const {items, loading} = this.state; - - return
- {loading && } - {items.length > 0 && ( -
-
- {items.map(item => { - return
- {EntryLink({ - ...this.props, - entry: { - category: "foo", - author: item.author, - permlink: item.permlink, - }, - afterClick: () => { - const {onHide} = this.props; - onHide(); - }, - children:
- {UserAvatar({ - ...this.props, - username: item.author, - size: "medium" - })} -
- {item.author} - {item.permlink} -
-
- })} -
- })} -
-
- )} - {(!loading && items.length === 0) && ( -
- {_t('g.empty-list')} -
- )} -
- } + state: BookmarksState = { + loading: true, + items: [] + }; + + componentDidMount() { + this.fetch(); + } + + fetch = () => { + const { activeUser } = this.props; + + this.stateSet({ loading: true }); + getBookmarks(activeUser?.username!) + .then((items) => { + const sorted = items.sort((a, b) => (b.timestamp > a.timestamp ? 1 : -1)); + this.stateSet({ items: sorted, loading: false }); + }) + .catch(() => { + this.stateSet({ loading: false }); + error(_t("g.server-error")); + }); + }; + + render() { + const { items, loading } = this.state; + + return ( +
+ {loading && } + {items.length > 0 && ( +
+
+ {items.map((item) => { + return ( +
+ {EntryLink({ + ...this.props, + entry: { + category: "foo", + author: item.author, + permlink: item.permlink + }, + afterClick: () => { + const { onHide } = this.props; + onHide(); + }, + children: ( +
+ +
+ {item.author} + {item.permlink} +
+
+ ) + })} +
+ ); + })} +
+
+ )} + {!loading && items.length === 0 &&
{_t("g.empty-list")}
} +
+ ); + } } - interface FavoritesProps { - history: History; - global: Global; - activeUser: ActiveUser | null; - addAccount: (data: Account) => void; - onHide: () => void; + history: History; + global: Global; + activeUser: ActiveUser | null; + addAccount: (data: Account) => void; + onHide: () => void; } interface FavoritesState { - loading: boolean, - items: Favorite[] + loading: boolean; + items: Favorite[]; } export class Favorites extends BaseComponent { - state: FavoritesState = { - loading: true, - items: [] - } - - componentDidMount() { - this.fetch(); - } - - fetch = () => { - const {activeUser} = this.props; - - this.stateSet({loading: true}); - getFavorites(activeUser?.username!).then(items => { - const sorted = items.sort((a, b) => b.timestamp > a.timestamp ? 1 : -1); - this.stateSet({items: sorted, loading: false}); - }).catch(() => { - this.stateSet({loading: false}); - error(_t('g.server-error')); - }) - } - - render() { - const {items, loading} = this.state; - - return
- {loading && } - {items.length > 0 && ( -
-
- {items.map(item => { - return
- {ProfileLink({ - ...this.props, - username: item.account, - afterClick: () => { - const {onHide} = this.props; - onHide(); - }, - children:
- {UserAvatar({ - ...this.props, - username: item.account, - size: "medium" - })} -
- {item.account} -
-
- })} -
- })} -
-
- )} - {(!loading && items.length === 0) && ( -
- {_t('g.empty-list')} -
- )} -
- } + state: FavoritesState = { + loading: true, + items: [] + }; + + componentDidMount() { + this.fetch(); + } + + fetch = () => { + const { activeUser } = this.props; + + this.stateSet({ loading: true }); + getFavorites(activeUser?.username!) + .then((items) => { + const sorted = items.sort((a, b) => (b.timestamp > a.timestamp ? 1 : -1)); + this.stateSet({ items: sorted, loading: false }); + }) + .catch(() => { + this.stateSet({ loading: false }); + error(_t("g.server-error")); + }); + }; + + render() { + const { items, loading } = this.state; + + return ( +
+ {loading && } + {items.length > 0 && ( +
+
+ {items.map((item) => { + return ( +
+ {ProfileLink({ + ...this.props, + username: item.account, + afterClick: () => { + const { onHide } = this.props; + onHide(); + }, + children: ( +
+ +
+ {item.account} +
+
+ ) + })} +
+ ); + })} +
+
+ )} + {!loading && items.length === 0 &&
{_t("g.empty-list")}
} +
+ ); + } } - interface DialogProps { - history: History; - global: Global; - activeUser: ActiveUser | null; - addAccount: (data: Account) => void; - onHide: () => void; + history: History; + global: Global; + activeUser: ActiveUser | null; + addAccount: (data: Account) => void; + onHide: () => void; } -type DialogSection = "bookmarks" | "favorites" +type DialogSection = "bookmarks" | "favorites"; interface DialogState { - section: DialogSection + section: DialogSection; } -export default class BookmarksDialog extends Component { - state: DialogState = { - section: "bookmarks" - } - - changeSection = (section: DialogSection) => { - this.setState({section}); - } - - hide = () => { - const {onHide} = this.props; - onHide(); - } - - render() { - const {section} = this.state; - - return ( - - - -
-
{ - this.changeSection("bookmarks"); - }}>{_t("bookmarks.title")}
-
{ - this.changeSection("favorites"); - }}>{_t("favorites.title")}
-
- {section === "bookmarks" && } - {section === "favorites" && } -
-
- ); - } +class BookmarksDialog extends Component { + state: DialogState = { + section: "bookmarks" + }; + + changeSection = (section: DialogSection) => { + this.setState({ section }); + }; + + hide = () => { + const { onHide } = this.props; + onHide(); + }; + + render() { + const { section } = this.state; + + return ( + + + +
+
{ + this.changeSection("bookmarks"); + }} + > + {_t("bookmarks.title")} +
+
{ + this.changeSection("favorites"); + }} + > + {_t("favorites.title")} +
+
+ {section === "bookmarks" && } + {section === "favorites" && } +
+
+ ); + } } + +export default ({ history, onHide }: Pick) => { + const { global, activeUser, addAccount } = useMappedStore(); + + return ( + + ); +}; diff --git a/src/common/components/boost/__snapshots__/index.spec.tsx.snap b/src/common/components/boost/__snapshots__/index.spec.tsx.snap index 25f69c3eade..8a39f1ffe10 100644 --- a/src/common/components/boost/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/boost/__snapshots__/index.spec.tsx.snap @@ -141,6 +141,11 @@ exports[`(1) Default render 1`] = ` > Next + + Actual value of vote you receive may vary +
@@ -293,6 +298,11 @@ exports[`(2) Insufficient Funds 1`] = ` > Next + + Actual value of vote you receive may vary +
@@ -372,7 +382,7 @@ exports[`(2) With entry 1`] = ` onChange={[Function]} placeholder="username/permlink" type="text" - value="good-karma/awesome-hive" + value="" />
@@ -435,12 +445,17 @@ exports[`(2) With entry 1`] = ` > + + Actual value of vote you receive may vary +
diff --git a/src/common/components/boost/_index.scss b/src/common/components/boost/_index.scss index ee6120ef0f2..d11f647051b 100644 --- a/src/common/components/boost/_index.scss +++ b/src/common/components/boost/_index.scss @@ -1,3 +1,8 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + .boost-dialog { .boost-dialog-content { .balance-input { diff --git a/src/common/components/boost/index.spec.tsx b/src/common/components/boost/index.spec.tsx index 5ec18d18b2a..2f1f260c646 100644 --- a/src/common/components/boost/index.spec.tsx +++ b/src/common/components/boost/index.spec.tsx @@ -1,112 +1,104 @@ import React from "react"; -import {Boost} from "./index"; +import { Boost } from "./index"; import TestRenderer from "react-test-renderer"; -import {dynamicPropsIntance1, globalInstance, entryInstance1, allOver} from "../../helper/test-helper"; +import { + dynamicPropsIntance1, + globalInstance, + entryInstance1, + allOver +} from "../../helper/test-helper"; jest.mock("../../api/private-api", () => ({ - getBoostOptions: () => - new Promise((resolve) => { - resolve([150, 200, 250, 300, 350, 400, 450, 500, 550]); - }), + getBoostOptions: () => + new Promise((resolve) => { + resolve([150, 200, 250, 300, 350, 400, 450, 500, 550]); + }) })); - it("(1) Default render", async () => { - const props = { - global: globalInstance, - dynamicProps: dynamicPropsIntance1, - activeUser: { - username: 'foo', - data: { - name: 'foo', - balance: '12.234 HIVE', - hbd_balance: '4321.212', - savings_balance: '2123.000 HIVE' - }, - points: { - points: "500.000", - uPoints: "0.000" - } - }, - signingKey: '', - updateActiveUser: () => { - }, - setSigningKey: () => { - }, - onHide: () => { - } - }; + const props = { + global: globalInstance, + dynamicProps: dynamicPropsIntance1, + activeUser: { + username: "foo", + data: { + name: "foo", + balance: "12.234 HIVE", + hbd_balance: "4321.212", + savings_balance: "2123.000 HIVE" + }, + points: { + points: "500.000", + uPoints: "0.000" + } + }, + signingKey: "", + updateActiveUser: () => {}, + setSigningKey: () => {}, + onHide: () => {} + }; - const renderer = await TestRenderer.create(); - await allOver(); - expect(renderer.toJSON()).toMatchSnapshot(); + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); }); - it("(2) Insufficient Funds", async () => { - const props = { - global: globalInstance, - dynamicProps: dynamicPropsIntance1, - activeUser: { - username: 'foo', - data: { - name: 'foo', - balance: '12.234 HIVE', - hbd_balance: '4321.212', - savings_balance: '2123.000 HIVE' - }, - points: { - points: "10.000", - uPoints: "0.000" - } - }, - signingKey: '', - updateActiveUser: () => { - }, - setSigningKey: () => { - }, - onHide: () => { + const props = { + global: globalInstance, + dynamicProps: dynamicPropsIntance1, + activeUser: { + username: "foo", + data: { + name: "foo", + balance: "12.234 HIVE", + hbd_balance: "4321.212", + savings_balance: "2123.000 HIVE" + }, + points: { + points: "10.000", + uPoints: "0.000" + } + }, + signingKey: "", + updateActiveUser: () => {}, + setSigningKey: () => {}, + onHide: () => {} + }; - } - }; - - const renderer = await TestRenderer.create(); - await allOver(); - expect(renderer.toJSON()).toMatchSnapshot(); + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); }); - it("(2) With entry", async () => { - const props = { - global: globalInstance, - dynamicProps: dynamicPropsIntance1, - activeUser: { - username: 'foo', - data: { - name: 'foo', - balance: '12.234 HIVE', - hbd_balance: '4321.212', - savings_balance: '2123.000 HIVE' - }, - points: { - points: "500.000", - uPoints: "0.000" - } - }, - signingKey: '', - entry: entryInstance1, - updateActiveUser: () => { - }, - setSigningKey: () => { - }, - onHide: () => { - } - }; + const props = { + global: globalInstance, + dynamicProps: dynamicPropsIntance1, + activeUser: { + username: "foo", + data: { + name: "foo", + balance: "12.234 HIVE", + hbd_balance: "4321.212", + savings_balance: "2123.000 HIVE" + }, + points: { + points: "500.000", + uPoints: "0.000" + } + }, + signingKey: "", + entry: entryInstance1, + updateActiveUser: () => {}, + setSigningKey: () => {}, + onHide: () => {} + }; - const renderer = await TestRenderer.create(); - await allOver(); - expect(renderer.toJSON()).toMatchSnapshot(); + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); }); diff --git a/src/common/components/boost/index.tsx b/src/common/components/boost/index.tsx index 7d3d320d2fa..b155c2609df 100644 --- a/src/common/components/boost/index.tsx +++ b/src/common/components/boost/index.tsx @@ -1,375 +1,426 @@ -import React, {Component} from "react"; +import React, { Component } from "react"; import isEqual from "react-fast-compare"; -import {Button, Col, Form, FormControl, Modal, Row} from "react-bootstrap"; +import { Button, Col, Form, FormControl, Modal, Row } from "react-bootstrap"; -import {PrivateKey} from "@hiveio/dhive"; +import { PrivateKey } from "@hiveio/dhive"; -import {Global} from "../../store/global/types"; -import {Account} from "../../store/accounts/types"; -import {DynamicProps} from "../../store/dynamic-props/types"; -import {ActiveUser} from "../../store/active-user/types"; -import {Entry} from "../../store/entries/types"; +import { Global } from "../../store/global/types"; +import { Account } from "../../store/accounts/types"; +import { DynamicProps } from "../../store/dynamic-props/types"; +import { ActiveUser } from "../../store/active-user/types"; +import { EntryHeader, Entry } from "../../store/entries/types"; import BaseComponent from "../base"; import LinearProgress from "../linear-progress"; import SuggestionList from "../suggestion-list"; import KeyOrHot from "../key-or-hot"; -import {error} from "../feedback"; +import { error } from "../feedback"; -import {getPost} from "../../api/bridge"; -import {getBoostOptions, getBoostedPost} from "../../api/private-api"; -import {searchPath} from "../../api/search-api"; -import {boost, boostHot, boostKc, formatError} from "../../api/operations"; +import { getPostHeader } from "../../api/bridge"; +import { getBoostOptions, getBoostedPost } from "../../api/private-api"; +import { searchPath } from "../../api/search-api"; +import { boost, boostHot, boostKc, formatError } from "../../api/operations"; -import {_t} from "../../i18n"; +import { _t } from "../../i18n"; import _c from "../../util/fix-class-names"; import formattedNumber from "../../util/formatted-number"; -import {checkAllSvg} from "../../img/svg"; - +import { checkAllSvg } from "../../img/svg"; +import "./_index.scss"; interface Props { - global: Global; - dynamicProps: DynamicProps; - activeUser: ActiveUser; - signingKey: string; - entry?: Entry; - updateActiveUser: (data?: Account) => void; - setSigningKey: (key: string) => void; - onHide: () => void; + global: Global; + dynamicProps: DynamicProps; + activeUser: ActiveUser; + signingKey: string; + entry?: Entry; + updateActiveUser: (data?: Account) => void; + setSigningKey: (key: string) => void; + onHide: () => void; } interface State { - balanceError: string; - path: string; - postError: string; - paths: string[]; - options: number[]; - amount: number; - inProgress: boolean; - step: 1 | 2 | 3; + balanceError: string; + path: string; + postError: string; + paths: string[]; + options: number[]; + amount: number; + inProgress: boolean; + step: 1 | 2 | 3; } - const pathComponents = (p: string): string[] => p.replace("@", "").split("/"); export class Boost extends BaseComponent { - state: State = { - balanceError: "", - path: "", - postError: "", - paths: [], - options: [], - amount: 0, - inProgress: true, - step: 1 - } - - _timer: any = null; - - componentDidMount() { - this.init().then(() => { - const {updateActiveUser} = this.props; - updateActiveUser(); - }).then(() => { - const {entry} = this.props; - - if (entry) { - this.stateSet({path: `${entry.author}/${entry.permlink}`}); - } - }) - } - - componentDidUpdate(prevProps: Readonly) { - if (!isEqual(this.props.activeUser.points, prevProps.activeUser.points)) { - this.checkBalance(); + state: State = { + balanceError: "", + path: "", + postError: "", + paths: [], + options: [], + amount: 0, + inProgress: true, + step: 1 + }; + + _timer: any = null; + + componentDidMount() { + this.init() + .then(() => { + const { updateActiveUser } = this.props; + updateActiveUser(); + }) + .then(() => { + const { entry } = this.props; + + if (entry) { + this.stateSet({ path: `${entry.author}/${entry.permlink}` }); } - } - - init = () => { - const {activeUser} = this.props; + }); + } - return getBoostOptions(activeUser.username).then(r => { - this.stateSet({options: r, amount: r[0], inProgress: false}, () => { - this.checkBalance(); - }); - }).catch(() => { - error(_t('g.server-error')); - }); + componentDidUpdate(prevProps: Readonly) { + if (!isEqual(this.props.activeUser.points, prevProps.activeUser.points)) { + this.checkBalance(); } + } - pathChanged = (e: React.ChangeEvent) => { - const path = e.target.value; - this.stateSet({path, postError: ''}); + init = () => { + const { activeUser } = this.props; - clearTimeout(this._timer); + return getBoostOptions(activeUser.username) + .then((r) => { + this.stateSet({ options: r, amount: r[0], inProgress: false }, () => { + this.checkBalance(); + }); + }) + .catch(() => { + error(_t("g.server-error")); + }); + }; - if (path.trim().length < 3) { - this.stateSet({paths: []}); - return; - } + pathChanged = (e: React.ChangeEvent) => { + const path = e.target.value; + this.stateSet({ path, postError: "" }); - const {activeUser} = this.props; + clearTimeout(this._timer); - this._timer = setTimeout( - () => - searchPath(activeUser.username, path).then(resp => { - this.stateSet({paths: resp}); - }), - 500 - ); + if (path.trim().length < 3) { + this.stateSet({ paths: [] }); + return; } - pathSelected = (path: string) => { - this.stateSet({path: path, paths: []}); - } + const { activeUser } = this.props; - checkBalance = () => { - const {activeUser} = this.props; - const {amount} = this.state; + this._timer = setTimeout( + () => + searchPath(activeUser.username, path).then((resp) => { + this.stateSet({ paths: resp }); + }), + 500 + ); + }; - const balanceError = parseFloat(activeUser.points.points) < amount ? _t('trx-common.insufficient-funds') : ""; + pathSelected = (path: string) => { + this.stateSet({ path: path, paths: [] }); + }; - this.stateSet({balanceError}); - }; + checkBalance = () => { + const { activeUser } = this.props; + const { amount } = this.state; - isValidPath = (p: string) => { - if (p.indexOf("/") === -1) { - return; - } + const balanceError = + parseFloat(activeUser.points.points) < amount ? _t("trx-common.insufficient-funds") : ""; - const [author, permlink] = pathComponents(p); - return author.length >= 3 && permlink.length >= 3; - }; + this.stateSet({ balanceError }); + }; - sliderChanged = (e: React.ChangeEvent) => { - const amount = Number(e.target.value); - this.stateSet({amount}, () => { - this.checkBalance(); - }); + isValidPath = (p: string) => { + if (p.indexOf("/") === -1) { + return; } - next = async () => { - const {activeUser} = this.props; - const {path} = this.state; + const [author, permlink] = pathComponents(p); + return author.length >= 3 && permlink.length >= 3; + }; - const [author, permlink] = pathComponents(path); + sliderChanged = (e: React.ChangeEvent) => { + const amount = Number(e.target.value); + this.stateSet({ amount }, () => { + this.checkBalance(); + }); + }; - this.stateSet({inProgress: true}); + next = async () => { + const { activeUser } = this.props; + const { path } = this.state; - // Check if post is valid - let post: Entry | null; - try { - post = await getPost(author, permlink); - } catch (e) { - post = null; - } + const [author, permlink] = pathComponents(path); - if (!post) { - this.stateSet({postError: _t("redeem-common.post-error"), inProgress: false}); - return; - } - - // Check if the post already boosted - const boosted = await getBoostedPost(activeUser.username, author, permlink); - if (boosted) { - this.stateSet({postError: _t("redeem-common.post-error-exists"), inProgress: false}); - return; - } + this.stateSet({ inProgress: true }); - this.stateSet({inProgress: false, step: 2}); + // Check if post is valid + let post: EntryHeader | null; + try { + post = await getPostHeader(author, permlink); + } catch (e) { + post = null; } - sign = (key: PrivateKey) => { - const {activeUser} = this.props; - const {path, amount} = this.state; - const [author, permlink] = pathComponents(path); - - this.setState({inProgress: true}); - boost(key, activeUser.username, author, permlink, `${amount}.000`).then(() => { - this.stateSet({step: 3}); - }).catch(err => { - error(formatError(err)); - }).finally(() => { - this.setState({inProgress: false}); - }); + if (!post) { + this.stateSet({ postError: _t("redeem-common.post-error"), inProgress: false }); + return; } - signKs = () => { - const {activeUser} = this.props; - const {path, amount} = this.state; - const [author, permlink] = pathComponents(path); - - this.setState({inProgress: true}); - boostKc(activeUser.username, author, permlink, `${amount}.000`).then(() => { - this.stateSet({step: 3}); - }).catch(err => { - error(formatError(err)); - }).finally(() => { - this.setState({inProgress: false}); - }); - } - - hotSign = () => { - const {activeUser, onHide} = this.props; - const {path, amount} = this.state; - const [author, permlink] = pathComponents(path); - - boostHot(activeUser.username, author, permlink, `${amount}.000`); - onHide(); + // Check if the post already boosted + const boosted = await getBoostedPost(activeUser.username, author, permlink); + if (boosted) { + this.stateSet({ postError: _t("redeem-common.post-boosted-exists"), inProgress: false }); + return; } - finish = () => { - const {onHide} = this.props; - onHide(); + this.stateSet({ inProgress: false, step: 2 }); + }; + + sign = (key: PrivateKey) => { + const { activeUser } = this.props; + const { path, amount } = this.state; + const [author, permlink] = pathComponents(path); + + this.setState({ inProgress: true }); + boost(key, activeUser.username, author, permlink, `${amount}.000`) + .then(() => { + this.stateSet({ step: 3 }); + }) + .catch((err) => { + error(...formatError(err)); + }) + .finally(() => { + this.setState({ inProgress: false }); + }); + }; + + signKs = () => { + const { activeUser } = this.props; + const { path, amount } = this.state; + const [author, permlink] = pathComponents(path); + + this.setState({ inProgress: true }); + boostKc(activeUser.username, author, permlink, `${amount}.000`) + .then(() => { + this.stateSet({ step: 3 }); + }) + .catch((err) => { + error(...formatError(err)); + }) + .finally(() => { + this.setState({ inProgress: false }); + }); + }; + + hotSign = () => { + const { activeUser, onHide } = this.props; + const { path, amount } = this.state; + const [author, permlink] = pathComponents(path); + + boostHot(activeUser.username, author, permlink, `${amount}.000`); + onHide(); + }; + + finish = () => { + const { onHide } = this.props; + onHide(); + }; + + pointsToSbd = (points: number) => { + //const {dynamicProps} = this.props; + //const {base, quote} = dynamicProps; + return points / 150; //* 0.01 * (base / quote); + }; + + render() { + const { activeUser } = this.props; + + const { balanceError, path, postError, amount, paths, options, inProgress, step } = this.state; + + const canSubmit = !postError && !balanceError && this.isValidPath(path); + + let sliderMin = 0; + let sliderMax = 10; + let sliderStep = 1; + + if (options.length > 1) { + sliderMin = options[0]; + sliderMax = options[options.length - 1]; + sliderStep = options[1] - options[0]; } - pointsToSbd = (points: number) => { - //const {dynamicProps} = this.props; - //const {base, quote} = dynamicProps; - return points / 150; //* 0.01 * (base / quote); - }; - - render() { - const {activeUser} = this.props; - - const {balanceError, path, postError, amount, paths, options, inProgress, step} = this.state; - - const canSubmit = !postError && !balanceError && this.isValidPath(path); - - let sliderMin = 0; - let sliderMax = 10; - let sliderStep = 1 - - if (options.length > 1) { - sliderMin = options[0]; - sliderMax = options[options.length - 1]; - sliderStep = options[1] - options[0]; - } - - return
- {step === 1 && ( -
-
-
1
-
-
{_t('boost.title')}
-
{_t('boost.sub-title')}
-
-
- {inProgress && } -
- - {_t('redeem-common.balance')} - - - {balanceError && {balanceError}} - - - - {_t('redeem-common.post')} - - i} onSelect={this.pathSelected}> - - - {postError && {postError}} - {!postError && {_t('redeem-common.post-hint')}} - - - - {_t('boost.amount')} - -
-
- {formattedNumber(this.pointsToSbd(amount), {fractionDigits: 3, suffix: '$'})} - {amount} POINTS -
- - {_t('boost.slider-hint')} -
- -
- - - - - - -
-
- )} - - {step === 2 && ( -
-
-
2
-
-
{_t('trx-common.sign-title')}
-
{_t('trx-common.sign-sub-title')}
-
-
- {inProgress && } -
- {KeyOrHot({ - ...this.props, - inProgress, - onKey: this.sign, - onHot: this.hotSign, - onKc: this.signKs - })} + return ( +
+ {step === 1 && ( +
+
+
1
+
+
{_t("boost.title")}
+
{_t("boost.sub-title")}
+
+
+ {inProgress && } +
+ + + + {_t("redeem-common.balance")} + + + + {balanceError && {balanceError}} + + + + + {_t("redeem-common.post")} + + + i} onSelect={this.pathSelected}> + + + {postError && {postError}} + {!postError && ( + {_t("redeem-common.post-hint")} + )} + + + + + {_t("boost.amount")} + + +
+
+ {formattedNumber(this.pointsToSbd(amount), { + fractionDigits: 3, + suffix: "$" + })} + {amount} POINTS
-
- )} - - {step === 3 && ( -
-
-
3
-
-
{_t('trx-common.success-title')}
-
{_t('trx-common.success-sub-title')}
-
-
- {inProgress && } -
-

- {checkAllSvg} {_t("redeem-common.success-message")} -

-
- -
-
-
- )} -
- } + + {_t("boost.slider-hint")} +
+ + + + + + + {_t("boost.hint")} + + +
+
+ )} + + {step === 2 && ( +
+
+
2
+
+
{_t("trx-common.sign-title")}
+
{_t("trx-common.sign-sub-title")}
+
+
+ {inProgress && } +
+ {KeyOrHot({ + ...this.props, + inProgress, + onKey: this.sign, + onHot: this.hotSign, + onKc: this.signKs + })} +
+
+ )} + + {step === 3 && ( +
+
+
3
+
+
{_t("trx-common.success-title")}
+
{_t("trx-common.success-sub-title")}
+
+
+ {inProgress && } +
+

+ {checkAllSvg}{" "} + {_t("redeem-common.success-message")} +

+
+ +
+
+
+ )} +
+ ); + } } export default class BoostDialog extends Component { - render() { - const {onHide} = this.props; - return ( - - - - - - - ); - } + render() { + const { onHide } = this.props; + return ( + + + + + + + ); + } } diff --git a/src/common/components/button-group/_index.scss b/src/common/components/button-group/_index.scss new file mode 100644 index 00000000000..357bd00c011 --- /dev/null +++ b/src/common/components/button-group/_index.scss @@ -0,0 +1,39 @@ +@import "src/style/vars_mixins"; + +.toggle-with-label { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 1rem; + + @include border-radius(1rem); + @include padding(0.5rem); + + span { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + transition: 0.3s; + + @include border-radius(0.5rem); + @include padding(0.5rem); + + &:not(.selected):hover { + cursor: pointer; + box-shadow: 0 6px 20px -10px rgba(0, 0, 0, 0.25); + } + + &.selected { + background-color: $primary; + color: $white; + } + } + + @include themify(day) { + background-color: $gray-100; + } + + @include themify(night) { + background-color: $gray-800; + } +} \ No newline at end of file diff --git a/src/common/components/button-group/index.tsx b/src/common/components/button-group/index.tsx new file mode 100644 index 00000000000..7bb41175b8e --- /dev/null +++ b/src/common/components/button-group/index.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import "./_index.scss"; + +interface Props { + labels: string[]; + selected: number; + className?: string; + setSelected: (i: number) => void; +} + +export const ButtonGroup = ({ labels, selected, className, setSelected }: Props) => { + return ( +
+ {labels.map((label, key) => ( + setSelected(key)} + > + {label} + + ))} +
+ ); +}; diff --git a/src/common/components/buy-sell-hive/index.tsx b/src/common/components/buy-sell-hive/index.tsx index b1e31595e89..211554d03d6 100644 --- a/src/common/components/buy-sell-hive/index.tsx +++ b/src/common/components/buy-sell-hive/index.tsx @@ -21,7 +21,7 @@ import { limitOrderCreate, limitOrderCancelKc, limitOrderCancelHot, - limitOrderCancel, + limitOrderCancel } from "../../api/operations"; import { _t } from "../../i18n"; @@ -30,12 +30,13 @@ import { AnyAction, bindActionCreators, Dispatch } from "redux"; import { connect } from "react-redux"; import { AppState } from "../../store"; import { PrivateKey } from "@hiveio/dhive"; +import "./_index.scss"; export enum TransactionType { None = 0, Sell = 2, Buy = 1, - Cancel = 3, + Cancel = 3 } interface Props { @@ -62,30 +63,31 @@ export class BuySellHive extends BaseComponent { super(props); this.state = { step: 1, - inProgress: false, + inProgress: false }; } updateAll = (a: any) => { - const { addAccount, updateActiveUser, onTransactionSuccess } = - this.props; + const { addAccount, updateActiveUser, onTransactionSuccess } = this.props; // refresh addAccount(a); // update active updateActiveUser(a); - this.setState({ inProgress: false,step: 3 }); + this.setState({ inProgress: false, step: 3 }); onTransactionSuccess(); }; promiseCheck = (p: any) => { - const { onHide, } = this.props; - p && p.then(() => getAccountFull(this.props.activeUser!.username)) - .then((a: any) => this.updateAll(a)) - .catch((err: any) => { - error(formatError(err)); - this.setState({ inProgress: false }); - onHide(); - }); + const { onHide } = this.props; + p && + p + .then(() => getAccountFull(this.props.activeUser!.username)) + .then((a: any) => this.updateAll(a)) + .catch((err: any) => { + error(...formatError(err)); + this.setState({ inProgress: false }); + onHide(); + }); }; sign = (key: PrivateKey) => { @@ -95,11 +97,9 @@ export class BuySellHive extends BaseComponent { this.promiseCheck(limitOrderCancel(activeUser!.username, key, orderid)); } else { const { - values: { total, amount }, + values: { total, amount } } = this.props; - this.promiseCheck( - limitOrderCreate(activeUser!.username, key, total, amount, Ttype) - ); + this.promiseCheck(limitOrderCreate(activeUser!.username, key, total, amount, Ttype)); } }; @@ -110,11 +110,9 @@ export class BuySellHive extends BaseComponent { this.promiseCheck(limitOrderCancelHot(activeUser!.username, orderid)); } else { const { - values: { total, amount }, + values: { total, amount } } = this.props; - this.promiseCheck( - limitOrderCreateHot(activeUser!.username, total, amount, Ttype) - ); + this.promiseCheck(limitOrderCreateHot(activeUser!.username, total, amount, Ttype)); } }; @@ -125,11 +123,9 @@ export class BuySellHive extends BaseComponent { this.promiseCheck(limitOrderCancelKc(activeUser!.username, orderid)); } else { const { - values: { total, amount }, + values: { total, amount } } = this.props; - this.promiseCheck( - limitOrderCreateKc(activeUser!.username, total, amount, Ttype) - ); + this.promiseCheck(limitOrderCreateKc(activeUser!.username, total, amount, Ttype)); } }; @@ -137,7 +133,7 @@ export class BuySellHive extends BaseComponent { const { onHide, onTransactionSuccess } = this.props; onTransactionSuccess(); onHide(); - } + }; render() { const { step, inProgress } = this.state; @@ -146,7 +142,7 @@ export class BuySellHive extends BaseComponent { amount: 0, price: 0, total: 0, - available: 0, + available: 0 }, onHide, global, @@ -174,14 +170,18 @@ export class BuySellHive extends BaseComponent {
{Ttype === TransactionType.Cancel ? (
- {_t("market.confirm-cancel", {orderid:orderid})} + {_t("market.confirm-cancel", { orderid: orderid })}
) : (
{available < (Ttype === TransactionType.Buy ? total : amount) ? _t("market.transaction-low") - : _t("market.confirm-buy", {amount, price, total, balance: parseFloat(available - total as any).toFixed(3)}) - } + : _t("market.confirm-buy", { + amount, + price, + total, + balance: parseFloat((available - total) as any).toFixed(3) + })}
)}
@@ -195,9 +195,7 @@ export class BuySellHive extends BaseComponent { @@ -221,7 +219,7 @@ export class BuySellHive extends BaseComponent { inProgress, onHot: this.signHs, onKey: this.sign, - onKc: this.signKc, + onKc: this.signKc })}

{ } if (step === 3) { - - const formHeader4 =

+ ); return (
- {formHeader4} -
+ {formHeader4} +
{_t("market.transaction-succeeded")}
- - + +
+
-
); } @@ -298,7 +291,7 @@ class BuySellHiveDialog extends Component { } const mapStateToProps = (state: AppState) => ({ - global: state.global, + global: state.global }); const mapDispatchToProps = (dispatch: Dispatch) => @@ -308,7 +301,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => setActiveUser, updateActiveUser, addAccount, - setSigningKey, + setSigningKey }, dispatch ); diff --git a/src/common/components/chart-stats/index.scss b/src/common/components/chart-stats/index.scss index 9cfeee6b2de..50916991ded 100644 --- a/src/common/components/chart-stats/index.scss +++ b/src/common/components/chart-stats/index.scss @@ -1,3 +1,8 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + .skeleton-loading { height: 30px; width: 40px; diff --git a/src/common/components/chart-stats/index.spec.tsx b/src/common/components/chart-stats/index.spec.tsx index b1731dcf896..69bbd8f1c95 100644 --- a/src/common/components/chart-stats/index.spec.tsx +++ b/src/common/components/chart-stats/index.spec.tsx @@ -1,36 +1,36 @@ import React from "react"; -import {ChartStats} from "./index"; +import { ChartStats } from "./index"; import TestRenderer from "react-test-renderer"; -import {allOver} from "../../helper/test-helper"; +import { allOver } from "../../helper/test-helper"; it("(1) Default render", async () => { - const props = { - loading: false, - data: { - hbd_volume: "dummy value", - highest_bid: "dummy value", - hive_volume: "dummy value", - latest: "dummy value", - lowest_ask: "dummy value", - percent_change: "dummy value", - } - }; + const props = { + loading: false, + data: { + hbd_volume: "dummy value", + highest_bid: "dummy value", + hive_volume: "dummy value", + latest: "dummy value", + lowest_ask: "dummy value", + percent_change: "dummy value" + } + }; - const renderer = await TestRenderer.create(); - await allOver(); - expect(renderer.toJSON()).toMatchSnapshot(); + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); }); it("(2) Render with loading", async () => { - const props = { - loading: true, - data: null - }; + const props = { + loading: true, + data: null + }; - const renderer = await TestRenderer.create(); - await allOver(); - expect(renderer.toJSON()).toMatchSnapshot(); -}); \ No newline at end of file + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/common/components/chart-stats/index.tsx b/src/common/components/chart-stats/index.tsx index 3bc0c901915..c29bc2762c4 100644 --- a/src/common/components/chart-stats/index.tsx +++ b/src/common/components/chart-stats/index.tsx @@ -1,54 +1,88 @@ -import React from 'react'; -import { Table } from 'react-bootstrap'; -import { MarketStatistics } from '../../api/hive'; -import { _t } from '../../i18n'; -import { isMobile } from '../../util/is-mobile'; -import { Skeleton } from '../skeleton'; +import React from "react"; +import { Table } from "react-bootstrap"; +import { MarketStatistics } from "../../api/hive"; +import { _t } from "../../i18n"; +import { isMobile } from "../../util/is-mobile"; +import { Skeleton } from "../skeleton"; +import "./index.scss"; interface Props { - loading: boolean; - data: MarketStatistics | null; + loading: boolean; + data: MarketStatistics | null; } -export const ChartStats = ({loading, data}: Props) =>{ - - return loading ? - - - - - - - - - - - - - - - - - - - -
: - - - - - - - - - - - - - - - - - - -
{_t("market.last-price")}{_t("market.volume")}{_t("market.bid")}{_t("market.ask")}{_t("market.spread")}
${data ? parseFloat(data!.latest!).toFixed(6) : null} (+0.00%)${data? parseFloat(data!.hbd_volume)!.toFixed(2):null}${data? parseFloat(data!.highest_bid)!.toFixed(6):null}${data? parseFloat(data!.lowest_ask)!.toFixed(6):null}{data? ((200 * (parseFloat(data.lowest_ask) - parseFloat(data.highest_bid))) / (parseFloat(data.highest_bid) + parseFloat(data.lowest_ask))).toFixed(3) : null}%
-} \ No newline at end of file +export const ChartStats = ({ loading, data }: Props) => { + return loading ? ( + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + +
+ ) : ( + + + + + + + + + + + + + + + + + + + +
{_t("market.last-price")}{_t("market.volume")}{_t("market.bid")}{_t("market.ask")}{_t("market.spread")}
+ ${data ? parseFloat(data!.latest!).toFixed(6) : null} ( + +0.00%) + ${data ? parseFloat(data!.hbd_volume)!.toFixed(2) : null}${data ? parseFloat(data!.highest_bid)!.toFixed(6) : null}${data ? parseFloat(data!.lowest_ask)!.toFixed(6) : null} + {data + ? ( + (200 * (parseFloat(data.lowest_ask) - parseFloat(data.highest_bid))) / + (parseFloat(data.highest_bid) + parseFloat(data.lowest_ask)) + ).toFixed(3) + : null} + % +
+ ); +}; diff --git a/src/common/components/clickaway-listener/index.spec.tsx b/src/common/components/clickaway-listener/index.spec.tsx index 243e336d88a..f647ca15a12 100644 --- a/src/common/components/clickaway-listener/index.spec.tsx +++ b/src/common/components/clickaway-listener/index.spec.tsx @@ -2,20 +2,22 @@ import React from "react"; import ClickAwayListener from "./index"; import TestRenderer from "react-test-renderer"; - it("(1) triggers onClickAway", async () => { - const onClickaway = () => { - return null - } - let ComponentToTest = () => + const onClickaway = () => { + return null; + }; + let ComponentToTest = () => (
- - - - + + + +
+ ); - const renderer = TestRenderer.create(); - renderer.root.findByProps({ id: "inside" }).props.onClick(); - expect(renderer.root.findByProps({ id: "inside" })).toBeFalsy; + const renderer = TestRenderer.create(); + renderer.root.findByProps({ id: "inside" }).props.onClick(); + expect(renderer.root.findByProps({ id: "inside" })).toBeFalsy; }); diff --git a/src/common/components/clickaway-listener/index.tsx b/src/common/components/clickaway-listener/index.tsx index 6c7bd4e85dd..200ff111ca4 100644 --- a/src/common/components/clickaway-listener/index.tsx +++ b/src/common/components/clickaway-listener/index.tsx @@ -1,47 +1,51 @@ -import React, { Component } from 'react'; +import React, { Component } from "react"; /** * Component that alerts if you click outside of it */ interface Props { - children:any; - onClickAway:()=>void; - className?:string; + children: any; + onClickAway: () => void; + className?: string; } export default class ClickAwayListener extends Component { - wrapperRef: any; - constructor(props:Props){ + wrapperRef: any; + constructor(props: Props) { super(props); this.setWrapperRef = this.setWrapperRef.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); } componentDidMount() { - document.addEventListener('mousedown', this.handleClickOutside); + document.addEventListener("mousedown", this.handleClickOutside); } componentWillUnmount() { - document.removeEventListener('mousedown', this.handleClickOutside); + document.removeEventListener("mousedown", this.handleClickOutside); } /** * Set the wrapper ref */ - setWrapperRef(node:any) { + setWrapperRef(node: any) { this.wrapperRef = node; } /** * Alert if clicked on outside of element */ - handleClickOutside(event:any) { + handleClickOutside(event: any) { if (this.wrapperRef && !this.wrapperRef.contains(event.target)) { - this.props.onClickAway(); + this.props.onClickAway(); } } render() { - return
{this.props.children}
; + return ( +
+ {this.props.children} +
+ ); } } diff --git a/src/common/components/comment-engagement/_index.scss b/src/common/components/comment-engagement/_index.scss new file mode 100644 index 00000000000..8303d81d7dc --- /dev/null +++ b/src/common/components/comment-engagement/_index.scss @@ -0,0 +1,31 @@ +@import "src/style/colors"; +@import "src/style/variables"; +@import "src/style/bootstrap_vars"; +@import "src/style/mixins"; + +.comment-engagement { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 50px; + + .icon { + padding: 14px; + border-radius: 50%; + margin-bottom: 20px; + color: $dark-sky-blue; + border: 2px solid $dark-sky-blue; + width: 54px; + height: 54px; + + svg { + height: 24px; + margin-bottom: -2px; + } + } + + .label { + font-size: 1.4rem; + margin-bottom: 20px; + } + } \ No newline at end of file diff --git a/src/common/components/comment-engagement/index.spec.tsx b/src/common/components/comment-engagement/index.spec.tsx new file mode 100644 index 00000000000..c737815f966 --- /dev/null +++ b/src/common/components/comment-engagement/index.spec.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import CommentEngagement from "./index"; +import renderer from "react-test-renderer"; + +describe("Comment engagement component", () => { + it("Should scroll to comment section", () => { + const component = renderer.create(); + const scrollButton = component.root.findByProps({ id: "scroll-to-input" }); + scrollButton.props.onClick(); + expect(scrollButton).toBeFalsy; + }); +}); diff --git a/src/common/components/comment-engagement/index.tsx b/src/common/components/comment-engagement/index.tsx new file mode 100644 index 00000000000..dbdfec84eee --- /dev/null +++ b/src/common/components/comment-engagement/index.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { commentSvg } from "../../img/svg"; +import { Button } from "react-bootstrap"; +import { _t } from "../../i18n"; +import "./_index.scss"; + +function CommentEngagement() { + const scrollToCommentInput = () => { + // const inputSection: Element | null = document.querySelector('.entry-footer'); + // inputSection?.scrollIntoView({behavior: "smooth"}); + const inputS: HTMLElement | null = document.querySelector(".the-editor"); + inputS?.focus(); + }; + return ( +
+
{commentSvg}
+
{_t("discussion.no-conversation")}
+ +
+ ); +} + +export default CommentEngagement; diff --git a/src/common/components/comment/__snapshots__/index.spec.tsx.snap b/src/common/components/comment/__snapshots__/index.spec.tsx.snap index 2ffa2707955..c3da7ae4150 100644 --- a/src/common/components/comment/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/comment/__snapshots__/index.spec.tsx.snap @@ -11,7 +11,7 @@ exports[`(1) Default render 1`] = `
+
+
+ + + +
+
-