diff --git a/.gitignore b/.gitignore index e1e8fc6b9ec..634c7c65dc7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ npm-debug.log* .DS_Store .idea cache - coverage node_modules build @@ -13,8 +12,9 @@ public/static .env.development.local .env.test.local .env.production.local - -src/config.ts .vscode - dev.sh +*~ +*.marks +defaults-proofofbrain.json +defaults-localhost.json diff --git a/README.md b/README.md index 4adf6cc61ab..0b208ea295f 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ Feel free to test it out and submit improvements and pull requests. ##### Clone -`$ git clone https://github.com/ecency/ecency-vision` - +`$ git clone https://github.com/steemfiles/ecency-vision` +`$ git checkout proofofbrainbranding` `$ cd ecency-vision` ##### Install dependencies @@ -54,32 +54,49 @@ Feel free to test it out and submit improvements and pull requests. * `REDIS_URL` - support for caching amp pages -###### Hivesigner Variables +##### Hivesigner Variables When setting up another service like Ecency with Ecency-vision software: +- `HIVESIGNER_ID` - iff USE_PRIVATE is 0, set this to what account will handle the permissions for posting level operations. +- `HIVESIGNER_SECRET` - iff USE_PRIVATE is 0, set this to the "secret" field value in the [Hive Signer profile](https://hivesigner.com/profile) for the user named as your `HIVESIGNER_ID`. This should be a lengthy lowercase hex string. + 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 +##### 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 +Also for URLs other than https://ecency.com\_... 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. +The 'appURL' member should be the URL of the front end you are running. If you are running this testing, it should be "http://localhost" so hiveSigner redirects you back to localhost. The default is 'https://ecency.com'. +The testnet member should be set to false, unless you want to use a Hive testnet + +#### defaults.json file + +The defaults.json set site settings. Some of these are defaults that the user can change. Some are default and +cannot be changed. The key difference here is that data here is exported to the front end and thus readable by the +front end and at some point, even the user. Things that should not be secret from the user can go here, even if they +may not be able to change some things. + +###### menuOrder + +The menuOrder item determines which order the wallet menu order items should occur. Possible values are, +"pob", "points", "hive", "engine" and "spk". Nothing else is supported. -##### Start website in dev +## Start website in dev `$ yarn start` -##### Start desktop in dev +## Start desktop in dev `$ cd src/desktop` `$ yarn` `$ yarn dev` -##### Pushing new code / Pull requests +## Pushing new code / Pull requests - Make sure to branch off your changes from `development` branch. - Make sure to run `yarn test` and add tests to your changes. diff --git a/package.json b/package.json index 70a3fc2bb62..991fc1b18a4 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@firebase/analytics": "^0.8.0", "@firebase/app": "^0.7.28", "@firebase/messaging": "^0.9.16", - "@hiveio/dhive": "^1.2.1", + "@hiveio/dhive": "^1.2.8", "@hiveio/hivescript": "^1.2.7", "@loadable/component": "^5.15.2", "@loadable/server": "^5.15.2", @@ -43,6 +43,7 @@ "hive-uri": "^0.2.3", "hivesigner": "^3.2.8", "html-react-parser": "^1.2.1", + "html5-qrcode": "^2.3.8", "i18next": "^19.4.4", "i18next-browser-languagedetector": "^4.2.0", "immutability-helper": "^3.0.2", diff --git a/public/favicon.ico b/public/favicon.ico index 1a4beb57306..ba68c621836 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png index 31444c327b2..4781ead35a8 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 6e1024ce492..1aca7680db3 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -1,17 +1,17 @@ // 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'); +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' + 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); @@ -21,28 +21,28 @@ const messaging = firebase.messaging(); messaging.onBackgroundMessage(function (payload) { //console.log('Received bg notification', payload); - const notificationTitle = payload.notification?.title || 'Ecency'; + 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, + icon: payload.notification?.image || "https://ecency.com/static/media/logo-circle.2df6f251.svg", + data: payload.data }); }); -self.addEventListener('notificationclick', function (event) { +self.addEventListener("notificationclick", function (event) { const data = event.notification.data; - let url = 'https://ecency.com'; + let url = "https://ecency.com"; const fullPermlink = data.permlink1 + data.permlink2 + data.permlink3; - if (['vote', 'unvote', 'spin', 'inactive'].includes(data.type)) { - url += '/@' + data.target; + if (["vote", "unvote", "spin", "inactive"].includes(data.type)) { + url += "/@" + data.target; } else { // delegation, mention, transfer, follow, unfollow, ignore, blacklist, reblog - url += '/@' + data.source; + url += "/@" + data.source; } if (fullPermlink) { - url += '/' + fullPermlink; + url += "/" + fullPermlink; } - clients.openWindow(url, '_blank'); -}); \ No newline at end of file + clients.openWindow(url, "_blank"); +}); diff --git a/public/logo192.png b/public/logo192.png index ab5a551f990..d96eb1004db 100644 Binary files a/public/logo192.png and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png index 752be7f0129..c6771670b78 100644 Binary files a/public/logo512.png and b/public/logo512.png differ diff --git a/public/og.jpg b/public/og.jpg index 3e680f5adb4..02f9c0abd95 100644 Binary files a/public/og.jpg and b/public/og.jpg differ diff --git a/public/placeholder.jpg b/public/placeholder.jpg index a22ca3f3d22..ad9ac5c073e 100644 Binary files a/public/placeholder.jpg and b/public/placeholder.jpg differ diff --git a/razzle.config.js b/razzle.config.js index 40b45899785..8241ce73095 100644 --- a/razzle.config.js +++ b/razzle.config.js @@ -1,26 +1,26 @@ -'use strict'; -const LoadableWebpackPlugin = require('@loadable/webpack-plugin'); -const { loadableTransformer } = require('loadable-ts-transformer'); -const path = require('path'); +"use strict"; +const LoadableWebpackPlugin = require("@loadable/webpack-plugin"); +const { loadableTransformer } = require("loadable-ts-transformer"); +const path = require("path"); module.exports = { - plugins: ['typescript', 'scss'], + plugins: ["typescript", "scss"], 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 if (target === "web") { @@ -31,16 +31,18 @@ module.exports = { webpackConfig.plugins.push( new LoadableWebpackPlugin({ outputAsset: true, - writeToDisk: { filename }, + writeToDisk: { filename } }) ); } // Enable SSR lazy-loading - const tsLoader = webpackConfig.module.rules.find(rule => !(rule.test instanceof Array) && rule.test && rule.test.test('.tsx')); + const tsLoader = webpackConfig.module.rules.find( + (rule) => !(rule.test instanceof Array) && rule.test && rule.test.test(".tsx") + ); tsLoader.use[0].options.getCustomTransformers = () => ({ before: [loadableTransformer] }); - webpackConfig.devtool = dev ? 'source-map' : false; + webpackConfig.devtool = dev ? "source-map" : false; return webpackConfig; } }; diff --git a/src/client/index.tsx b/src/client/index.tsx index 2d5fd9b3836..be2ef50a7e7 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -12,7 +12,7 @@ import "../style/theme-day.scss"; import "../style/theme-night.scss"; import "./base-handlers"; import { loadableReady } from "@loadable/component"; - +import { toggleLoginKcAct } from "../common/store/ui"; declare var window: AppWindow; const store = configureStore(window["__PRELOADED_STATE__"]); @@ -53,6 +53,7 @@ loadableReady().then(() => { setTimeout(() => { if (window.hive_keychain) { window.hive_keychain.requestHandshake(() => { + store.dispatch(toggleLoginKcAct()); store.dispatch(hasKeyChainAct()); }); } diff --git a/src/common/_index.scss b/src/common/_index.scss new file mode 100644 index 00000000000..b88e51e8af4 --- /dev/null +++ b/src/common/_index.scss @@ -0,0 +1,4 @@ +.tracker { + width: 100%; + height: 100%; +} diff --git a/src/common/api/hive-engine.ts b/src/common/api/hive-engine.ts index 3d367a337b0..3cd703f25de 100644 --- a/src/common/api/hive-engine.ts +++ b/src/common/api/hive-engine.ts @@ -43,8 +43,47 @@ export interface TokenStatus { precision: number; } +export interface Unstake { + _id: number; + account: string; + symbol: string; + quantity: string; + quantityLeft: string; + nextTransactionTimestamp: number; + numberTransactionsLeft: string; + millisecPerPeriod: string; + txID: string; +} + +import { HECoarseTransaction, HEFineTransaction } from "../store/transactions/types"; + const HIVE_ENGINE_RPC_URL = engine.engineRpcUrl; +export const getPendingUnstakes = (account: string, tokenName: string): Promise> => { + const data = { + jsonrpc: "2.0", + method: "find", + params: { + contract: "tokens", + table: "pendingUnstakes", + query: { + account: account, + token: tokenName + } + }, + id: 1 + }; + + return axios + .post(HIVE_ENGINE_RPC_URL, data, { + headers: { "Content-type": "application/json" } + }) + .then((r) => r.data.result) + .catch((e) => { + return []; + }); +}; + export const getTokenBalances = (account: string): Promise => { const data = { jsonrpc: "2.0", @@ -93,6 +132,22 @@ const getTokens = (tokens: string[]): Promise => { }); }; +export const getHiveEngineTokenBalance = async ( + account: string, + tokenName: string +): Promise => { + // commented just to try removing the non-existing unknowing HiveEngineTokenBalance type + // ): Promise => { + let balances = await getTokenBalances(account); + const tokens = await getTokens([tokenName]); + + const balance = balances.find((balance) => balance.symbol == tokenName); + const token = tokens[0]; + const tokenMetadata = token && (JSON.parse(token!.metadata) as TokenMetadata); + + return new HiveEngineToken({ ...balance, ...token, ...tokenMetadata } as any); +}; + export const getHiveEngineTokenBalances = async (account: string): Promise => { // commented just to try removing the non-existing unknowing HiveEngineTokenBalance type // ): Promise => { @@ -151,6 +206,83 @@ export const stakeTokens = async ( return broadcastPostingJSON(account, "ssc-mainnet-hive", json); }; +export interface DelegationEntry { + _id: number; + from: string; + to: string; + symbol: string; + quantity: string; +} + +export async function getTokenDelegations(account: string): Promise> { + const data = { + jsonrpc: "2.0", + method: "find", + params: { + contract: "tokens", + table: "delegations", + query: { + $or: [{ from: account }, { to: account }] + } + }, + id: 3 + }; + return axios + .post(HIVE_ENGINE_RPC_URL, data, { + headers: { "Content-type": "application/json" } + }) + .then((r) => { + const list: Array = r.data.result; + return list; + }) + .catch((e) => { + console.log(e.message); + return []; + }); +} + +// Exclude author and curation reward details +export async function getCoarseTransactions( + account: string, + limit: number, + symbol: string, + offset: number = 0 +) { + const response = await axios({ + url: "https://accounts.hive-engine.com/accountHistory", + method: "GET", + params: { + account, + limit, + offset, + type: "user", + symbol + } + }); + return response.data; +} + +// Include virtual transactions like curation and author reward details. +export async function getFineTransactions( + symbol: string, + account: string, + limit: number, + offset: number +): Promise> { + return axios({ + url: `https://scot-api.hive-engine.com/get_account_history`, + method: "GET", + params: { + account, + token: symbol, + limit, + offset + } + }).then((response) => { + return response.data; + }); +} + export const getMetrics: any = async (symbol?: any, account?: any) => { const data = { jsonrpc: "2.0", @@ -166,11 +298,6 @@ export const getMetrics: any = async (symbol?: any, account?: any) => { 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" } @@ -188,3 +315,67 @@ export const getMarketData = async (symbol: any) => { }); return history; }; + +export interface ScotVoteShare { + authorperm: ""; + block_num: number; + percent: number; + revoted: any; + rshares: number; + timestamp: string; + token: string; + voter: string; + weight: number; +} + +// See https://github.com/hive-engine/scotbot-docs/blob/master/docs/api/README.md for an example using +// https://scot-api.hive-engine.com/@${author}/${permlink}?hive=1. +export interface ScotRewardsInformation { + [coin_id: string]: { + active_votes: Array<{ + authorperm: string; + block_num: number; + percent: number; + rshares: number; + timestamp: string; + token: string; + voter: string; + weight: number; + }>; + app: string; + author: string; + author_curve_exponent: number; + authorperm: string; + beneficiaries_payout_value: number; + block: number; + cashout_time: string; + children: number; + created: string; + curator_payout_value: number; + decline_payout: boolean; + desc: string; + json_metadata: string; + last_payout: string; + last_update: string; + main_post: boolean; + pending_token: number; + precision: number; + promoted: number; + score_hot: number; + score_trend: number; + tags: string; + title: string; + token: string; + total_payout_value: number; + total_vote_weight: number; + vote_rshares: number; + }; +} + +export const getScotRewardsInformation = async (author: string, permlink: string) => { + const info: any = await axios(`https://scot-api.hive-engine.com/@${author}/${permlink}?hive=1`, { + method: "GET", + params: {} + }); + return info.data as ScotRewardsInformation; +}; diff --git a/src/common/api/hive.ts b/src/common/api/hive.ts index fba7ba5275e..e9daa80a72f 100644 --- a/src/common/api/hive.ts +++ b/src/common/api/hive.ts @@ -1,4 +1,5 @@ import { Client, RCAPI, utils } from "@hiveio/dhive"; +import { DEFAULT_CHAIN_ID, DEFAULT_ADDRESS_PREFIX } from "@hiveio/dhive"; import { RCAccount } from "@hiveio/dhive/lib/chain/rc"; @@ -10,16 +11,29 @@ import parseAsset from "../helper/parse-asset"; import { vestsToRshares } from "../helper/vesting"; import isCommunity from "../helper/is-community"; -import SERVERS from "../constants/servers.json"; +import MAINNET_SERVERS from "../constants/servers.json"; import { dataLimit } from "./bridge"; import moment from "moment"; -export const client = new Client(SERVERS, { - timeout: 3000, - failoverThreshold: 3, - consoleOnFailover: true +export const CHAIN_ID = DEFAULT_CHAIN_ID.toString("hex"); +export const ADDRESS_PREFIX = DEFAULT_ADDRESS_PREFIX; +export const HIVE_API_NAME = "HIVE"; +export const DOLLAR_API_NAME = "HBD"; +export const HIVE_LANGUAGE_KEY = HIVE_API_NAME.toLowerCase(); +export const HIVE_HUMAN_NAME = "Hive"; +export const HIVE_HUMAN_NAME_UPPERCASE = "HIVE"; +export const DOLLAR_HUMAN_NAME = DOLLAR_API_NAME; +export const client = new Client(MAINNET_SERVERS, { + timeout: 4000, + failoverThreshold: 10, + consoleOnFailover: true, + addressPrefix: ADDRESS_PREFIX, + chainId: CHAIN_ID }); +export const HIVE_COLLATERALIZED_CONVERSION_FEE = 0.05; +export const HIVE_CONVERSION_COLLATERAL_RATIO = 2; + export interface Vote { percent: number; reputation: number; @@ -42,10 +56,7 @@ export interface DynamicGlobalProperties { } export interface FeedHistory { - current_median_history: { - base: string; - quote: string; - }; + current_median_history: Price; } export interface RewardFund { @@ -545,3 +556,89 @@ export interface BlogEntry { export const getBlogEntries = (username: string, limit: number = dataLimit): Promise => client.call("condenser_api", "get_blog_entries", [username, 0, limit]); + +export interface Price { + base: string; + quote: string; +} +/* group number and string */ +const gnas = (a: string) => { + const d = a.split(" "); + try { + const t = { n: parseFloat(d[0].replace(/,/g, "")), s: d[1] }; + return t; + } catch (e) { + return { n: 0, s: "" }; + } +}; +/** Translated from hive/hive/libraries/protocol/include/hive/protocol + Applies price to given asset in order to calculate its value in the second asset (like operator* ). + Additionally applies fee scale factor to specific asset in price. Used f.e. to apply fee to + collateralized conversions. Fee scale parameter in basis points. + */ +export function multiply_with_fee( + a: string, + p: Price, + fee: number, + apply_fee_to: string +): string | undefined { + if (a.indexOf(" ") == -1) return undefined; + let a_quantity: number; + let a_symbol: string; + { + const d = gnas(a); + a_quantity = d.n; + a_symbol = d.s; + } + const is_negative: boolean = a_quantity < 0; + let result: number = is_negative ? -a_quantity : a_quantity; + let scale_b = 1; + let scale_q = 1; + const { n: price_base_amount, s: price_base_symbol } = gnas(p.base); + const { n: price_quote_amount, s: price_quote_symbol } = gnas(p.quote); + if (apply_fee_to == price_base_symbol) { + scale_b += fee; + } else { + if (!(apply_fee_to == price_quote_symbol)) { + throw new Error(`Invalid fee symbol ${apply_fee_to} for price ${p.base}/${p.quote}`); + } + scale_q += fee; + } + if (a_symbol == price_base_symbol) { + result = (result * price_quote_amount * scale_q) / (price_base_amount * scale_b); + return `${is_negative ? -result : result} ${price_quote_symbol}`; + } else { + console.log({ + result, + price_base_amount, + scale_b, + price_quote_amount, + scale_q + }); + if (a_symbol !== price_quote_symbol) + throw new Error(`invalid ${a} != ${price_quote_symbol} nor ${price_base_symbol}`); + result = (result * price_base_amount * scale_b) / (price_quote_amount * scale_q); + return `${is_negative ? -result : result} ${price_base_symbol}`; + } +} + +export const estimateRequiredHiveCollateral = async ( + hbd_amount_to_get: number +): Promise => { + const fhistory = await getFeedHistory(); + if (fhistory.current_median_history === null) + throw new Error("Cannot estimate conversion collateral because there is no price feed."); + const needed_hive = multiply_with_fee( + `${hbd_amount_to_get} ${DOLLAR_API_NAME}`, + fhistory.current_median_history, + HIVE_COLLATERALIZED_CONVERSION_FEE, + HIVE_API_NAME + ); + if (!needed_hive) { + console.log({ needed_hive }); + return -1; + } + const { n: needed_hive_quantity, s: needed_hive_symbol } = gnas(needed_hive); + const _amount = needed_hive_quantity * HIVE_CONVERSION_COLLATERAL_RATIO; + return _amount; +}; diff --git a/src/common/api/operations.ts b/src/common/api/operations.ts index 69c3266a4f8..91f5823dc88 100644 --- a/src/common/api/operations.ts +++ b/src/common/api/operations.ts @@ -1,4 +1,7 @@ import hs from "hivesigner"; +// so far using browser only front end code to determine the redirect URL hasn't broken any tests +// import { appURL } from "../constants/defaults.json"; +import { HIVE_API_NAME } from "./hive"; import { AccountUpdateOperation, @@ -320,7 +323,56 @@ export const claimRewardBalance = ( return broadcastPostingOperations(username, opArray); }; - +export const claimRewardBalanceHiveEngineAssetJSON = ( + from: string, + to: string, + amount: string +): string => { + const [quantity, token_name] = amount.split(/ /); + const json = JSON.stringify({ + symbol: token_name + }); + return json; +}; +interface ClaimTokenParams { + id: "scot_claim_token"; + json: string; + required_auths: []; + required_posting_auths: [string]; +} +export const claimHiveEngineRewardBalance = (from: string, to: string, amount: string) => { + const params: ClaimTokenParams = { + id: "scot_claim_token", + json: claimRewardBalanceHiveEngineAssetJSON(from, to, amount), + required_auths: [], + required_posting_auths: [from] + }; + const opArray: Operation[] = [["custom_json", params]]; + return broadcastPostingOperations(from, opArray); +}; +/* +export const HECustomJSONWithPostingKey = (key: PrivateKey, from:string, json: string): Promise => { + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [], + required_posting_auths: [from] + }; + return hiveClient.broadcast.json(op, key); +} +export const HECustomJSONPostingKc = (from:string, json: string, description: string): Promise => { + return keychain.customJson(from, "ssc-mainnet-hive", "Posting", json, description); +} +export const HECustomJSONPostingHot = (from:string, json: string, destination: string) => { + const params = { + authority: "posting", + required_auths: `[]`, + required_posting_auths: `["${from}"]`, + id: "ssc-mainnet-hive", + json + } + hotSign("custom-json", params, destination); +}*/ export const transfer = ( from: string, key: PrivateKey, @@ -349,7 +401,9 @@ export const transferHot = (from: string, to: string, amount: string, memo: stri } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -441,7 +495,9 @@ export const transferToSavingsHot = (from: string, to: string, amount: string, m } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -455,7 +511,6 @@ export const transferToSavingsKc = (from: string, to: string, amount: string, me memo } ]; - return keychain.broadcast(from, [op], "Active"); }; @@ -549,7 +604,9 @@ export const limitOrderCreateHot = ( ]; const params: Parameters = { - callback: `https://ecency.com/market${idPrefix === OrderIdPrefix.SWAP ? "#swap" : ""}` + callback: `${document.location.protocol}//${document.location.host}/market${ + idPrefix === OrderIdPrefix.SWAP ? "#swap" : "" + }` }; return hs.sendOperation(op, params, () => {}); }; @@ -563,7 +620,9 @@ export const limitOrderCancelHot = (owner: string, orderid: number) => { } ]; - const params: Parameters = { callback: `https://ecency.com/market` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/market` + }; return hs.sendOperation(op, params, () => {}); }; @@ -643,7 +702,9 @@ export const convertHot = (owner: string, amount: string) => { } ]; - const params: Parameters = { callback: `https://ecency.com/@${owner}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${owner}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -660,6 +721,75 @@ export const convertKc = (owner: string, amount: string) => { return keychain.broadcast(owner, [op], "Active"); }; +const transferHiveEngineAssetJSON = ( + from: string, + to: string, + amount: string, + memo: string +): string => { + const [quantity, token_name] = amount.replace(/,/g, "").split(/ /); + const json = JSON.stringify({ + // is it always 'tokens'? + contractName: "tokens", + contractAction: "transfer", + contractPayload: { + symbol: token_name, + to: to, + quantity, + memo: memo + } + }); + return json; +}; + +export const transferHiveEngineAsset = ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + const json = transferHiveEngineAssetJSON(from, to, amount, memo); + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [from], + required_posting_auths: [] + }; + return hiveClient.broadcast.json(op, key); +}; +export const transferHiveEngineAssetKc = ( + from: string, + to: string, + amount: string, + memo: string +) => { + const json = transferHiveEngineAssetJSON(from, to, amount, memo); + return keychain.customJson( + from, + "ssc-mainnet-hive", + "Active", + json, + "Hive Engine Asset Transfer" + ); +}; +export const transferHiveEngineAssetHot = ( + from: string, + to: string, + amount: string, + memo: string +) => { + const json = transferHiveEngineAssetJSON(from, to, amount, memo); + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: "ssc-mainnet-hive", + json + }; + hotSign("custom-json", params, `@${from}/wallet`); +}; + export const transferFromSavings = ( from: string, key: PrivateKey, @@ -693,7 +823,9 @@ export const transferFromSavingsHot = (from: string, to: string, amount: string, } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -761,7 +893,9 @@ export const claimInterestHot = (from: string, to: string, amount: string, memo: } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperations([op, cop], params, () => {}); }; @@ -787,67 +921,158 @@ export const claimInterestKc = (from: string, to: string, amount: string, memo: return keychain.broadcast(from, [op, cop], "Active"); }; - -export const transferToVesting = ( - from: string, +export const collateralizedConvert = ( + owner: string, key: PrivateKey, - to: string, amount: string ): Promise => { const op: Operation = [ - "transfer_to_vesting", + "collateralized_convert", { - from, - to, - amount + owner, + amount, + requestid: new Date().getTime() >>> 0 } ]; - return hiveClient.broadcast.sendOperations([op], key); }; - -export const transferToVestingHot = (from: string, to: string, amount: string) => { +export const collateralizedConvertHot = (owner: string, amount: string): void => { const op: Operation = [ - "transfer_to_vesting", + "collateralized_convert", { - from, - to, - amount + 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: document.location.toString() + }; + hs.sendOperation(op, params, () => {}); }; - -export const transferToVestingKc = (from: string, to: string, amount: string) => { +export const collateralizedConvertKc = (owner: string, amount: string) => { const op: Operation = [ - "transfer_to_vesting", + "collateralized_convert", { - from, - to, - amount + owner, + amount, + requestid: new Date().getTime() >>> 0 } ]; + return keychain.broadcast(owner, [op], "Active"); +}; +export const createTransferToVestingOp = (from: string, to: string, amount: string): Operation => { + const parts = amount.split(/ /); + const currency = parts[parts.length - 1]; + const quantity = parts[0].replace(/,/g, ""); + console.log(from, to, amount); + if (currency === HIVE_API_NAME) { + return [ + "transfer_to_vesting", + { + from, + to, + amount + } + ]; + } else { + return [ + "custom_json", + { + id: "ssc-mainnet-hive", + required_auths: [from], + required_posting_auths: [], + json: JSON.stringify({ + contractName: "tokens", + contractAction: "stake", + contractPayload: { + symbol: currency, + to: to, + quantity: quantity + } + }) + } + ]; + } +}; + +export const transferToVesting = ( + from: string, + key: PrivateKey, + to: string, + amount: string +): Promise => { + const op: Operation = createTransferToVestingOp(from, to, amount); + + return hiveClient.broadcast.sendOperations([op], key); +}; + +export const transferToVestingHot = (from: string, to: string, amount: string) => { + const op: Operation = createTransferToVestingOp(from, to, amount); + + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; + return hs.sendOperation(op, params, () => {}); +}; + +export const transferToVestingKc = (from: string, to: string, amount: string) => { + const op: Operation = createTransferToVestingOp(from, to, amount); return keychain.broadcast(from, [op], "Active"); }; +export const createDelegateVestingSharesOp = ( + delegator: string, + delegatee: string, + vestingShares: string +): Operation => { + if (!/[0-9]+(.[0-9]+)? [A-Z][A-Z0-9]+/.test(vestingShares)) { + throw new Error(`Invalid vestingShares Amount specified: "${vestingShares}"`); + } + const parts = vestingShares.split(/ /); + const currency = parts[parts.length - 1]; + const quantity = parts[0].replace(/,/g, ""); + if (currency === "HP" || currency === "HIVE") { + throw new Error(`Invalid parameter: ${currency} can be an HiveEngine asset or VESTS`); + } + if (currency === "VESTS") { + return [ + "delegate_vesting_shares", + { + delegator, + delegatee, + vesting_shares: vestingShares + } + ]; + } else { + return [ + "custom_json", + { + id: "ssc-mainnet-hive", + required_auths: [delegator], + required_posting_auths: [], + json: JSON.stringify({ + contractName: "tokens", + contractAction: "delegate", + contractPayload: { + symbol: currency, + to: delegatee, + quantity: quantity + } + }) + } + ]; + } +}; + 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 = createDelegateVestingSharesOp(delegator, delegatee, vestingShares); return hiveClient.broadcast.sendOperations([op], key); }; @@ -856,16 +1081,13 @@ export const delegateVestingSharesHot = ( delegatee: string, vestingShares: string ) => { - const op: Operation = [ - "delegate_vesting_shares", - { - delegator, - delegatee, - vesting_shares: vestingShares - } - ]; - - const params: Parameters = { callback: `https://ecency.com/@${delegator}/wallet` }; + const op: Operation = createDelegateVestingSharesOp(delegator, delegatee, vestingShares); + const parts = vestingShares.split(/ /); + const currency = parts[parts.length - 1]; + const quantity = parts[0].replace(/,/g, ""); + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${delegator}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -874,15 +1096,7 @@ export const delegateVestingSharesKc = ( delegatee: string, vestingShares: string ) => { - const op: Operation = [ - "delegate_vesting_shares", - { - delegator, - delegatee, - vesting_shares: vestingShares - } - ]; - + const op: Operation = createDelegateVestingSharesOp(delegator, delegatee, vestingShares); return keychain.broadcast(delegator, [op], "Active"); }; @@ -910,9 +1124,43 @@ export const withdrawVestingHot = (account: string, vestingShares: string) => { vesting_shares: vestingShares } ]; +}; - const params: Parameters = { callback: `https://ecency.com/@${account}/wallet` }; - return hs.sendOperation(op, params, () => {}); +export const cancelWithdrawVesting = ( + account: string, + key: PrivateKey, + txID: string +): Promise => { + const op: Operation = createCancelPowerDownOp(account, txID); + return hiveClient.broadcast.sendOperations([op], key); +}; +export const cancelWithdrawVestingHot = (account: string, txID: string) => { + const op: Operation = createCancelPowerDownOp(account, txID); + const params: Parameters = { + callback: document.location.toString() + }; + hs.sendOperation(op, params, () => {}); +}; +export const cancelWithdrawVestingKc = (account: string, txID: string) => { + const op: Operation = createCancelPowerDownOp(account, txID); + return keychain.broadcast(account, [op], "Active"); +}; +export const createCancelPowerDownOp = (account: string, txID: string): Operation => { + return [ + "custom_json", + { + id: "ssc-mainnet-hive", + required_auths: ["leprechaun"], + required_posting_auths: [], + json: JSON.stringify({ + contractName: "tokens", + contractAction: "cancelUnstake", + contractPayload: { + txID: txID + } + }) + } + ]; }; export const withdrawVestingKc = (account: string, vestingShares: string) => { @@ -963,7 +1211,9 @@ export const setWithdrawVestingRouteHot = ( } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; diff --git a/src/common/app.tsx b/src/common/app.tsx index dd96b707ed7..9713d96964b 100644 --- a/src/common/app.tsx +++ b/src/common/app.tsx @@ -22,7 +22,6 @@ import i18n from "i18next"; import { pageMapDispatchToProps, pageMapStateToProps } from "./pages/common"; import { connect } from "react-redux"; import loadable from "@loadable/component"; - // Define lazy pages const ProfileContainer = loadable(() => import("./pages/profile-functional")); const ProfilePage = (props: any) => ; diff --git a/src/common/components/boost/index.tsx b/src/common/components/boost/index.tsx index 6ebabdd1f5b..b058f78eab1 100644 --- a/src/common/components/boost/index.tsx +++ b/src/common/components/boost/index.tsx @@ -29,6 +29,7 @@ import _c from "../../util/fix-class-names"; import formattedNumber from "../../util/formatted-number"; import { checkAllSvg } from "../../img/svg"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/buy-sell-hive/index.tsx b/src/common/components/buy-sell-hive/index.tsx index dcd236bdf63..6c2e5a551db 100644 --- a/src/common/components/buy-sell-hive/index.tsx +++ b/src/common/components/buy-sell-hive/index.tsx @@ -30,6 +30,7 @@ import { AnyAction, bindActionCreators, Dispatch } from "redux"; import { connect } from "react-redux"; import { AppState } from "../../store"; import { PrivateKey } from "@hiveio/dhive"; +import { base } from "../../constants/defaults.json"; export enum TransactionType { None = 0, diff --git a/src/common/components/comment/__snapshots__/index.spec.tsx.snap b/src/common/components/comment/__snapshots__/index.spec.tsx.snap index 3edac66ef66..894aa5e351a 100644 --- a/src/common/components/comment/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/comment/__snapshots__/index.spec.tsx.snap @@ -289,6 +289,7 @@ exports[`(1) Default render 1`] = ` "currencyRate": 1, "currencySymbol": "$", "filter": "hot", + "footer": "", "hasKeyChain": false, "hsClientId": "ecency.app", "intro": true, @@ -297,10 +298,20 @@ exports[`(1) Default render 1`] = ` "lang": "en-US", "lastIndexPath": null, "listStyle": "row", + "lowRewardThreshold": 100, + "menuOrder": Array [ + "points", + "hive", + "engine", + "spk", + ], "newVersion": null, "notifications": true, "nsfw": false, "searchIndexCount": 10000000, + "showFrontEnd": true, + "showRewardSplit": true, + "showSelfVote": true, "tag": "", "theme": "day", "usePrivate": true, @@ -626,6 +637,7 @@ exports[`(2) Cancellable, in progress 1`] = ` "currencyRate": 1, "currencySymbol": "$", "filter": "hot", + "footer": "", "hasKeyChain": false, "hsClientId": "ecency.app", "intro": true, @@ -634,10 +646,20 @@ exports[`(2) Cancellable, in progress 1`] = ` "lang": "en-US", "lastIndexPath": null, "listStyle": "row", + "lowRewardThreshold": 100, + "menuOrder": Array [ + "points", + "hive", + "engine", + "spk", + ], "newVersion": null, "notifications": true, "nsfw": false, "searchIndexCount": 10000000, + "showFrontEnd": true, + "showRewardSplit": true, + "showSelfVote": true, "tag": "", "theme": "day", "usePrivate": true, diff --git a/src/common/components/community-rewards-registration/index.tsx b/src/common/components/community-rewards-registration/index.tsx index d2fa9b7bf6a..851dbf2dd78 100644 --- a/src/common/components/community-rewards-registration/index.tsx +++ b/src/common/components/community-rewards-registration/index.tsx @@ -22,6 +22,7 @@ import { import { getRewardedCommunities } from "../../api/private-api"; import { _t } from "../../i18n"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/converts-collateralized/__snapshots__/index.spec.tsx.snap b/src/common/components/converts-collateralized/__snapshots__/index.spec.tsx.snap index 678db143a49..1bc2232e933 100644 --- a/src/common/components/converts-collateralized/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/converts-collateralized/__snapshots__/index.spec.tsx.snap @@ -50,7 +50,7 @@ exports[`(1) Default render 1`] = ` className="date" title="Saturday, September 17, 2022 1:42 AM" > - 3 months ago + 8 months ago diff --git a/src/common/components/converts/__snapshots__/index.spec.tsx.snap b/src/common/components/converts/__snapshots__/index.spec.tsx.snap index 5baf99ecbe9..e2abce00f98 100644 --- a/src/common/components/converts/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/converts/__snapshots__/index.spec.tsx.snap @@ -40,7 +40,7 @@ exports[`(1) Default render 1`] = ` className="date" title="Sunday, February 6, 2022 2:59 AM" > - 10 months ago + a year ago diff --git a/src/common/components/delegated-vesting-hive-engine/index.tsx b/src/common/components/delegated-vesting-hive-engine/index.tsx new file mode 100644 index 00000000000..14ea96446c2 --- /dev/null +++ b/src/common/components/delegated-vesting-hive-engine/index.tsx @@ -0,0 +1,203 @@ +import React, { Component } from "react"; +import { History } from "history"; +import { Modal } from "react-bootstrap"; +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 BaseComponent from "../base"; +import ProfileLink from "../profile-link"; +import UserAvatar from "../user-avatar"; +import LinearProgress from "../linear-progress"; +import Tooltip from "../tooltip"; +import KeyOrHotDialog from "../key-or-hot-dialog"; +import { error } from "../feedback"; +import { + claimRewards, + getHiveEngineTokenBalances, + getUnclaimedRewards, + getTokenDelegations, + TokenStatus, + DelegationEntry, + Unstake, + getPendingUnstakes +} from "../../api/hive-engine"; +import { + undelegateHiveEngineKey, + undelegateHiveEngineHs, + undelegateHiveEngineKc, + formatError +} from "../../api/operations"; +import { _t } from "../../i18n"; +import { vestsToHp } from "../../helper/vesting"; +import parseAsset from "../../helper/parse-asset"; +import formattedNumber from "../../util/formatted-number"; +import _c from "../../util/fix-class-names"; +import HiveEngineToken, { HiveEngineTokenEntryDelta } from "../../helper/hive-engine-wallet"; + +interface Props { + history: History; + global: Global; + activeUser: ActiveUser | null; + account: Account; + dynamicProps: DynamicProps; + signingKey: string; + addAccount: (data: Account) => void; + setSigningKey: (key: string) => void; + onHide: () => void; + updateActiveUser: (data?: Account) => void; + modifyTokenValues: (delta: HiveEngineTokenEntryDelta) => void; + hiveEngineToken: HiveEngineToken; + delegationList: Array; +} + +interface State { + inProgress: boolean; + data: DelegationEntry[]; + hideList: boolean; +} + +export class ListHE extends BaseComponent { + state: State = { + inProgress: false, + data: [], + hideList: false + }; + componentDidMount() { + const { delegationList, hiveEngineToken, activeUser } = this.props; + const data = delegationList.filter( + (d) => d.symbol === hiveEngineToken.symbol && activeUser && d.from === activeUser.username + ); + this.setState({ data }); + } + render() { + const { data, hideList, inProgress } = this.state; + const { dynamicProps, activeUser, account, updateActiveUser, delegationList, hiveEngineToken } = + this.props; + const { hivePerMVests } = dynamicProps; + + return ( +
+
+
+ {data.length === 0 &&
{_t("g.empty-list")}
} + {data.map((x) => { + const { symbol, quantity, to: username } = x; + const deleteBtn = + activeUser && activeUser.username === account.name + ? KeyOrHotDialog({ + ...this.props, + activeUser: activeUser, + children: ( + + {_t("delegated-vesting.undelegate")} + + ), + onToggle: () => { + const { hideList } = this.state; + this.stateSet({ hideList: !hideList }); + }, + onKey: (key) => { + this.stateSet({ inProgress: true }); + undelegateHiveEngineKey( + activeUser.username, + key, + symbol, + username, + quantity + ) + .then((TxC) => { + const { modifyTokenValues } = this.props; + this.stateSet({ + data: data.filter((y) => y.to != x.to) + }); + modifyTokenValues({ symbol, delegationsOutDelta: -quantity }); + }) + .catch((err) => error(err.message)) + .finally(() => { + this.setState({ inProgress: false }); + updateActiveUser(activeUser.data); + }); + }, + onHot: () => { + undelegateHiveEngineHs(activeUser.username, username, symbol, quantity); + }, + onKc: () => { + this.stateSet({ inProgress: true }); + undelegateHiveEngineKc(activeUser.username, username, symbol, quantity) + .then(() => { + const { modifyTokenValues } = this.props; + this.stateSet({ + data: data.filter((y) => y.to !== x.to) + }); + modifyTokenValues({ symbol, delegationsOutDelta: -quantity }); + }) + .catch((err) => error(err.message)) + .finally(() => { + this.stateSet({ inProgress: false }); + updateActiveUser(activeUser.data); + }); + } + }) + : null; + return ( +
+
+ {ProfileLink({ + ...this.props, + username, + children: ( + <> + {UserAvatar({ + ...this.props, + username: x.to, + size: "small" + })} + + ) + })} +
+ {ProfileLink({ + ...this.props, + username, + children: {username} + })} +
+
+
+ + {formattedNumber(x.quantity, { suffix: x.symbol })} + + {deleteBtn} +
+
+ ); + })} +
+
+
+ ); + } +} + +export default class DelegatedVestingHE extends Component { + render() { + const { onHide, hiveEngineToken } = this.props; + return ( + <> + + + {_t("staked", hiveEngineToken)} + + + + + + + ); + } +} diff --git a/src/common/components/delegated-vesting/index.tsx b/src/common/components/delegated-vesting/index.tsx index 02e85d8b6ef..6dacb2ffeef 100644 --- a/src/common/components/delegated-vesting/index.tsx +++ b/src/common/components/delegated-vesting/index.tsx @@ -36,6 +36,7 @@ import formattedNumber from "../../util/formatted-number"; import _c from "../../util/fix-class-names"; import MyPagination from "../pagination"; +import { base } from "../../constants/defaults.json"; interface Props { history: History; diff --git a/src/common/components/engine-tokens-estimated/index.spec.tsx b/src/common/components/engine-tokens-estimated/index.spec.tsx index 523495d2dca..c446d491451 100644 --- a/src/common/components/engine-tokens-estimated/index.spec.tsx +++ b/src/common/components/engine-tokens-estimated/index.spec.tsx @@ -1,7 +1,7 @@ import React from "react"; import { EngineTokensEstimated } from "./index"; import renderer from "react-test-renderer"; -import { dynamicPropsIntance1 } from "../../helper/test-helper"; +import { dynamicPropsIntance1, allOver } from "../../helper/test-helper"; const props = { dynamicProps: dynamicPropsIntance1, @@ -10,6 +10,7 @@ const props = { const component = renderer.create(); -it("(1) Default render", () => { +it("(1) Default render", async () => { + await allOver(); expect(component.toJSON()).toMatchSnapshot(); }); diff --git a/src/common/components/entry-he-payout/__snapshots__/index.spec.tsx.snap b/src/common/components/entry-he-payout/__snapshots__/index.spec.tsx.snap new file mode 100644 index 00000000000..e84932c42f4 --- /dev/null +++ b/src/common/components/entry-he-payout/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`(1) Default render 1`] = ` +
+ + + +
+`; + +exports[`(2) Detail render 1`] = ` +
+ + 2.4 mPESOS, 38.7 kPIMP, 42 LEO + +
+`; + +exports[`(3) Detail render with full power 1`] = ` +
+ + 2.4 mPESOS, 38.7 kPIMP, 42 LEO + +
+`; + +exports[`(4) Detail render with max payout 1`] = ` +
+ + 1.19 LEO, 1.94 POB + +
+`; + +exports[`(5) Default with max payout 1`] = ` +
+ + 1.6 ARCHON, 4.69 CTP, 358.27 kPIMP, 1.08 POB + +
+`; diff --git a/src/common/components/entry-he-payout/_index.scss b/src/common/components/entry-he-payout/_index.scss new file mode 100644 index 00000000000..499090e88a2 --- /dev/null +++ b/src/common/components/entry-he-payout/_index.scss @@ -0,0 +1,58 @@ +.entry-payout { + color: $steel-grey; + margin-right: 15px; + cursor: default; + + @media (min-width: $sm-break) { + margin-right: 25px; + } + @media (max-width: $xxsm-break) { + font-size: 12px; + margin-right: 5px; + } + @include themify(night) { + font-weight: 500; + } + + &.payout-declined { + color: lighten($steel-grey, 15); + text-decoration: line-through; + } + + &.payout-limit-hit { + color: lighten($steel-grey, 15); + opacity: .8; + } +} + +.payout-popover { + max-width: none !important; + font-size: .92em !important; + .popover-body { + max-width: none !important; + + .payout-popover-content { + p { + margin: 0 0 8px 0; + display: flex; + + &:last-of-type { + margin-bottom: 0; + } + + .label { + display: inline-block; + font-weight: 500; + width: 130px; + } + + .value { + } + } + } + } +} + +.grey { + color: grey; +} diff --git a/src/common/components/entry-he-payout/index.spec.tsx b/src/common/components/entry-he-payout/index.spec.tsx new file mode 100644 index 00000000000..53777a7c7e0 --- /dev/null +++ b/src/common/components/entry-he-payout/index.spec.tsx @@ -0,0 +1,66 @@ +import React from "react"; + +import { EntryHEPayoutDisplay } from "./index"; +import TestRenderer from "react-test-renderer"; + +import { + globalInstance, + entryInstance1, + dynamicPropsIntance1, + allOver +} from "../../helper/test-helper"; + +jest.mock("moment", () => () => ({ + fromNow: () => "in 4 days", + format: (f: string, s: string) => "2020-01-01 23:12:00" +})); + +it("(1) Default render", async () => { + const props = { + scotRewards: [] + }; + + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("(2) Detail render", async () => { + const props = { + scotRewards: ["2.4 mPESOS", "38.7 kPIMP", "42 LEO"] + }; + + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("(3) Detail render with full power", async () => { + const props = { + scotRewards: ["2.4 mPESOS", "38.7 kPIMP", "42 LEO"] + }; + + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("(4) Detail render with max payout", async () => { + const props = { + scotRewards: ["1.19 LEO", "1.94 POB"] + }; + + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("(5) Default with max payout", async () => { + const props = { + scotRewards: "1.6 ARCHON, 4.69 CTP, 358.27 kPIMP, 1.08 POB".split(/, /) + }; + + const renderer = await TestRenderer.create(); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/common/components/entry-he-payout/index.tsx b/src/common/components/entry-he-payout/index.tsx new file mode 100644 index 00000000000..14355c84da8 --- /dev/null +++ b/src/common/components/entry-he-payout/index.tsx @@ -0,0 +1,149 @@ +import React, { Component, Fragment } from "react"; + +import { Popover, OverlayTrigger } from "react-bootstrap"; + +import { Entry } from "../../store/entries/types"; +import { Global } from "../../store/global/types"; +import { DynamicProps } from "../../store/dynamic-props/types"; +import { ScotRewardsInformation, getScotRewardsInformation } from "../../api/hive-engine"; + +import FormattedCurrency from "../formatted-currency/index"; + +import parseAsset from "../../helper/parse-asset"; +import { dateToFullRelative } from "../../helper/parse-date"; + +import formattedNumber from "../../util/formatted-number"; + +import { _t } from "../../i18n"; + +import _c from "../../util/fix-class-names"; +import { hiveEngineSvg } from "../../img/svg"; + +interface Props { + global: Global; + dynamicProps: DynamicProps; + entry: Entry; +} + +interface State { + loadingScotRewardsInformation: boolean; + scotRewards: Array; + scotRewardsError: boolean; +} + +interface DisplayProps { + scotRewards: Array; +} + +export function HiveEngineLoadingData(props: {}) { + return {hiveEngineSvg}; +} + +export function EntryHEPayoutDisplay(props: DisplayProps) { + const { scotRewards } = props; + return ( +
+ {scotRewards.join(", ")} +
+ ); +} + +export class EntryHEPayout extends Component { + state: State = { + loadingScotRewardsInformation: true, + scotRewardsError: false, + scotRewards: [] + }; + + componentDidMount() { + const { entry } = this.props; + const { author, permlink } = entry; + getScotRewardsInformation(author, permlink) + .then((rawScotRewardsInformation) => { + try { + if (this === null) return; + const rewardsHash: { [tokenName: string]: string } = {}; + for (const tokenName in rawScotRewardsInformation) { + const ti = rawScotRewardsInformation[tokenName]; + const { total_payout_value, pending_token, precision } = ti; + let score: number = (total_payout_value + pending_token) * Math.pow(10, -precision); + let prefix = ""; + if (pending_token + total_payout_value == 0) { + continue; + } else if (score < 0.0000001) { + score = (total_payout_value + pending_token) * Math.pow(10, 9 - precision); + prefix = "n"; + } else if (score < 0.0001) { + score = (total_payout_value + pending_token) * Math.pow(10, 6 - precision); + // mu symbol + prefix = "\u03bc"; + } else if (score < 0.1) { + score = (total_payout_value + pending_token) * Math.pow(10, 3 - precision); + prefix = "m"; + } else if (score > 10000) { + score /= 1000; + prefix = "k"; + } else if (score > 1000000) { + score = (total_payout_value + pending_token) * Math.pow(10, -6 - precision); + prefix = "M"; + } + // @ts-ignore + rewardsHash[prefix + tokenName] = + (formattedNumber(score, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + truncate: true, + debug: false, + suffix: prefix + tokenName + }) as string) + ""; + } + + const rewardsArray = Object.keys(rewardsHash).map((tokenName) => rewardsHash[tokenName]); + this.setState({ + loadingScotRewardsInformation: false, + scotRewards: rewardsArray + }); + } catch (e) { + console.log(e); + this.setState({ loadingScotRewardsInformation: false, scotRewardsError: true }); + } + }) + .catch((e) => { + console.log(e); + this.setState({ loadingScotRewardsInformation: false, scotRewardsError: true }); + }); + } + + render() { + const { loadingScotRewardsInformation, scotRewards, scotRewardsError } = this.state; + return scotRewardsError ? ( + Hive Engine Server Error} + delay={1000} + > + {hiveEngineSvg} + + ) : loadingScotRewardsInformation ? ( + Loading Hive Engine Rewards Data In Progress...} + delay={1000} + > + + + ) : ( + + ); + } +} + +export default (p: Props) => { + const props = { + global: p.global, + dynamicProps: p.dynamicProps, + entry: p.entry + }; + + return ; +}; diff --git a/src/common/components/entry-list-item/__snapshots__/index.spec.tsx.snap b/src/common/components/entry-list-item/__snapshots__/index.spec.tsx.snap index 8c7dfd5c31c..07cd7f9fbc0 100644 --- a/src/common/components/entry-list-item/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/entry-list-item/__snapshots__/index.spec.tsx.snap @@ -92,6 +92,33 @@ exports[`(1) Default render 1`] = ` > 5d +   + 50 + % HP +   + + + +
`; + +exports[`(7) Default render with flags off 1`] = ` +
+ +`; + +exports[`(8) Default render with flags off 1`] = ` +
+ +`; + +exports[`(9) Up to 50 HBD accepted with nsfw on flags off 1`] = ` +
+ +`; + +exports[`(10) Nsfw on with other flags off. Maximum accepted reward 50 HBD but threshold at 10 1`] = ` +
+ +`; diff --git a/src/common/components/entry-list-item/_index.scss b/src/common/components/entry-list-item/_index.scss index 14d97abfce2..8a25e6c914c 100644 --- a/src/common/components/entry-list-item/_index.scss +++ b/src/common/components/entry-list-item/_index.scss @@ -179,6 +179,15 @@ width: 20px; } +.tiny-image { + width: 20px; + max-height: 30px; +} + +.tiny-image:hover { + width: 25px; +} + .author-down-arrow { display: none; diff --git a/src/common/components/entry-list-item/index.spec.tsx b/src/common/components/entry-list-item/index.spec.tsx index 22b1a52c934..c1e8a050fa2 100644 --- a/src/common/components/entry-list-item/index.spec.tsx +++ b/src/common/components/entry-list-item/index.spec.tsx @@ -164,3 +164,85 @@ it("(6) Cross post. Bottom menu", async () => { await allOver(); expect(renderer.toJSON()).toMatchSnapshot(); }); + +it("(7) Default render with flags off", async () => { + const renderer = await TestRenderer.create( + + + + ); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("(8) Default render with flags off", async () => { + const renderer = await TestRenderer.create( + + + + ); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("(9) Up to 50 HBD accepted with nsfw on flags off", async () => { + const props = { + ...defProps, + entry: { + ...entryInstance1, + json_metadata: { + ...entryInstance1.json_metadata, + tags: [...entryInstance1.json_metadata.tags, "nsfw"] + }, + max_accepted_payout: "50" + }, + global: { + ...globalInstance, + nsfw: true, + showSelfVote: false, + showRewardSplit: false, + lowRewardThreshold: 1000 + } + }; + const renderer = await TestRenderer.create( + + + + ); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("(10) Nsfw on with other flags off. Maximum accepted reward 50 HBD but threshold at 10", async () => { + const props = { + ...defProps, + entry: { + ...entryInstance1, + json_metadata: { + ...entryInstance1.json_metadata, + tags: [...entryInstance1.json_metadata.tags, "nsfw"] + }, + max_accepted_payout: "50" + }, + global: { + ...globalInstance, + nsfw: true, + showSelfVote: false, + showRewardSplit: false, + lowRewardThreshold: 10 + } + }; + const renderer = await TestRenderer.create( + + + + ); + await allOver(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/common/components/entry-list-item/index.tsx b/src/common/components/entry-list-item/index.tsx index 5c6fe4fdfa9..f4467bcdda7 100644 --- a/src/common/components/entry-list-item/index.tsx +++ b/src/common/components/entry-list-item/index.tsx @@ -45,12 +45,14 @@ import { menuDownSvg } from "../../img/svg"; -import defaults from "../../constants/defaults.json"; +import defaults from "./../../constants/defaults.json"; import { ProfilePopover } from "../profile-popover"; import { match } from "react-router-dom"; import { getPost } from "../../api/bridge"; import { SearchResult } from "../../api/search-api"; - +import appName from "./../../helper/app-name"; +import formattedNumber from "../../util/formatted-number"; +import EntryHEPayout from "../entry-he-payout"; setProxyBase(defaults.imageServer); interface MatchParams { @@ -207,10 +209,8 @@ export default class EntryListItem extends Component { // const account = accounts?.find((x) => x.name === accountUsername) as FullAccount const pageAccount = account as FullAccount; const pinned = account && pageAccount.profile?.pinned; - - const fallbackImage = global.isElectron - ? "./img/fallback.png" - : require("../../img/fallback.png"); + const { showSelfVote, showRewardSplit, lowRewardThreshold, showFrontEnd } = global; + const fallbackImage = "./img/fallback.png"; const noImage = global.isElectron ? "./img/noimage.svg" : require("../../img/noimage.svg"); const nsfwImage = global.isElectron ? "./img/nsfw.png" : require("../../img/nsfw.png"); const crossPost = !!theEntry.original_entry; @@ -286,6 +286,14 @@ export default class EntryListItem extends Component { const cls = `entry-list-item ${promoted ? "promoted-item" : ""} ${global.filter}`; + const selfVoteEntryRShares = + entry.active_votes?.find((x) => x.voter == entry.author)?.rshares ?? 0; + const selfVote = selfVoteEntryRShares > 0; + const HPPortion = 100 * (1 - entry.percent_hbd / 20000); + const maxPayout: number = parseFloat(entry.max_accepted_payout); + const app = appName(entry.json_metadata.app); + const appShort = app.split("/")[0].split(" ")[0]; + return mounted ? (
{(() => { @@ -361,6 +369,24 @@ export default class EntryListItem extends Component { {dateRelative} + {showSelfVote && selfVote && <> {_t("entry.self_voted")}} + {showRewardSplit && maxPayout > 0 && <> {HPPortion}% HP} + {showFrontEnd && app && ( + <> +   + + + + + + )} + {maxPayout > 0 && maxPayout <= lowRewardThreshold && ( + <>  ≤ {formattedNumber(maxPayout, { fractionDigits: 3, suffix: "HBD" })} + )}
{isPinned && ( @@ -523,6 +549,10 @@ export default class EntryListItem extends Component { ...this.props, entry })} + {EntryHEPayout({ + ...this.props, + entry + })} {EntryVotes({ ...this.props, entry diff --git a/src/common/components/entry-read-time/index.tsx b/src/common/components/entry-read-time/index.tsx index f49d357a3f2..b1e778a6e11 100644 --- a/src/common/components/entry-read-time/index.tsx +++ b/src/common/components/entry-read-time/index.tsx @@ -2,32 +2,42 @@ import React, { useState, useEffect } from "react"; import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { _t } from "../../i18n"; import { informationVariantSvg } from "../../img/svg"; - +import appName from "../../helper/app-name"; +import { EntryVote } from "../../store/entries/types"; export const ReadTime = (props: any) => { const { entry, global, isVisible, toolTip } = props; + const { showSelfVote, showRewardSplit, lowRewardThreshold } = global; const [readTime, setReadTime] = useState(0); const [wordCount, setWordCount] = useState(0); + const selfVoteEntryRShares = + entry.active_votes?.find((x: EntryVote) => x.voter == entry.author)?.rshares ?? 0; + const selfVote = selfVoteEntryRShares > 0; + const HPPortion = 100 * (1 - entry.percent_hbd / 20000); + const maxPayout: number = parseFloat(entry.max_accepted_payout); + const app = appName(entry.json_metadata.app); + const appShort = app.split("/")[0].split(" ")[0]; + useEffect(() => { calculateExtras(); }, [entry]); const calculateExtras = async () => { - const entryCount = countWords(entry.body) + const entryCount = countWords(entry.body); const wordPerMinuite: number = 225; setWordCount(entryCount); setReadTime(Math.ceil(entryCount / wordPerMinuite)); }; - const countWords = (entry: string) =>{ - const cjkEntry = new RegExp("[\u4E00-\u9FFF]","g"); - entry = entry.replace(cjkEntry," {CJK} "); - const splitEntry: any = entry.trim().split(/\s+/); + const countWords = (entry: string) => { + const cjkEntry = new RegExp("[\u4E00-\u9FFF]", "g"); + entry = entry.replace(cjkEntry, " {CJK} "); + const splitEntry: any = entry.trim().split(/\s+/); const cjkCount = splitEntry.filter((e: string) => e === "{CJK}"); const count: any = splitEntry.includes("{CJK}") ? cjkCount.length : splitEntry.length; return count; -} + }; return toolTip ? (
@@ -45,6 +55,9 @@ export const ReadTime = (props: any) => {

{_t("entry.post-read-time")} {readTime} {_t("entry.post-read-minuites")}

+ {selfVote &&

{_t("entry.self_voted")}

} + {maxPayout > 0 &&

{HPPortion}% HP

} + {maxPayout > 0 && maxPayout < lowRewardThreshold &&

≤ {maxPayout} HBD

}
diff --git a/src/common/components/entry-tip-btn/index.tsx b/src/common/components/entry-tip-btn/index.tsx index c2104fc414d..8a304b21829 100644 --- a/src/common/components/entry-tip-btn/index.tsx +++ b/src/common/components/entry-tip-btn/index.tsx @@ -55,7 +55,10 @@ export class TippingDialog extends Component { const transactions: Transactions = { list: [], loading: false, - group: "" + group: "", + oldest: null, + newest: null, + debug: "" }; return ( diff --git a/src/common/components/entry-vote-btn/index.tsx b/src/common/components/entry-vote-btn/index.tsx index 24e06fa2754..df679f2e7ee 100644 --- a/src/common/components/entry-vote-btn/index.tsx +++ b/src/common/components/entry-vote-btn/index.tsx @@ -545,8 +545,8 @@ export class EntryVoteBtn extends BaseComponent { activeUser={activeUser as any} isPostSlider={this.props.isPostSlider} onClick={this.vote} - upVoted={upVoted} - downVoted={downVoted} + upVoted={upVoted ? true : false} + downVoted={downVoted ? true : false} />
diff --git a/src/common/components/error-boundary.tsx b/src/common/components/error-boundary.tsx new file mode 100644 index 00000000000..499123669a8 --- /dev/null +++ b/src/common/components/error-boundary.tsx @@ -0,0 +1,33 @@ +import React, { Component } from "react"; + +interface State { + hasError: boolean; +} + +interface Props {} + +export default class ErrorBoundary extends React.Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: unknown) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error: unknown, errorInfo: unknown) { + // You can also log the error to an error reporting service + console.error(error, errorInfo); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return

Something went wrong.

; + } + + return this.props.children; + } +} diff --git a/src/common/components/feedback/index.tsx b/src/common/components/feedback/index.tsx index b338f391660..353cec67dc5 100644 --- a/src/common/components/feedback/index.tsx +++ b/src/common/components/feedback/index.tsx @@ -8,15 +8,35 @@ import { ErrorTypes } from "../../enums"; import { ActiveUser } from "../../store/active-user/types"; import { _t } from "../../i18n"; -export const error = (message: string, errorType = ErrorTypes.COMMON) => { - const detail: ErrorFeedbackObject = { - id: random(), - type: "error", - message, - errorType - }; - const ev = new CustomEvent("feedback", { detail }); - window.dispatchEvent(ev); +function isString(x: any): x is string { + return typeof x === "string"; +} + +function isComposedErrorPair( + value: string | [string, ErrorTypes.COMMON | ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS] +): value is [string, ErrorTypes.COMMON | ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS] { + return ( + (value.length === 2 && typeof value[0] === "string" && value[1] === ErrorTypes.COMMON) || + value[1] === ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS + ); +} + +export const error = ( + message: string | [string, ErrorTypes.COMMON | ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS], + errorType = ErrorTypes.COMMON +) => { + if (isString(message)) { + const detail: ErrorFeedbackObject = { + id: random(), + type: "error", + message, + errorType + }; + const ev = new CustomEvent("feedback", { detail }); + window.dispatchEvent(ev); + } else { + error(message[0], message[1]); + } }; export const success = (message: string) => { diff --git a/src/common/components/formatted-currency/index.tsx b/src/common/components/formatted-currency/index.tsx index 56bf51c5c49..966903686ab 100644 --- a/src/common/components/formatted-currency/index.tsx +++ b/src/common/components/formatted-currency/index.tsx @@ -10,6 +10,8 @@ interface Props { fixAt: number; } +const prefixes = ["", "m", "µ", "n"]; + export default class FormattedCurrency extends Component { public static defaultProps: Partial = { fixAt: 2 @@ -21,6 +23,36 @@ export default class FormattedCurrency extends Component { const valInCurrency = value * currencyRate; - return <>{formattedNumber(valInCurrency, { fractionDigits: fixAt, prefix: currencySymbol })}; + let multiplier = 1.0; + let prefix = ""; + if (currencySymbol === "฿" && valInCurrency > 0) { + let decimal_places = 0; + let currency_string: string; + for (; decimal_places < 9; decimal_places += 3, multiplier *= 1000) { + const truncated_amount = Math.trunc(valInCurrency * Math.pow(10, decimal_places + fixAt)); + if (isNaN(truncated_amount) || truncated_amount > 100) break; + } + if (decimal_places === 9) { + if (isNaN(multiplier * valInCurrency) || multiplier * valInCurrency === 0) { + decimal_places = 0; + multiplier = 1.0; + } else { + return formattedNumber(valInCurrency * Math.pow(10, 8), { + fractionDigits: 0, + suffix: "sats" + }); + } + } + prefix = prefixes[decimal_places / 3]; + } + + return ( + <> + {formattedNumber(valInCurrency * multiplier, { + fractionDigits: fixAt, + prefix: prefix + currencySymbol + })} + + ); } } diff --git a/src/common/components/key-or-hot-dialog/index.tsx b/src/common/components/key-or-hot-dialog/index.tsx index 21ba77d7cad..a646ec788fb 100644 --- a/src/common/components/key-or-hot-dialog/index.tsx +++ b/src/common/components/key-or-hot-dialog/index.tsx @@ -8,6 +8,7 @@ import { Global } from "../../store/global/types"; import { ActiveUser } from "../../store/active-user/types"; import KeyOrHot from "../key-or-hot"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/key-or-hot/index.tsx b/src/common/components/key-or-hot/index.tsx index 8485764a1e2..db7987919b1 100644 --- a/src/common/components/key-or-hot/index.tsx +++ b/src/common/components/key-or-hot/index.tsx @@ -13,6 +13,7 @@ import { error } from "../feedback"; import { _t } from "../../i18n"; import { keySvg } from "../../img/svg"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/login/__snapshots__/index.spec.tsx.snap b/src/common/components/login/__snapshots__/index.spec.tsx.snap index 8b2c4b12bb8..8d3726691ac 100644 --- a/src/common/components/login/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/login/__snapshots__/index.spec.tsx.snap @@ -48,11 +48,6 @@ Array [ value="" />
-
-
-

@@ -241,11 +236,6 @@ Array [ value="" />

-
-
-

@@ -437,11 +427,6 @@ Array [ value="" />

-
-
-

@@ -535,11 +520,6 @@ Array [ value="" />

-
-
-

diff --git a/src/common/components/login/index.tsx b/src/common/components/login/index.tsx index 9e296a94187..074310b0875 100644 --- a/src/common/components/login/index.tsx +++ b/src/common/components/login/index.tsx @@ -1,4 +1,4 @@ -import React, { Component } from "react"; +import React, { Component, MouseEvent } from "react"; import { Modal, Form, Button, FormControl, Spinner } from "react-bootstrap"; @@ -28,33 +28,38 @@ import { error } from "../feedback"; import { getAuthUrl, makeHsCode } from "../../helper/hive-signer"; import { hsLogin } from "../../../desktop/app/helper/hive-signer"; -import { getAccount } from "../../api/hive"; +import { client, getAccount } from "../../api/hive"; import { usrActivity } from "../../api/private-api"; import { hsTokenRenew } from "../../api/auth-api"; import { formatError, grantPostingPermission, revokePostingPermission } from "../../api/operations"; import { getRefreshToken } from "../../helper/user-token"; -import ReCAPTCHA from "react-google-recaptcha"; - import { addAccountAuthority, removeAccountAuthority, signBuffer } from "../../helper/keychain"; - +import { logUser } from "../../helper/log-user"; import { _t } from "../../i18n"; import _c from "../../util/fix-class-names"; import { deleteForeverSvg } from "../../img/svg"; +import { Html5QrcodeScanner, Html5QrcodeResult } from "html5-qrcode"; declare var window: AppWindow; interface LoginKcProps { + history: History; toggleUIProp: (what: ToggleType) => void; + setActiveUser: (username: string | null) => void; + deleteUser: (username: string) => void; doLogin: ( hsCode: string, postingKey: null | undefined | string, account: Account ) => Promise; global: Global; + userListRef?: any; + activeUser: ActiveUser | null; + users: User[]; } interface LoginKcState { @@ -84,6 +89,49 @@ export class LoginKc extends BaseComponent { toggleUIProp("login"); }; + userSelect = (user: User) => { + const { doLogin } = this.props; + + this.stateSet({ inProgress: true }); + + getAccount(user.username) + .then((account) => { + let token = getRefreshToken(user.username); + + if (!token) { + error(`${_t("login.error-user-not-found-cache")}`); + } + return token ? doLogin(token, user.postingKey, account) : this.userDelete(user); + }) + .then(() => { + this.hide(); + let shouldShowTutorialJourney = ls.get(`${user.username}HadTutorial`); + + if ( + !shouldShowTutorialJourney && + shouldShowTutorialJourney && + shouldShowTutorialJourney !== "true" + ) { + ls.set(`${user.username}HadTutorial`, "false"); + } + }) + .catch(() => { + error(_t("g.server-error")); + }) + .finally(() => { + this.stateSet({ inProgress: false }); + }); + }; + + userDelete = (user: User) => { + const { activeUser, deleteUser, setActiveUser } = this.props; + deleteUser(user.username); + + // logout if active user + if (activeUser && user.username === activeUser.username) { + setActiveUser(null); + } + }; login = async () => { const { hsClientId } = this.props.global; const { username } = this.state; @@ -135,7 +183,7 @@ export class LoginKc extends BaseComponent { try { code = await makeHsCode(hsClientId, username, signer); } catch (err) { - error(...formatError(err)); + error(formatError(err)); this.stateSet({ inProgress: false }); return; } @@ -154,6 +202,24 @@ export class LoginKc extends BaseComponent { }); }; + hsLogin = () => { + const { global, history } = this.props; + const { hsClientId } = global; + if (global.isElectron) { + hsLogin(hsClientId) + .then((r) => { + this.hide(); + history.push(`/auth?code=${r.code}`); + }) + .catch((e) => { + error(e); + }); + return; + } + + window.location.href = getAuthUrl(hsClientId); + }; + back = () => { const { toggleUIProp } = this.props; toggleUIProp("loginKc"); @@ -163,6 +229,13 @@ export class LoginKc extends BaseComponent { const { username, inProgress } = this.state; const { global } = this.props; + const { users, userListRef } = this.props; + const logo = global.isElectron ? "./img/logo-circle.svg" : require("../../img/logo-circle.svg"); + + const hsLogo = global.isElectron + ? "./img/hive-signer.svg" + : require("../../img/hive-signer.svg"); + const keyChainLogo = global.isElectron ? "./img/keychain.png" : require("../../img/keychain.png"); @@ -173,6 +246,37 @@ export class LoginKc extends BaseComponent { return ( <> + {users.length === 0 && ( +

+ Logo +

{_t("login.title")}

+
+ )} + + {users.length > 0 && ( + <> +
+
{_t("g.login-as")}
+
+ {users.map((u) => { + return ( + + ); + })} +
+
+ + + )} +
Logo

{_t("login.with-keychain")}

@@ -194,13 +298,36 @@ export class LoginKc extends BaseComponent { onKeyDown={this.inputKeyDown} /> - - + +
+ ); @@ -282,7 +409,8 @@ interface State { username: string; key: string; inProgress: boolean; - isVerified: boolean; + quickReliablePreviewVisible: boolean; + quickReliablePreviewSetup: boolean; } export class Login extends BaseComponent { @@ -290,9 +418,12 @@ export class Login extends BaseComponent { username: "", key: "", inProgress: false, - isVerified: this.props.global.isElectron ? true : false + quickReliablePreviewVisible: false, + quickReliablePreviewSetup: false }; + html5QrcodeScanner: undefined | Html5QrcodeScanner = undefined; + shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean { return ( !isEqual(this.props.users, nextProps.users) || @@ -303,6 +434,7 @@ export class Login extends BaseComponent { hide = () => { const { toggleUIProp } = this.props; + this.stateSet({ quickReliablePreviewVisible: false }); toggleUIProp("login"); }; @@ -388,24 +520,14 @@ export class Login extends BaseComponent { toggleUIProp("loginKc"); }; - captchaCheck = (value: string | null) => { - if (value) { - this.setState({ isVerified: true }); - } - }; - login = async () => { const { hsClientId } = this.props.global; - const { username, key } = this.state; + const { username, key, quickReliablePreviewVisible } = this.state; if (username === "" || key === "") { error(_t("login.error-fields-required")); return; } - if (!this.state.isVerified) { - error(_t("login.captcha-check-required")); - return; - } // Warn if the code is a public key try { PublicKey.fromString(key); @@ -497,25 +619,7 @@ export class Login extends BaseComponent { const { doLogin } = this.props; doLogin(code, withPostingKey ? key : null, account) - .then(() => { - if ( - !ls.get(`${username}HadTutorial`) || - (ls.get(`${username}HadTutorial`) && ls.get(`${username}HadTutorial`) !== "true") - ) { - ls.set(`${username}HadTutorial`, "false"); - } - - let shouldShowTutorialJourney = ls.get(`${username}HadTutorial`); - - if ( - !shouldShowTutorialJourney && - shouldShowTutorialJourney && - shouldShowTutorialJourney === "false" - ) { - ls.set(`${username}HadTutorial`, "false"); - } - this.hide(); - }) + .then(this.handleDoLogin.bind(this, username)) .catch(() => { error(_t("g.server-error")); }) @@ -524,9 +628,176 @@ export class Login extends BaseComponent { }); }; + handleDoLogin = (username: string) => { + if ( + !ls.get(`${username}HadTutorial`) || + (ls.get(`${username}HadTutorial`) && ls.get(`${username}HadTutorial`) !== "true") + ) { + ls.set(`${username}HadTutorial`, "false"); + } + + let shouldShowTutorialJourney = ls.get(`${username}HadTutorial`); + + if ( + !shouldShowTutorialJourney && + shouldShowTutorialJourney && + shouldShowTutorialJourney === "false" + ) { + ls.set(`${username}HadTutorial`, "false"); + } + + if (this.html5QrcodeScanner) { + this.html5QrcodeScanner.clear(); + } + + this.hide(); + }; + + hideQRPreview = (e: MouseEvent): void => { + if (this.state.quickReliablePreviewSetup === false) return; + const scanner = this.html5QrcodeScanner; + if (this.state.quickReliablePreviewVisible === true && scanner) { + this.setState((old_state: State) => { + if (!old_state.quickReliablePreviewVisible) return old_state; + try { + scanner.pause(); + } catch (emessage: string) { + console.error(emessage); + } finally { + return { ...old_state, quickReliablePreviewVisible: false }; + } + }); + } // if + }; + + showQRPreview = (e: MouseEvent): void => { + const { hsClientId } = this.props.global; + const { quickReliablePreviewSetup, quickReliablePreviewVisible } = this.state; + if (quickReliablePreviewVisible) { + return; + } else if (quickReliablePreviewSetup) { + const scanner = this.html5QrcodeScanner; + if (scanner) { + this.stateSet((old_state: State) => { + if (old_state.quickReliablePreviewVisible) return old_state; + try { + scanner.resume(); + } catch (emessage: string) { + console.error(emessage); + } finally { + return { ...old_state, quickReliablePreviewVisible: true }; + } + }); + } + } else { + this.stateSet({ quickReliablePreviewVisible: true, quickReliablePreviewSetup: true }); + + if (!navigator.mediaDevices.getUserMedia) { + console.log("media devices not supported"); + return; + } + + const onScanSuccess = (decodedText: string, decodedResult: Html5QrcodeResult) => { + // handle the scanned code as you like, for example: + const privateKeyString = decodedText.trim(); + const handleFailure = () => + this.setState({ key: privateKeyString, quickReliablePreviewVisible: false }); + // setting the username leaves the login button disabled (why?). + try { + const privateKeyObject = PrivateKey.fromString(privateKeyString); + const publicKeyObject = privateKeyObject.createPublic(); + const publicKeyString = publicKeyObject.toString(); + console.log("line 671:", { publicKeyString }); + // still in development: //getKeyReferences({"keys": [publicKey]}) + client + .call("account_by_key_api", "get_key_references", { keys: [publicKeyString] }) + .then(async (result: { accounts: string[][] }) => { + console.log("line 675:", result); + if (result.accounts.length == 0) { + handleFailure(); + error("Invalid return value from server."); + } else { + const usernamesForFirstKey: string[] = result.accounts[0]; + if (usernamesForFirstKey.length === 0) { + handleFailure(); + error("No user for this key"); + } else if (usernamesForFirstKey.length > 1) { + handleFailure(); + error("More than one user for this key"); + } else { + const firstUsername: string = usernamesForFirstKey[0]; + + let account: Account; + + this.stateSet({ + key: privateKeyString, + quickReliablePreviewVisible: false, + inProgress: true, + username: firstUsername + }); + + try { + console.log({ username: firstUsername }); + account = await getAccount(firstUsername); + } catch (err) { + this.stateSet({ inProgress: false }); + error(_t("login.error-user-fetch")); + return; + } + + // Prepare hivesigner code + const signer = (message: string): Promise => { + const hash = cryptoUtils.sha256(message); + return new Promise((resolve) => + resolve(privateKeyObject.sign(hash).toString()) + ); + }; + const code = await makeHsCode(hsClientId, firstUsername, signer); + + const { doLogin } = this.props; + + doLogin(code, privateKeyString, account) + .then(this.handleDoLogin.bind(this, firstUsername)) + .catch(() => { + error(_t("g.server-error")); + }) + .finally(() => { + this.stateSet({ inProgress: false }); + }); + } + } + }) + .catch((e) => { + console.log(685, e); + handleFailure(); + }); + } catch (e) { + console.log("689", e); + if (typeof e.message == "string") { + error(e.message); + } + } + }; + + function onScanFailure(error: string) { + // handle scan failure, usually better to ignore and keep scanning. + // for example: + // doing nothing + } + + this.html5QrcodeScanner = new Html5QrcodeScanner( + "reader", + { fps: 10, qrbox: { width: 250, height: 250 } }, + /* verbose= */ false + ); + this.html5QrcodeScanner.render(onScanSuccess, onScanFailure); + } + }; + render() { - const { username, key, inProgress, isVerified } = this.state; + const { username, key, inProgress, quickReliablePreviewVisible } = this.state; const { users, activeUser, global, userListRef } = this.props; + const isEcency = false; const logo = global.isElectron ? "./img/logo-circle.svg" : require("../../img/logo-circle.svg"); const hsLogo = global.isElectron ? "./img/hive-signer.svg" @@ -590,24 +861,25 @@ export class Login extends BaseComponent { /> - - - {!global.isElectron && ( -
- + + {navigator.mediaDevices.getUserMedia && !quickReliablePreviewVisible && ( + + )}
- )} +
+
+ +
+

{_t("login.login-info-1")}{" "} { {_t("login.login-info-2")}

- @@ -703,9 +979,6 @@ export default class LoginDialog extends Component { componentWillUnmount() { const { toggleUIProp, ui } = this.props; - if (ui.loginKc) { - toggleUIProp("loginKc"); - } } doLogin = async (hsCode: string, postingKey: null | undefined | string, account: Account) => { @@ -735,6 +1008,10 @@ export default class LoginDialog extends Component { usrActivity(user.username, 20); } + if (user.username) { + logUser(user.username, global); + } + // redirection based on path name const { location, history } = this.props; if (location.pathname.startsWith("/signup")) { @@ -745,7 +1022,7 @@ export default class LoginDialog extends Component { }; render() { - const { ui } = this.props; + const { ui, users } = this.props; return ( { {!ui.loginKc && ( )} - {ui.loginKc && } + {ui.loginKc && ( + + )} ); diff --git a/src/common/components/preferences/__snapshots__/index.spec.tsx.snap b/src/common/components/preferences/__snapshots__/index.spec.tsx.snap index 4c7f42ec408..47596e5955a 100644 --- a/src/common/components/preferences/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/preferences/__snapshots__/index.spec.tsx.snap @@ -396,6 +396,161 @@ exports[`(1) Default render 1`] = `
+
+ Entry Item Options + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +