Skip to content

Commit

Permalink
Support proposing curator on child bounty detail page, #4782 (#4805)
Browse files Browse the repository at this point in the history
* Init ProposeCuratorPopup, #4782

* Add ProposeCuratorPopup balance & fee fileds, #4782

* Add getCheckedFee, #4782

* Add proposeCurator params, #4782

* proposeCurator params, #4782

* modify proposeCurator params, #4782

* Fix lint error, #4782

* Proposed button null guard, #4782

* modify state, #4782

* add disabled judgment, #4782

* Fix lint error, #4782

* Modify parentCurator judgment, #4782

* Add useFeeAmount, #4782

* optimize code, #4782

* optimize code, #4782

* add useSubBountyOnChainData, #4782

* rename useSubParentBountyData, #4782

* optimize code, #4782

* fix useSubChildBountyIsAdded, #4782

* fix lint error, #4782
  • Loading branch information
leocs2417 authored Sep 30, 2024
1 parent 3bff514 commit 205cab4
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from "react";
import useProposeCuratorPopup from "./useProposeCurator";
import PrimaryButton from "next-common/lib/button/primary";
import { useOnchainData } from "next-common/context/post";
import Tooltip from "next-common/components/tooltip";
import useRealAddress from "next-common/utils/hooks/useRealAddress";
import useSubStorage from "next-common/hooks/common/useSubStorage";

function useSubParentBountyData(bountyIndex) {
const { result, loading } = useSubStorage("bounties", "bounties", [
bountyIndex,
]);
const data = result?.toJSON();

return {
status: data?.status,
loading,
};
}

function useSubChildBountyIsAdded(parentBountyId, index) {
const { loading, result: onChainStorage } = useSubStorage(
"childBounties",
"childBounties",
[parentBountyId, index],
);

if (loading || !onChainStorage?.isSome) {
return false;
}
const { status } = onChainStorage.toJSON();
if (!status || !("added" in status)) {
return false;
}
return true;
}

function isParentBountyCurator(status = {}, address) {
for (const item of Object.values(status)) {
if (item?.curator && item.curator === address) {
return true;
}
}
return false;
}

