diff --git a/src/common/api/hive.ts b/src/common/api/hive.ts index 0a1757aa371..b589dce292a 100644 --- a/src/common/api/hive.ts +++ b/src/common/api/hive.ts @@ -485,3 +485,24 @@ export const getBlogEntries = (username: string, limit: number = dataLimit): Pro 0, limit ]); + + export const getRcOperationStats = (): Promise => client.call("rc_api", "get_rc_stats", {}); + + 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; + }; \ No newline at end of file diff --git a/src/common/api/mutations.ts b/src/common/api/mutations.ts new file mode 100644 index 00000000000..5733eb948a1 --- /dev/null +++ b/src/common/api/mutations.ts @@ -0,0 +1,39 @@ +import { useMutation } from "@tanstack/react-query"; +import { usrActivity } from "./private-api"; +import { claimAccount, claimAccountByKeychain } from "./operations"; +import { FullAccount } from "../store/accounts/types"; +import { PrivateKey } from "@hiveio/dhive"; + +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); + } + }); +} + +export function useAccountClaiming(account: FullAccount) { + return useMutation( + ["account-claiming", account.name], + async ({ isKeychain, key }: { key?: PrivateKey; isKeychain?: boolean }) => { + try { + if (isKeychain) { + return await claimAccountByKeychain(account); + } + + if (key) { + return await claimAccount(account, key); + } + + throw new Error(); + } catch (error) { + throw new Error("Failed RC claiming. Please, try again or contact with support."); + } + } + ); +} diff --git a/src/common/api/operations.ts b/src/common/api/operations.ts index cff747727dc..0b5433fe02e 100644 --- a/src/common/api/operations.ts +++ b/src/common/api/operations.ts @@ -2,11 +2,11 @@ import hs from "hivesigner"; import {PrivateKey, Operation, OperationName, TransactionConfirmation, AccountUpdateOperation, CustomJsonOperation} from '@hiveio/dhive'; -import {Parameters} from 'hive-uri'; +import {encodeOp, Parameters} from 'hive-uri'; import {client as hiveClient} from "./hive"; -import {Account} from "../store/accounts/types"; +import {Account, FullAccount} from "../store/accounts/types"; import {usrActivity} from "./private-api"; @@ -1211,4 +1211,81 @@ export const createAccountWithCredit = async (data: any, creator_account: string return err; } }; - \ No newline at end of file + + 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 + } + ]; + + return broadcastPostingJSON(delegator, "rc", json); + }; + + export const claimAccountByHiveSigner = (account: FullAccount) => + hotSign( + encodeOp( + [ + "claim_account", + { + fee: "0.000 HIVE", + creator: account.name, + extensions: [] + } + ], + {} + ).replace("hive://sign/", ""), + { + authority: "active", + required_auths: `["${account.name}"]`, + required_posting_auths: "[]" + }, + `@${account.name}/wallet` + ); + + export const claimAccount = async (account: FullAccount, key: PrivateKey) => { + if (!key) { + throw new Error("[Account claiming] Active/owner key is not provided"); + } + + return hiveClient.broadcast.sendOperations( + [ + [ + "claim_account", + { + fee: { + amount: "0", + precision: 3, + nai: "@@000000021" + }, + creator: account.name, + extensions: [] + } + ] + ], + key + ); + }; + + export const claimAccountByKeychain = (account: FullAccount) => + keychain.broadcast( + account.name, + [ + [ + "claim_account", + { + creator: account.name, + extensions: [], + fee: "0.000 HIVE" + } + ] + ], + "Active" + ); \ No newline at end of file diff --git a/src/common/components/profile-card/_index.scss b/src/common/components/profile-card/_index.scss index a52906c0c99..3dc9bbb4953 100644 --- a/src/common/components/profile-card/_index.scss +++ b/src/common/components/profile-card/_index.scss @@ -63,11 +63,13 @@ } .vpower-line { - background: $duck-egg-blue; - height: 2px; + // background: $duck-egg-blue; + // height: 15px; width: 300px; + // border-radius: 20px; margin: 0 auto 8px auto; - + cursor: pointer; + @include themify(dusk) { backgorund: $dusk-background; } @@ -77,8 +79,10 @@ } .vpower-line-inner { - background: $dark-sky-blue; - height: 2px; + // background: $dark-sky-blue; + // height: 100%; + width: 100%; + border-radius: 20px; @include themify(dusk) { backgorund: $dusk-primary; diff --git a/src/common/components/profile-card/index.tsx b/src/common/components/profile-card/index.tsx index 8ba488451d7..5d7fa48dba3 100644 --- a/src/common/components/profile-card/index.tsx +++ b/src/common/components/profile-card/index.tsx @@ -5,6 +5,7 @@ import {History} from "history"; import {Link} from "react-router-dom"; import moment from "moment"; +import { RCAccount } from "@hiveio/dhive/lib/chain/rc"; import {Global} from "../../store/global/types"; import {Account, FullAccount} from "../../store/accounts/types"; @@ -20,7 +21,7 @@ import formattedNumber from "../../util/formatted-number"; import defaults from "../../constants/defaults.json"; -import {votingPower} from "../../api/hive"; +import {votingPower, findRcAccounts, rcPower} from "../../api/hive"; import {_t} from "../../i18n"; @@ -37,6 +38,7 @@ import { import { EditPic } from '../community-card'; import { getRelationshipBetweenAccounts } from "../../api/bridge"; import { Skeleton } from "../skeleton"; +import { ResourceCreditsInfo } from "../resource-credits"; interface Props { global: Global; @@ -61,6 +63,8 @@ export const ProfileCard = (props: Props) => { const [followsActiveUser, setFollowsActiveUser] = useState(false); const [isMounted, setIsmounted] = useState(false); const [followsActiveUserLoading, setFollowsActiveUserLoading] = useState(false); + const [rcPercent, setRcPercent] = useState(100); + const [, updateState] = useState(); const forceUpdate = useCallback(() => updateState({} as any), []); @@ -71,6 +75,16 @@ export const ProfileCard = (props: Props) => { setFollowsActiveUserLoading(activeUser && activeUser.username ? true : false); getFollowsInfo(account.name); } + + findRcAccounts(account?.name) + .then((r: RCAccount[]) => { + if (r && r[0]) { + setRcPercent(rcPower(r[0])); + } + }) + .catch((e) => { + setRcPercent(100); + }); }, [account]); @@ -145,16 +159,15 @@ export const ProfileCard = (props: Props) => {
{account.name}
-
-
+
+
-
- - {vPower.toFixed(2)} - -
- {loggedIn && !isMyProfile &&
{followsActiveUserLoading ? : followsActiveUser ?
{_t("profile.follows-you")}
: null}
} + {loggedIn && !isMyProfile && +
+ {followsActiveUserLoading ? : followsActiveUser ? +
{_t("profile.follows-you")}
: null} +
} {(account.profile?.name || account.profile?.about) && (
@@ -165,7 +178,6 @@ export const ProfileCard = (props: Props) => { {account.__loaded && (
- {account.follow_stats?.follower_count !== undefined && (
diff --git a/src/common/components/resource-credits/_index.scss b/src/common/components/resource-credits/_index.scss new file mode 100644 index 00000000000..2f355fdb456 --- /dev/null +++ b/src/common/components/resource-credits/_index.scss @@ -0,0 +1,197 @@ +// @import "src/style/vars_mixins"; + +.rc-infocontainer { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + +// @include media-breakpoint-down(md) { +// grid-template-columns: 1fr; +// } + + .percent { + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; + gap: 15px; + + @media (max-width: 991px) { + flex-direction: column; + width: 300px; + } + + @media (max-width: 500px) { + flex-direction: column; + } + + .circle { + width: 160px; + height: 160px; + position: relative; + + .outer-circle { + width: 160px; + height: 160px; + background-color: transparent; + border-radius: 50%; + padding: 20px; + + .inner-circle { + width: 120px; + height: 120px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + + span { + font-size: 16px; + font-weight: 500px; + } + } + } + + svg { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + } + } + + .percentage-info { + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 10px; + + @media (max-width: 991px) { + flex-direction: column; + } + + .unused { + display: flex; + align-items: center; + flex-direction: row; + gap: 10px; + + .unused-box { + width: 15px; + height: 15px; + background-color: #357ce6; + } + + } + + .used { + display: flex; + align-items: center; + flex-direction: row; + gap: 10px; + + .used-box { + width: 15px; + height: 15px; + background-color: #F0706A; + } + + } + + } + + } + + .rc-details { + display: flex; + flex-direction: column; + + .delegations { + display: flex; + flex-direction: column; + gap: 10px; + color: rgb(80, 134, 211); + font-weight: 600px; + text-decoration: underline; + + .outgoing { + cursor: pointer; + font-size: 16px; + } + + .incoming { + cursor: pointer; + font-size: 16px; + } + + } + + .line-break { + + hr { + background-color: gray; + } + } + + .extra-details { + p { + font-size: 16px; + } + + ul { + line-height: 30px; + + li { + font-size: 13px; + } + } + + .rc-info-extras { + .ececny-list-item { + font-size: 0.875rem; + + > span { + // color: $primary; + margin-left: 0.5rem; + } + } + } + } + } + + svg { + width: 160px; + height: 160px; + + circle { + fill: none; + stroke-width: 20px; + // stroke-linecap: round; + } + } + +} + +.rc-header { + display: flex; + flex-direction: column; + gap: 10px; + + .about-rc { + gap: 10px; + + span { + font-size: 16px; + } + + a { + font-size: 16px; + } + } +} + +.progress-bar.used { + background-color: #F0706A; +} \ No newline at end of file diff --git a/src/common/components/resource-credits/circle.tsx b/src/common/components/resource-credits/circle.tsx new file mode 100644 index 00000000000..17450c88911 --- /dev/null +++ b/src/common/components/resource-credits/circle.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +const RcProgressCircle = (props: any) => { + const { radius, dasharray, usedOffset, unUsedOffset } = props; + return ( + + + + + + ); +}; + +export default RcProgressCircle; diff --git a/src/common/components/resource-credits/claim-credit.tsx b/src/common/components/resource-credits/claim-credit.tsx new file mode 100644 index 00000000000..c77fb18bf08 --- /dev/null +++ b/src/common/components/resource-credits/claim-credit.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from "react"; +import { arrowLeftSvg } from "../../img/svg"; +import "./claim.scss"; +import { FullAccount } from "../../store/accounts/types"; +import { useAccountClaiming } from "../../api/mutations"; +import { _t } from "../../i18n"; +import KeyOrHot from "../key-or-hot"; +import { useMappedStore } from "../../store/use-mapped-store"; +import { claimAccountByHiveSigner } from "../../api/operations"; +import { Button } from "react-bootstrap"; + +interface Props { + account: FullAccount; + claimAccountAmount: number; +} + +const ClaimAccountCredit = ({ account, claimAccountAmount }: Props) => { + const { global, activeUser, addAccount } = useMappedStore(); + const { + mutateAsync: claimAccount, + isLoading, + error, + isSuccess, + reset + } = useAccountClaiming(account); + + const [key, setKey] = useState(""); + const [isKeySetting, setIsKeySetting] = useState(false); + + useEffect(() => { + if (isSuccess) { + addAccount({ + ...account, + pending_claimed_accounts: 0 + }); + } + }, [isSuccess]); + + return ( +
+
+ {isKeySetting ? ( +
{ + setIsKeySetting(false); + reset(); + }} + > + {arrowLeftSvg} +
+ ) : ( + <> + )} + {_t("rc-info.claim-accounts")} + {claimAccountAmount} +
+ {!isSuccess && + !isKeySetting && + claimAccountAmount > 0 && + activeUser?.username === account.name ? ( + + ) : ( + <> + )} + {isSuccess ? ( + {_t("rc-info.successfully-claimed")} + ) : ( + <> + )} + {error ? ( + {(error as Error)?.message} + ) : ( + <> + )} + {isKeySetting && !isSuccess ? ( + setKey(k)} + global={global} + activeUser={activeUser!} + onKey={(key) => claimAccount({ key })} + onHot={() => claimAccountByHiveSigner(account)} + onKc={() => + claimAccount({ + isKeychain: true + }) + } + /> + ) : ( + <> + )} +
+ ); +}; + +export default ClaimAccountCredit; diff --git a/src/common/components/resource-credits/claim.scss b/src/common/components/resource-credits/claim.scss new file mode 100644 index 00000000000..eeb19c5192c --- /dev/null +++ b/src/common/components/resource-credits/claim.scss @@ -0,0 +1,33 @@ +.claim-credit { + &-title { + display: flex; + align-items: center; + gap: 0.5rem; + } + + &-back { + cursor: pointer; + + svg { + width: 1rem; + height: 1rem; + } + } + + // @include themify(night) { + // color: $gray-400; + // } + // } + + .key-or-hot { + padding: 0; + margin: 1rem 0 0; + + .input-group { + svg { + width: 1rem; + height: 1rem; + } + } + } + } \ No newline at end of file diff --git a/src/common/components/resource-credits/delegation.scss b/src/common/components/resource-credits/delegation.scss new file mode 100644 index 00000000000..d31a841a15c --- /dev/null +++ b/src/common/components/resource-credits/delegation.scss @@ -0,0 +1,32 @@ +.rc-progress-line { + cursor: pointer; + justify-self: center; + display: flex; + flex-direction: row; + width: 100%; + justify-content: space-between; + align-items: center; + height: 3px; + background-color: #ccc; + margin-bottom: 3px; + position: relative; + } + + .rc-fill { + height: 100%; + background-color: #357ce6; + position: absolute; + top: 0; + left: 0; + transition: width 0.5s ease; + } + + .rc-points { + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #357ce6; + cursor: pointer; + z-index: 999; + } + \ No newline at end of file diff --git a/src/common/components/resource-credits/delegation.tsx b/src/common/components/resource-credits/delegation.tsx new file mode 100644 index 00000000000..67adf41650b --- /dev/null +++ b/src/common/components/resource-credits/delegation.tsx @@ -0,0 +1,334 @@ +import React, { useCallback, useState } from "react"; +import _ from "lodash"; +import badActors from "@hiveio/hivescript/bad-actors.json"; +import LinearProgress from "../linear-progress"; +import UserAvatar from "../user-avatar"; +import { error } from "../feedback"; +import "./delegation.scss"; +import { delegateRC, formatError } from "../../api/operations"; +import { getAccount } from "../../api/hive"; +import { arrowRightSvg } from "../../img/svg"; +import { _t } from "../../i18n"; +import { Button, Form, FormControl, InputGroup } from "react-bootstrap"; + +export const ResourceCreditsDelegation = (props: any) => { + const { resourceCredit, activeUser, hideDelegation, toFromList, amountFromList, delegateeData } = + props; + + const [to, setTo] = useState(toFromList || ""); + const [amount, setAmount] = useState(amountFromList || ""); + const [inProgress, setInProgress] = useState(false); + const [step, setStep] = useState(1); + const [amountError, setAmountError] = useState(""); + const [toError, setToError] = useState(""); + const [toWarning, setToWarning] = useState(""); + const [toData, setToData] = useState(delegateeData || ""); + const [convertedValue, setConvertedValue] = useState(null); + + const pointValues = [0, 25, 50, 75, 100]; + + const [fillWidth, setFillWidth] = useState(0); + + const [convertedVal, setConvertedVal] = useState(null); + + const convertToBillions = (input: string) => { + const inputNumber = parseFloat(input); + let resultDivided; + + if (!isNaN(inputNumber)) { + resultDivided = inputNumber / 1e9; + console.log("resultDivided", resultDivided!.toFixed(2) + "B") + } else { + setConvertedValue("null"); + } + return resultDivided!?.toFixed(2) + "B"; + }; + + const toChanged = (e: React.ChangeEvent) => { + const { value } = e.target; + setInProgress(true); + setTo(value); + delayedSearch(value); + }; + + const amountChanged = (e: React.ChangeEvent): void => { + const value = e.target.value; + console.log(Number(value)) + + const sonvertedAmount = (parseFloat(value) * 1e9).toFixed(2) + setConvertedVal(sonvertedAmount) + + setAmount(Number(value)); + console.log(amount) + if ( + sonvertedAmount === "" || + (Number(sonvertedAmount) >= 5000000000 && Number(sonvertedAmount) < Number(resourceCredit)) || + sonvertedAmount === "0" + ) { + setAmountError(""); + } else if (Number(sonvertedAmount) < 5000000000) { + setAmountError(_t("rc-info.minimum-rc-error")); + } else if (Number(sonvertedAmount) > Number(resourceCredit)) { + setAmountError(_t("rc-info.insufficient-rc-error")); + return; + } + }; + + const next = () => { + setInProgress(false); + setStep(2); + }; + + const back = () => { + setStep(1); + }; + + const signTransaction = () => { + const { activeUser } = props; + console.log(activeUser) + const username = activeUser?.username!; + const max_rc = `${parseFloat(amount) * 1e9}` ; + delegateRC(username, to, max_rc) + .then((res: any) => { + return res; + }) + .catch((e: any) => { + console.log({ e }); + return e; + }); + hideDelegation(); + return; + }; + + const canSubmit = + (toData || delegateeData) && + !toError && + !inProgress && + !amountError && + !!convertedVal && + (Number(convertedVal) === 0 || Number(convertedVal) >= 5000000000) && + Number(amount) < Number(resourceCredit); + + const handleTo = async (value: string) => { + setInProgress(true); + + if (value === "") { + setToWarning(""); + setToError(""); + setToData(null); + return; + } + if (badActors.includes(value)) { + setToWarning(_t("transfer.to-bad-actor")); + } else { + setToWarning(""); + } + setToData(null); + if (value.includes(",")) { + setToData(value); + setToError(""); + setInProgress(false); + return true; + } else { + return getAccount(value) + .then((resp) => { + if (resp) { + setToError(""); + setToData(resp); + } else { + setToError(_t("transfer.to-not-found")); + } + return resp; + }) + .catch((err: any) => { + // error(...formatError(err)); + console.log(err) + }) + .finally(() => { + setInProgress(false); + }); + } + }; + + const delayedSearch = useCallback(_.debounce(handleTo, 3000, { leading: true }), []); + + const formHeader1 = ( +
+
1
+
+
{_t("rc-info.delegate-title")}
+
{_t("rc-info.delegate-sub-title")}
+
+
+ ); + + const formHeader2 = ( +
+
2
+
+
{_t("transfer.confirm-title")}
+
{_t("transfer.confirm-sub-title")}
+
+
+ ); + + const handlePointClick = (index: number) => { + const fillPercentage = (index) * (100 / 4); + setFillWidth(fillPercentage); + const formartInput = (((fillPercentage / 100) * resourceCredit) / 1e9).toFixed(0) + console.log(fillPercentage) + setAmount(formartInput) + }; + + const handleProgressLineClick = (event: any) => { + const clickedPosition = (event.nativeEvent.offsetX / event.currentTarget.clientWidth) * 100; + console.log(clickedPosition) + + const formmattedInput = ((clickedPosition / 100) * resourceCredit / 1e9).toFixed(0); + setFillWidth(clickedPosition); + setAmount(formmattedInput); + }; + + return ( +
+ {step === 1 && ( +
+ {formHeader1} + {inProgress && } +
+
+
+ +
+
+ + + +
+
+ +
+
+ +
+
+ {/* */} + + + + {/* */} + + {toWarning && ( + {toWarning} + )} + {toError && {toError}} +
+
+ +
+
+ +
+
+ + Number(resourceCredit) && amountError ? "is-invalid" : "" + } + /> + +
+
+ + {amount < 5000000000 && {amountError}} + {amount > Number(resourceCredit) && ( + {amountError} + )} + +
+
+ +
handlePointClick(0)} + >
+
handlePointClick(1)} + >
+
handlePointClick(2)} + >
+
handlePointClick(3)} + >
+
handlePointClick(4)} + >
+
+ +
+
+
+ {_t("transfer.balance")} + {`: ${convertToBillions(resourceCredit)}`} +
+
+
+ +
+
+ +
+
+
+
+ )} + + {step === 2 && ( +
+ {formHeader2} +
+
+
Delegate
+
+
+ +
+ { + <> +
{arrowRightSvg}
+
+ +
+ + } +
+
{amount} RC
+
+
+ + + +
+
+
+ )} +
+ ); +}; diff --git a/src/common/components/resource-credits/index.tsx b/src/common/components/resource-credits/index.tsx new file mode 100644 index 00000000000..64fd2c5d396 --- /dev/null +++ b/src/common/components/resource-credits/index.tsx @@ -0,0 +1,333 @@ +import React, { useState } from "react"; +import { _t } from "../../i18n"; +import { findRcAccounts, getRcOperationStats } from "../../api/hive"; +import { ResourceCreditsDelegation } from "./delegation"; +import { ConfirmDelete, RcDelegationsList } from "./list"; +import { rcFormatter } from "../../util/formatted-number"; +import RcProgressCircle from "./circle"; +import "./_index.scss"; +// import { Modal, ModalBody, ModalHeader, ModalTitle } from "@ui/modal"; +// import { Button } from "@ui/button"; +import { Button, Modal, ModalBody, ModalTitle } from "react-bootstrap"; +import ClaimAccountCredit from "./claim-credit"; +// import { List, ListItem } from "@ui/list"; +// import ClaimAccountCredit from "./claim-credit"; + +export const ResourceCreditsInfo = (props: any) => { + const { rcPercent, account, activeUser } = props; + + const radius = 70; + const dasharray = 440; + const unUsedOffset = (rcPercent / 100) * dasharray; + const usedOffset = ((100 - rcPercent) / 100) * dasharray; + + const [showRcInfo, setShowRcInfo] = useState(false); + const [delegated, setDelegated] = useState(); + const [receivedDelegation, setReceivedDelegation] = useState(); + const [resourceCredit, setResourceCredit] = useState(); + const [showDelegationModal, setShowDelegationModal] = useState(false); + const [showDelegationsList, setShowDelegationsList] = useState(false); + const [listMode, setListMode] = useState(""); + const [toFromList, setToFromList] = useState(""); + const [amountFromList, setAmountFromList]: any = useState(""); + const [showConfirmDelete, setShowConfirmDelete] = useState(false); + const [delegateeData, setDelegateeData] = useState(""); + const [commentAmount, setCommentAmount] = useState(0); + const [voteAmount, setVoteAmount] = useState(0); + const [transferAmount, setTransferAmount] = useState(0); + const [customJsonAmount, setCustomJsonAmount] = useState(0); + const [claimAccountAmount, setClaimAccountAmount] = useState(0); + + const showModal = () => { + fetchRCData(); + setShowRcInfo(true); + }; + + const hideModal = () => { + setShowRcInfo(false); + }; + + const showDelegation = () => { + setShowDelegationModal(true); + setShowRcInfo(false); + }; + + const hideDelegation = () => { + setShowDelegationModal(false); + setToFromList(""); + setAmountFromList(""); + }; + + const showIncomingList = () => { + setShowDelegationsList(true); + setListMode("in"); + }; + + const showOutGoingList = () => { + setShowDelegationsList(true); + setListMode("out"); + }; + + const hideList = () => { + setShowDelegationsList(false); + }; + + const confirmDelete = () => { + setShowConfirmDelete(true); + setShowDelegationsList(false); + }; + + const hideConfirmDelete = () => { + setShowConfirmDelete(false); + }; + + const fetchRCData = () => { + findRcAccounts(account?.name) + .then((r) => { + const outGoing = r.map((a: any) => a.delegated_rc); + const delegated = outGoing[0]; + const formatOutGoing: any = rcFormatter(delegated); + setDelegated(formatOutGoing); + const availableResourceCredit: any = r.map((a: any) => Number(a.rc_manabar.current_mana)); + const inComing: any = r.map((a: any) => Number(a.received_delegated_rc)); + const formatIncoming = rcFormatter(inComing); + const totalRc = Number(availableResourceCredit) + Number(inComing); + setReceivedDelegation(formatIncoming); + setResourceCredit(totalRc); + + const rcOperationsCost = async () => { + 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 customJsonOperationsCosts = operationCosts.custom_json_operation.avg_cost; + const createClaimAccountCost = Number(operationCosts.claim_account_operation.avg_cost); + + const commentCount: number = Math.ceil(Number(availableResourceCredit) / commentCost); + const votetCount: number = Math.ceil(Number(availableResourceCredit) / voteCost); + const transferCount: number = Math.ceil(Number(availableResourceCredit) / transferCost); + const customJsonCount: number = Math.ceil( + Number(availableResourceCredit) / customJsonOperationsCosts + ); + const createClaimAccountCount: number = Math.floor( + Number(availableResourceCredit) / createClaimAccountCost + ); + setCommentAmount(commentCount); + setVoteAmount(votetCount); + setTransferAmount(transferCount); + setCustomJsonAmount(customJsonCount); + setClaimAccountAmount(createClaimAccountCount); + }; + rcOperationsCost(); + }) + .catch(console.log); + }; + + return ( +
+
+
+
+ {_t("rc-info.available")} +
+
+ {/* Utilized */} + {_t("rc-info.used")} +
+
+
+ {_t("rc-info.resource-credits")} +
+
+ + + + +
+ {_t("rc-info.resource-credits")} + {/*
+ {`${_t("rc-info.check-faq")} `} + {_t("rc-info.faq-link")} +
*/} +
+
+
+ +
+
+
+
+
+ {`${rcPercent}%`} +
+
+ +
+ +
+
+
+ {`${_t("rc-info.rc-available")}: ${rcFormatter( + (rcPercent / 100) * resourceCredit + )}`} +
+
+
+ + {`Utilized: ${rcFormatter( + ((100 - rcPercent) / 100) * resourceCredit + )}`} + +
+
+
+ +
+
+ + {`${_t("rc-info.delegated")}: ${delegated}`} + + + {`${_t("rc-info.received-delegations")}: ${receivedDelegation}`} + +
+ +
+
+
+ +
+

{_t("rc-info.extra-details-heading")}

+
+ + {_t("rc-info.comments-posts")} + {commentAmount} + + + {_t("rc-info.votes")} + {voteAmount} + + + {_t("rc-info.transfers")} + {transferAmount} + + + {_t("rc-info.reblogs-follows")} + {customJsonAmount} + + + + +
+
+
+
+ +
+ {activeUser && ( + + )} +
+ + + + + + + {listMode === "in" ? _t("rc-info.incoming") : _t("rc-info.outgoing")} + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+
+ ); +}; diff --git a/src/common/components/resource-credits/list.scss b/src/common/components/resource-credits/list.scss new file mode 100644 index 00000000000..e1b38982496 --- /dev/null +++ b/src/common/components/resource-credits/list.scss @@ -0,0 +1,67 @@ +.delgations-list { + // @include user-grid-list(1, 2, 2); + min-height: 361px; + display: flex; + flex-direction: column; + justify-content: space-between; + + .delegation-loading { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 1px; + } + .list-container { + display: flex; + flex-direction: column; + overflow-y: scroll; + height: 80vh; + + .search-box { + max-width: 300px; + align-self: center; + } + // @include user-grid-list(1, 2, 3); + } + .item-main{ + width: max-content; + .actionable{ + gap: 10px; + .actions{ + display: flex; + gap: 10px; + } + span{ + height: 20px; + } + } + } + .item-extra { + display: flex; + justify-content: space-between; + } + .load-more-btn { + padding: 20px; + text-align: center; + } + } + + .parent-wrap{ + display: flex; + flex-wrap: wrap; + gap: 10px; + } + + + .wrap-lists{ + width: max-content; + padding: 10px; + background-color: #212529; + border-radius: 10px; + + &:hover{ + background-color: none; + background: none; + } + } \ No newline at end of file diff --git a/src/common/components/resource-credits/list.tsx b/src/common/components/resource-credits/list.tsx new file mode 100644 index 00000000000..e426b5e2bb6 --- /dev/null +++ b/src/common/components/resource-credits/list.tsx @@ -0,0 +1,261 @@ +import React, { useEffect, useState } from "react"; +import { getAccount, getIncomingRc, getOutgoingRc } from "../../api/hive"; +import { delegateRC } from "../../api/operations"; +import { _t } from "../../i18n"; +import LinearProgress from "../linear-progress"; +import ProfileLink from "../profile-link"; +import UserAvatar from "../user-avatar"; +import { useParams } from "react-router"; +import { Account } from "../../store/accounts/types"; +import Tooltip from "../tooltip"; +import "./list.scss"; +import { Global } from "../../store/global/types"; +import { Button, FormControl } from "react-bootstrap"; + +interface Props { + addAccount: (data: Account) => void; + global: Global +} + +export const RcDelegationsList = (props: any) => { + const params: any = useParams(); + + const { + activeUser, + rcFormatter, + showDelegation, + listMode, + setToFromList, + setAmountFromList, + confirmDelete, + setDelegateeData, + setShowDelegationsList + } = props; + + const [outGoingList, setOutGoingList]: any = useState([]); + const [otherUser, setOtherUser]: any = useState(params.username.substring(1)); + const [incoming, setIncoming]: any = useState([]); + const [loading, setLoading] = useState(false); + const [search, setsearch] = useState(""); + const [loadList, setLoadList] = useState(21); + + useEffect(() => { + getOutGoingRcList(); + getIncomingRcList(); + }, []); + + const getOutGoingRcList = async () => { + setLoading(true); + const delegationsOutList: any = await getOutgoingRc(otherUser, ""); + const delegationsOutInfo = delegationsOutList.rc_direct_delegations; + setOutGoingList(delegationsOutInfo); + setLoading(false); + }; + + const getIncomingRcList = async () => { + setLoading(true); + const delegationsIn: any = await getIncomingRc(otherUser); + const incomingInfo = delegationsIn.list; + setIncoming(incomingInfo); + setLoading(false); + }; + + const loadMore = () => { + const moreList = loadList + 7; + setLoadList(moreList); + }; + + const getToData = async (data: any) => { + const toData = await getAccount(data); + return toData; + }; + + return ( +
+ {loading && ( +
+ +
+ )} +
+
+ setsearch(e.target.value)} + /> +
+ + {listMode === "out" && ( + <> + {outGoingList.length > 0 ? ( +
+ {outGoingList + ?.slice(0, loadList) + .filter( + (list: any) => + list.to.toLowerCase().startsWith(search) || + list.to.toLowerCase().includes(search) + ) + .map((list: any, i: any) => ( +
+
+ {/* {ProfileLink({ + ...props, + username: list.to, + children: + })} */} + +
+ {ProfileLink({ + ...props, + username: list.to, + children: {list.to} + })} +
+
+ +
+ ))} +
+ ) : ( +

{_t("rc-info.no-outgoing")}

+ )} + + )} + + {listMode === "in" && ( + <> + {incoming.length > 0 ? ( +
+ {incoming + ?.slice(0, loadList) + .filter( + (list: any) => + list.sender.toLowerCase().startsWith(search) || + list.sender.toLowerCase().includes(search) + ) + .map((list: any, i: any) => ( +
+
+ {/* {ProfileLink({ + ...props, + username: list.sender, + children: + })} */} +
+ +
+ {ProfileLink({ + ...props, + username: list.sender, + children: {list.sender} + })} +
+
+
+ + {rcFormatter(list.amount)} + +
+
+
+ ))} +
+ ) : ( +

{_t("rc-info.no-incoming")}

+ )} + + )} + + {((listMode === "in" && incoming.length >= loadList) || + (listMode === "out" && outGoingList.length >= loadList)) && ( +
+ +
+ )} +
+
+ ); +}; + +export const ConfirmDelete = (props: any) => { + const { to, activeUser, hideConfirmDelete } = props; + return ( + <> +
+
+ {_t("rc-info.confirm-delete")} +
+
+ + +
+
+ + ); +}; + +export default (p: Props) => { + const props: Props = { + addAccount: p.addAccount, + global: p.global + }; + + return ; +}; diff --git a/src/common/components/user-avatar/index.tsx b/src/common/components/user-avatar/index.tsx index 55bebeb5ad4..16ee800a714 100644 --- a/src/common/components/user-avatar/index.tsx +++ b/src/common/components/user-avatar/index.tsx @@ -4,6 +4,7 @@ import {Global} from "../../store/global/types"; import defaults from '../../constants/defaults.json'; import { proxifyImageSrc } from '@ecency/render-helper'; +// import { useMappedStore } from "../../store/use-mapped-store"; interface Props { global: Global; diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index b999973b1c1..0808f7d77f5 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -568,6 +568,47 @@ "link-copied": "Search link copied to clipboard", "no-posts-found": "Found nothing!" }, + "rc-info": { + "available": "Available", + "used": "Used", + "resource-credits": "Resource Credits", + "resource-credit": "RC", + "delegated": "Delegations Out", + "received-delegations": "Delegations In", + "delegation-button": "Delegate RC", + "rc-available": "Available", + "rc-used": "Used", + "outgoing": "Outgoing", + "incoming": "Incoming", + "delete": "delete", + "update": "update", + "confirm-delete": "Are you sure?", + "confirm": "Confirm", + "cancel": "Cancel", + "check-faq": "Learn more about RC in our", + "learn-more": "/faq#what-are-rc", + "faq-link": "FAQ", + "insufficient-rc-error": "Insufficient resource credits", + "minimum-rc-error": "Mininum RC is 5,000,000,000", + "extra-details-heading": "What's possible with current available RC?", + "transfers": "Transfers:", + "comments-posts": "Posts / Comments:", + "votes": "Votes:", + "reblogs-follows": "Reblogs / Follows:", + "claim-accounts": "Claim account credits:", + "no-outgoing": "No outgoing delegation", + "no-incoming": "No incoming delegation", + "delegate-title": "Delegate", + "delegate-sub-title": "Delegate Resource Credit", + "extra-details-transfer": "Transfers", + "extra-details-post": "Posts/Comments", + "extra-details-upvote": "Votes", + "boost": "Boost", + "you-have-claimed-rc": "Already claimed account credits", + "claim": "Claim", + "you-claiming": "You are claiming {{n}}", + "successfully-claimed": "Successfully claimed!" + }, "profile-edit": { "title": "Profile", "name": "Display Name", diff --git a/src/common/store/actions.ts b/src/common/store/actions.ts new file mode 100644 index 00000000000..4c3aa06617b --- /dev/null +++ b/src/common/store/actions.ts @@ -0,0 +1,91 @@ +import { + dismissNewVersion, + hideIntro, + muteNotifications, + newVersionChangeAct, + setCurrency, + setLang, + setLastIndexPath, + setNsfw, + toggleListStyle, + toggleTheme, + unMuteNotifications + } from "./global"; + import { fetchTrendingTags } from "./trending-tags"; + import { updateSubscriptions } from "./subscriptions"; + import { addEntry, fetchEntries, invalidateEntries, updateEntry } from "./entries"; + import { + addReply, + deleteReply, + fetchDiscussion, + resetDiscussion, + sortDiscussion, + updateReply + } from "./discussion"; + import { addAccount } from "./accounts"; + import { fetchTransactions, resetTransactions } from "./transactions"; + import { addUser, deleteUser } from "./users"; + import { setActiveUser, updateActiveUser } from "./active-user"; + import { toggleUIProp } from "./ui"; + import { addReblog, deleteReblog, fetchReblogs } from "./reblogs"; + import { + fetchNotifications, + // fetchNotificationsSettings, + fetchUnreadNotificationCount, + markNotifications, + setNotificationsFilter, + // setNotificationsSettingsItem, + // updateNotificationsSettings + } from "./notifications"; + import { setSigningKey } from "./signing-key"; + + // @note Do not use it directly + export const ACTIONS = { + toggleTheme, + newVersionChangeAct, + hideIntro, + toggleListStyle, + muteNotifications, + unMuteNotifications, + setCurrency, + setLang, + setNsfw, + setLastIndexPath, + dismissNewVersion, + fetchTrendingTags, + updateSubscriptions, + fetchEntries, + addEntry, + updateEntry, + invalidateEntries, + fetchDiscussion, + sortDiscussion, + resetDiscussion, + updateReply, + addReply, + deleteReply, + addAccount, + fetchTransactions, + resetTransactions, + addUser, + deleteUser, + setActiveUser, + updateActiveUser, + toggleUIProp, + addReblog, + deleteReblog, + fetchReblogs, + fetchNotifications, + fetchUnreadNotificationCount, + setNotificationsFilter, + markNotifications, + setSigningKey, + // updateNotificationsSettings, + // fetchNotificationsSettings, + // setNotificationsSettingsItem + }; + + export const getActions = () => ({ + ...ACTIONS + }); + \ No newline at end of file diff --git a/src/common/store/global/index.ts b/src/common/store/global/index.ts index 11dd695c800..a5c660a07ad 100644 --- a/src/common/store/global/index.ts +++ b/src/common/store/global/index.ts @@ -21,6 +21,7 @@ import { CurrencySetAction, LangSetAction, NsfwSetAction, + SetLastIndexPathAction, Theme, ThemeChangeAction, } from "./types"; @@ -33,10 +34,10 @@ import filterTagExtract from "../../helper/filter-tag-extract"; import { setupConfig } from "../../../setup"; export const initialState: Global = { - filter: AllFilter[defaults.filter], + // filter: AllFilter[defaults.filter], tag: "", - theme: Theme[setupConfig.selectedTheme], - listStyle: ListStyle[defaults.listStyle], + // theme: Theme[setupConfig.selectedTheme], + // listStyle: ListStyle[defaults.listStyle], intro: true, currency: defaults && defaults.currency && defaults.currency.currency, currencyRate: defaults && defaults.currency && defaults.currency.rate, @@ -277,3 +278,12 @@ export const hasKeyChainAct = (): HasKeyChainAction => { type: ActionTypes.HAS_KEYCHAIN, }; }; + +export const setLastIndexPath = (path: string | null) => (dispatch: Dispatch) => { + dispatch(setLastIndexPathAct(path)); +}; + +export const setLastIndexPathAct = (path: string | null): SetLastIndexPathAction => ({ + type: ActionTypes.SET_LAST_INDEX_PATH, + path +}); \ No newline at end of file diff --git a/src/common/store/global/types.ts b/src/common/store/global/types.ts index c870975244b..e1766a39e06 100644 --- a/src/common/store/global/types.ts +++ b/src/common/store/global/types.ts @@ -42,7 +42,7 @@ export enum AllFilter { comments = "comments", replies = "replies", communities = "communities", - feed = "feed" + feed = "feed", } export interface Global { @@ -81,6 +81,7 @@ export enum ActionTypes { LANG_SET = "@global/LANG_SET", NEW_VERSION_CHANGE = "@global/NEW_VERSION_CHANGE", NSFW_SET = "@global/NSFW_SET", + SET_LAST_INDEX_PATH = "@global/SET_LAST_INDEX_PATH", } export interface ThemeChangeAction { @@ -131,6 +132,11 @@ export interface HasKeyChainAction { type: ActionTypes.HAS_KEYCHAIN; } +export interface SetLastIndexPathAction { + type: ActionTypes.SET_LAST_INDEX_PATH; + path: string | null; + } + export type Actions = LocationChangeAction | ThemeChangeAction @@ -142,4 +148,5 @@ export type Actions = | CurrencySetAction | LangSetAction | NsfwSetAction - | HasKeyChainAction; \ No newline at end of file + | HasKeyChainAction + | SetLastIndexPathAction; \ No newline at end of file diff --git a/src/common/store/use-mapped-store.ts b/src/common/store/use-mapped-store.ts new file mode 100644 index 00000000000..9bb85598514 --- /dev/null +++ b/src/common/store/use-mapped-store.ts @@ -0,0 +1,22 @@ +import { useDispatch, useSelector, useStore } from "react-redux"; +import { AppState } from "./index"; +import { bindActionCreators } from "redux"; +import "./actions"; +import { getActions } from "./actions"; + +export const useMappedStore = () => { + const store = useStore(); + const dispatch = useDispatch(); + + const storeStateAccessor: AppState = store.getState(); + const storeStateAccessorProxy = new Proxy(storeStateAccessor, { + get(target, p, receiver: any): any { + return useSelector((state: any) => state[p]); + } + }); + + return { + ...storeStateAccessorProxy, + ...bindActionCreators(getActions(), dispatch) + }; +}; diff --git a/src/common/util/formatted-number.ts b/src/common/util/formatted-number.ts index 9418a9a7022..16a427d93e9 100644 --- a/src/common/util/formatted-number.ts +++ b/src/common/util/formatted-number.ts @@ -35,3 +35,19 @@ export default (value: number | string, options: Options | undefined = undefined return out; }; + +export const rcFormatter = (num: number) => { + const result: any = + Math.abs(num) > 999 && Math.abs(num) < 1000000 + ? `${Math.sign(num) * parseFloat((Math.abs(num) / 1000).toFixed(2))}K` + : Math.abs(num) > 999999 && Math.abs(num) < 1000000000 + ? `${Math.sign(num) * parseFloat((Math.abs(num) / 1000000).toFixed(2))}M` + : Math.abs(num) > 999999999 && Math.abs(num) < 1000000000000 + ? `${Math.sign(num) * parseFloat((Math.abs(num) / 1000000000).toFixed(2))}B` + : Math.abs(num) > 999999999999 && Math.abs(num) < 1000000000000000 + ? `${Math.sign(num) * parseFloat((Math.abs(num) / 1000000000000).toFixed(2))}T` + : Math.abs(num) > 999999999999999 && Math.abs(num) < 1000000000000000000 + ? `${Math.sign(num) * parseFloat((Math.abs(num) / 1000000000000000).toFixed(2))}Q` + : Math.sign(num) * Math.abs(num); + return result; +}; diff --git a/src/style/_components.scss b/src/style/_components.scss index cb780604a6f..fb6cecb58d4 100644 --- a/src/style/_components.scss +++ b/src/style/_components.scss @@ -120,6 +120,7 @@ @import "../common/components/open-orders-list/"; @import "../common/components/savings-withdraw/"; @import "../common/components/onboard-friend/"; +@import "../common/components/resource-credits/"; @import "../desktop/app/components/navbar"; @import "../desktop/app/components/updater";