export default function ProposeCurator() {
const address = useRealAddress();
const [isDisabled, setIsDisabled] = useState(true);
const [disabledTooltip, setDisabledTooltip] = useState("");
const { showPopupFn, component: ProposeCuratorPopup } =
useProposeCuratorPopup();
const { parentBountyId, index } = useOnchainData();
const { status, loading } = useSubParentBountyData(parentBountyId);
const isAddedState = useSubChildBountyIsAdded(parentBountyId, index);

// The dispatch origin for this call must be curator of parent bounty.
useEffect(() => {
if (loading) {
return;
}

const isParentCurator = isParentBountyCurator(status, address);
setIsDisabled(!isParentCurator);

if (!isParentCurator) {
const disabledTooltipContent =
"Only parent bounty curator can propose a curator";
setDisabledTooltip(disabledTooltipContent);
}
}, [loading, address, status]);

if (!address || !isAddedState) {
return null;
}

return (
<>
<Tooltip content={disabledTooltip}>
<PrimaryButton
className="w-full"
onClick={() => showPopupFn()}
disabled={isDisabled}
>
Propose Curator
</PrimaryButton>
</Tooltip>
{ProposeCuratorPopup}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { formatBalance } from "next-common/components/assets/assetsList";
import BalanceDisplay from "next-common/components/assets/balanceDisplay";
import Loading from "next-common/components/loading";
import PopupLabel from "next-common/components/popup/label";
import Input from "next-common/components/input";
import { useCallback, useState } from "react";
import BigNumber from "bignumber.js";

function checkFeeAmount({ feeAmount, decimals, balance }) {
if (!feeAmount) {
throw new Error("Please fill the fee");
}

const amount = new BigNumber(feeAmount).times(Math.pow(10, decimals));
if (amount.isNaN() || amount.lte(0) || !amount.isInteger()) {
throw new Error("Invalid fee");
}
if (balance && amount.gte(balance)) {
throw new Error("Fee should be less than child bounty value");
}

return amount.toFixed();
}

function MaxBalance({ value, isLoading, decimals, symbol }) {
return (
<div className="flex gap-[8px] items-center mb-[8px]">
<span className="text12Medium text-textTertiary leading-none">Max</span>
{isLoading ? (
<Loading size={12} />
) : (
<span>
<BalanceDisplay balance={formatBalance(value, decimals)} />
<span className="text-textPrimary ml-1">{symbol}</span>
</span>
)}
</div>
);
}

function FeeAmount({
balance,
decimals,
symbol,
isLoading,
address,
feeAmount,
setFeeAmount,
}) {
const handleFeeChange = useCallback(
(e) => {
setFeeAmount(e.target.value.replace("。", "."));
},
[setFeeAmount],
);

return (
<div>
<PopupLabel
text="Fee"
status={
!!address && (
<MaxBalance
value={balance}
isLoading={isLoading}
decimals={decimals}
symbol={symbol}
/>
)
}
/>
<Input
type="text"
placeholder="0.00"
value={feeAmount}
onChange={handleFeeChange}
symbol={symbol}
/>
</div>
);
}

export default function useFeeAmount(props = {}) {
const { balance, decimals, symbol, address, isLoading } = props;

const [feeAmount, setFeeAmount] = useState("");

const component = (
<FeeAmount
balance={balance}
decimals={decimals}
symbol={symbol}
isLoading={isLoading}
address={address}
feeAmount={feeAmount}
setFeeAmount={setFeeAmount}
/>
);

const getCheckedValue = useCallback(() => {
return checkFeeAmount({
feeAmount,
decimals,
balance,
});
}, [feeAmount, decimals, balance]);

return {
value: feeAmount,
component,
getCheckedValue,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import TxSubmissionButton from "next-common/components/common/tx/txSubmissionButton";
import Signer from "next-common/components/popup/fields/signerField";
import PopupWithSigner from "next-common/components/popupWithSigner";
import { usePopupParams } from "next-common/components/popupWithSigner/context";
import useAddressComboField from "next-common/components/preImages/createPreimagePopup/fields/useAddressComboField";
import { useContextApi } from "next-common/context/api";
import { newErrorToast } from "next-common/store/reducers/toastSlice";
import useRealAddress from "next-common/utils/hooks/useRealAddress";
import { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { useSubBalanceInfo } from "next-common/hooks/balance/useSubBalanceInfo";
import { useChainSettings } from "next-common/context/chain";
import { useOnchainData } from "next-common/context/post";
import useFeeAmount from "./useFeeAmount";
import useSubAddressBalance from "next-common/utils/hooks/useSubAddressBalance";

function PopupContent() {
const { onClose } = usePopupParams();
const { decimals, symbol } = useChainSettings();
const address = useRealAddress();
const { value: signerBalance, loading: signerBalanceLoading } =
useSubBalanceInfo(address);
const api = useContextApi();
const dispatch = useDispatch();

const {
parentBountyId,
index: childBountyId,
address: childBountyAddress,
} = useOnchainData();
const { balance: metadataBalance, isLoading: metadataBalanceLoading } =
useSubAddressBalance(childBountyAddress);

const { getCheckedValue: getCheckedFee, component: feeField } = useFeeAmount({
balance: metadataBalance,
decimals,
symbol,
address,
isLoading: metadataBalanceLoading,
});

const { value: curator, component: curatorSelect } = useAddressComboField({
title: "Curator",
});

const getTxFunc = useCallback(() => {
if (!curator) {
dispatch(newErrorToast("Please enter the recipient address"));
return;
}

let fee;
try {
fee = getCheckedFee();
} catch (e) {
dispatch(newErrorToast(e.message));
return;
}

return api.tx.childBounties?.proposeCurator(
parentBountyId,
childBountyId,
curator,
fee,
);
}, [curator, getCheckedFee, parentBountyId, childBountyId, api, dispatch]);

return (
<>
<Signer
balanceName="Available"
signerBalance={signerBalance?.balance}
isSignerBalanceLoading={signerBalanceLoading}
title="Origin"
/>
{curatorSelect}
{feeField}
<div className="flex justify-end">
<TxSubmissionButton
title="Confirm"
getTxFunc={getTxFunc}
onClose={onClose}
/>
</div>
</>
);
}

function ProposeCuratorPopup(props) {
return (
<PopupWithSigner title="Propose Curator" className="!w-[640px]" {...props}>
<PopupContent />
</PopupWithSigner>
);
}

export default function useProposeCuratorPopup() {
const [isOpen, setIsOpen] = useState(false);

return {
showPopupFn: () => setIsOpen(true),
component: isOpen ? (
<ProposeCuratorPopup onClose={() => setIsOpen(false)} />
) : null,
};
}
2 changes: 2 additions & 0 deletions packages/next/components/childBounty/sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Meta from "next-common/components/treasury/childBounty/metadata";
import { RightBarWrapper } from "next-common/components/layout/sidebar/rightBarWrapper";
import { usePostState } from "next-common/context/post";
import ChildBountySidebarBalance from "next-common/components/treasury/childBounty/balance";
import ProposeCurator from "next-common/components/treasury/childBounty/proposeCurator";
import ChildBountyAcceptCurator from "next-common/components/treasury/childBounty/acceptCurator";

export default function ChildBountySidebar() {
Expand All @@ -12,6 +13,7 @@ export default function ChildBountySidebar() {
return (
<RightBarWrapper>
<ChildBountySidebarBalance />
<ProposeCurator />
<ChildBountyAcceptCurator />
{isClaimable && (
<>
Expand Down

0 comments on commit 205cab4

Please sign in to comment.