mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-11-15 10:57:06 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
commit
d21bfb1320
5867 files changed
+329019
No files matched your search
@@ -0,0 +1,221 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, Balance, BlockNumber, OpenTipTo225 } from '@pezkuwi/types/interfaces';
|
||||
import type { PalletTipsOpenTip } from '@pezkuwi/types/lookup';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { AddressMini, AddressSmall, Checkbox, ExpanderScroll, Icon, LinkExternal, styled, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi } from '@pezkuwi/react-hooks';
|
||||
import { BlockToTime, FormatBalance } from '@pezkuwi/react-query';
|
||||
import { BN_ZERO, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import TipEndorse from './TipEndorse.js';
|
||||
import TipReason from './TipReason.js';
|
||||
|
||||
interface Props {
|
||||
bestNumber?: BlockNumber;
|
||||
className?: string;
|
||||
defaultId: string | null;
|
||||
hash: string;
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
onSelect: (hash: string, isSelected: boolean, value: BN) => void;
|
||||
onlyUntipped: boolean;
|
||||
tip: PalletTipsOpenTip | OpenTipTo225;
|
||||
}
|
||||
|
||||
interface TipState {
|
||||
closesAt: BlockNumber | null;
|
||||
deposit: Balance | null;
|
||||
finder: AccountId | null;
|
||||
isFinder: boolean;
|
||||
isTipped: boolean;
|
||||
isTipper: boolean;
|
||||
median: BN;
|
||||
}
|
||||
|
||||
function isCurrentTip (tip: PalletTipsOpenTip | OpenTipTo225): tip is PalletTipsOpenTip {
|
||||
return !!(tip as PalletTipsOpenTip)?.findersFee;
|
||||
}
|
||||
|
||||
function extractTipState (tip: PalletTipsOpenTip | OpenTipTo225, allAccounts: string[]): TipState {
|
||||
const closesAt = tip.closes.unwrapOr(null);
|
||||
let finder: AccountId | null = null;
|
||||
let deposit: Balance | null = null;
|
||||
|
||||
if (isCurrentTip(tip)) {
|
||||
finder = tip.finder;
|
||||
deposit = tip.deposit;
|
||||
} else if (tip.finder.isSome) {
|
||||
const finderInfo = tip.finder.unwrap();
|
||||
|
||||
finder = finderInfo[0];
|
||||
deposit = finderInfo[1];
|
||||
}
|
||||
|
||||
const values = tip.tips.map(([, value]) => value).sort((a, b) => a.cmp(b));
|
||||
const midIndex = Math.floor(values.length / 2);
|
||||
const median = values.length
|
||||
? values.length % 2
|
||||
? values[midIndex]
|
||||
: values[midIndex - 1].add(values[midIndex]).divn(2)
|
||||
: BN_ZERO;
|
||||
|
||||
return {
|
||||
closesAt,
|
||||
deposit,
|
||||
finder,
|
||||
isFinder: !!finder && allAccounts.includes(finder.toString()),
|
||||
isTipped: !!values.length,
|
||||
isTipper: tip.tips.some(([address]) => allAccounts.includes(address.toString())),
|
||||
median
|
||||
};
|
||||
}
|
||||
|
||||
function Tip ({ bestNumber, className = '', defaultId, hash, isMember, members, onSelect, onlyUntipped, tip }: Props): React.ReactElement<Props> | null {
|
||||
const { api } = useApi();
|
||||
const { t } = useTranslation();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const { closesAt, finder, isFinder, isTipped, isTipper, median } = useMemo(
|
||||
() => extractTipState(tip, allAccounts),
|
||||
[allAccounts, tip]
|
||||
);
|
||||
|
||||
const councilId = useMemo(
|
||||
() => allAccounts.find((accountId) => members.includes(accountId)) || null,
|
||||
[allAccounts, members]
|
||||
);
|
||||
|
||||
const [isMedianSelected, setMedianTip] = useState(false);
|
||||
|
||||
const renderTippers = useCallback(
|
||||
() => tip.tips.map(([tipper, balance]) => (
|
||||
<AddressMini
|
||||
balance={balance}
|
||||
key={tipper.toString()}
|
||||
value={tipper}
|
||||
withBalance
|
||||
/>
|
||||
)),
|
||||
[tip]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
onSelect(hash, isMedianSelected, median);
|
||||
}, [hash, isMedianSelected, median, onSelect]);
|
||||
|
||||
useEffect((): void => {
|
||||
setMedianTip(isMember && !isTipper);
|
||||
}, [isMember, isTipper]);
|
||||
|
||||
if (onlyUntipped && !closesAt && isTipper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { reason, tips, who } = tip;
|
||||
const recipient = who.toString();
|
||||
|
||||
return (
|
||||
<StyledTr className={className}>
|
||||
<td className='address'>
|
||||
<AddressSmall value={who} />
|
||||
</td>
|
||||
<td className='address media--1400'>
|
||||
{finder && (
|
||||
<AddressMini value={finder} />
|
||||
)}
|
||||
</td>
|
||||
<TipReason hash={reason} />
|
||||
<td className='expand media--1100'>
|
||||
{tips.length !== 0 && (
|
||||
<ExpanderScroll
|
||||
renderChildren={renderTippers}
|
||||
summary={
|
||||
<>
|
||||
<div>{t('Tippers ({{count}})', { replace: { count: tips.length } })}</div>
|
||||
<FormatBalance value={median} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='button together'>
|
||||
{closesAt
|
||||
? (bestNumber && closesAt.gt(bestNumber)) && (
|
||||
<div className='closingTimer'>
|
||||
<BlockToTime value={closesAt.sub(bestNumber)} />
|
||||
#{formatNumber(closesAt)}
|
||||
</div>
|
||||
)
|
||||
: finder && isFinder && (
|
||||
<TxButton
|
||||
accountId={finder}
|
||||
className='media--1400'
|
||||
icon='times'
|
||||
label={t('Cancel')}
|
||||
params={[hash]}
|
||||
tx={(api.tx.tips || api.tx.treasury).retractTip}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{(!closesAt || !bestNumber || closesAt.gt(bestNumber))
|
||||
? (
|
||||
<TipEndorse
|
||||
defaultId={defaultId}
|
||||
hash={hash}
|
||||
isMember={isMember}
|
||||
isTipped={isTipped}
|
||||
median={median}
|
||||
members={members}
|
||||
recipient={recipient}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<TxButton
|
||||
accountId={councilId}
|
||||
icon='times'
|
||||
label={t('Close')}
|
||||
params={[hash]}
|
||||
tx={(api.tx.tips || api.tx.treasury).closeTip}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</td>
|
||||
<td className='badge media--1700'>
|
||||
{isMember && (
|
||||
<Icon
|
||||
color={isTipper ? 'green' : 'gray'}
|
||||
icon='asterisk'
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Checkbox
|
||||
isDisabled={!isMember}
|
||||
onChange={setMedianTip}
|
||||
value={isMedianSelected}
|
||||
/>
|
||||
</td>
|
||||
<td className='links media--1700'>
|
||||
<LinkExternal
|
||||
data={hash}
|
||||
type='tip'
|
||||
/>
|
||||
</td>
|
||||
</StyledTr>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTr = styled.tr`
|
||||
.closingTimer {
|
||||
display: inline-block;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Tip);
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Input, InputAddress, InputBalance, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
members: string[];
|
||||
}
|
||||
|
||||
const MAX_REASON_LEN = 256;
|
||||
const MIN_REASON_LEN = 5;
|
||||
|
||||
function TipCreate ({ members }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [beneficiary, setBeneficiary] = useState<string | null>(null);
|
||||
const [reason, setReason] = useState('');
|
||||
const [value, setValue] = useState<BN | undefined>();
|
||||
const maxReasonLen = useMemo(
|
||||
() => Math.min(MAX_REASON_LEN, (
|
||||
(api.consts.tips || api.consts.treasury)?.maximumReasonLength?.toNumber() ||
|
||||
MAX_REASON_LEN
|
||||
)),
|
||||
[api]
|
||||
);
|
||||
const isMember = useMemo(
|
||||
() => !!accountId && members.includes(accountId),
|
||||
[accountId, members]
|
||||
);
|
||||
const hasValue = !!value && value.gt(BN_ZERO);
|
||||
const hasReason = !!reason && (reason.length >= MIN_REASON_LEN) && (reason.length <= maxReasonLen);
|
||||
|
||||
if (!(api.tx.tips.tipNew || api.tx.treasury.tipNew)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Propose tip')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={t('Submit tip request')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('Use this account to request the tip from. This can be a normal or council account.')}>
|
||||
<InputAddress
|
||||
label={t('submit with account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The beneficiary will received the tip as approved by council members.')}>
|
||||
<InputAddress
|
||||
label={t('beneficiary')}
|
||||
onChange={setBeneficiary}
|
||||
type='allPlus'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('A reason (to be stored-on-chain) as to why the recipient deserves a tip payout.')}>
|
||||
<Input
|
||||
autoFocus
|
||||
isError={!hasReason}
|
||||
label={t('tip reason')}
|
||||
onChange={setReason}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
{isMember && (
|
||||
<Modal.Columns hint={t('As a council member, you can suggest an initial value for the tip, each other council member can suggest their own.')}>
|
||||
<InputBalance
|
||||
isError={!hasValue}
|
||||
label={t('tip value')}
|
||||
onChange={setValue}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!accountId || (isMember && !hasValue) || !hasReason}
|
||||
label={t('Propose tip')}
|
||||
onStart={toggleOpen}
|
||||
params={
|
||||
isMember
|
||||
? [reason, beneficiary, value]
|
||||
: [reason, beneficiary]
|
||||
}
|
||||
tx={
|
||||
isMember
|
||||
? (api.tx.tips || api.tx.treasury).tipNew
|
||||
: (api.tx.tips || api.tx.treasury).reportAwesome
|
||||
}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TipCreate);
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveBalancesAll } from '@pezkuwi/api-derive/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, InputAddress, InputBalance, MarkWarning, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
defaultId: string | null;
|
||||
hash: string;
|
||||
isMember: boolean;
|
||||
isTipped: boolean;
|
||||
median: BN;
|
||||
members: string[];
|
||||
recipient: string;
|
||||
}
|
||||
|
||||
const OPT = {
|
||||
transform: ({ freeBalance, reservedBalance }: DeriveBalancesAll): BN =>
|
||||
freeBalance.add(reservedBalance)
|
||||
};
|
||||
|
||||
function TipEndorse ({ defaultId, hash, isMember, isTipped, median, members, recipient }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccountId] = useState<string | null>(defaultId);
|
||||
const [value, setValue] = useState<BN | undefined>();
|
||||
const totalBalance = useCall<BN>(api.derive.balances?.all, [recipient], OPT);
|
||||
|
||||
const tipTx = (api.tx.tips || api.tx.treasury).tip;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='check'
|
||||
isDisabled={!isMember}
|
||||
label={t('Tip')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
<TxButton
|
||||
accountId={defaultId}
|
||||
className='media--1600'
|
||||
icon='fighter-jet'
|
||||
isDisabled={!isMember || !isTipped}
|
||||
isIcon
|
||||
params={[hash, median]}
|
||||
tx={tipTx}
|
||||
withoutLink
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={t('Submit tip endorsement')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('Your endorsement will be applied for this account.')}>
|
||||
<InputAddress
|
||||
filter={members}
|
||||
label={t('submit with account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('Allocate a suggested tip amount. With enough endorsements, the suggested values are averaged and sent to the beneficiary.')}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
defaultValue={median}
|
||||
isZeroable
|
||||
label={t('value')}
|
||||
onChange={setValue}
|
||||
/>
|
||||
{totalBalance && totalBalance.isZero() && (
|
||||
<MarkWarning content={t('The recipient account has no balance, ensure the tip is more than the existential deposit to create the account.')} />
|
||||
)}
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='plus'
|
||||
isDisabled={!accountId}
|
||||
label={t('Submit tip')}
|
||||
onStart={toggleOpen}
|
||||
params={[hash, value]}
|
||||
tx={tipTx}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TipEndorse);
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Bytes, Option } from '@pezkuwi/types';
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { hexToString } from '@pezkuwi/util';
|
||||
|
||||
interface Props {
|
||||
hash: Hash;
|
||||
}
|
||||
|
||||
const OPT = {
|
||||
transform: (optBytes: Option<Bytes>) =>
|
||||
optBytes.isSome
|
||||
? hexToString(optBytes.unwrap().toHex())
|
||||
: null
|
||||
};
|
||||
|
||||
function TipReason ({ hash }: Props): React.ReactElement<Props> {
|
||||
const { api } = useApi();
|
||||
const reasonText = useCall<string | null>((api.query.tips || api.query.treasury).reasons, [hash], OPT);
|
||||
|
||||
return (
|
||||
<td className='start all'>{reasonText || hash.toHex()}</td>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TipReason);
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { OpenTipTo225 } from '@pezkuwi/types/interfaces';
|
||||
import type { PalletTipsOpenTip } from '@pezkuwi/types/lookup';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { styled, Table, Toggle } from '@pezkuwi/react-components';
|
||||
import { useApi, useBestNumber, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Tip from './Tip.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
defaultId: string | null;
|
||||
hashes?: string[] | null;
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
onSelectTip: (hash: string, isSelected: boolean, value: BN) => void,
|
||||
}
|
||||
|
||||
type TipType = [string, PalletTipsOpenTip | OpenTipTo225];
|
||||
|
||||
const TIP_OPTS = { withParams: true };
|
||||
|
||||
function extractTips (tipsWithHashes?: [[string[]], Option<PalletTipsOpenTip>[]], inHashes?: string[] | null): TipType[] | undefined {
|
||||
if (!tipsWithHashes || !inHashes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [[hashes], optTips] = tipsWithHashes;
|
||||
|
||||
return optTips
|
||||
.map((opt, index): [string, PalletTipsOpenTip | null] => [hashes[index], opt.unwrapOr(null)])
|
||||
.filter((val): val is [string, PalletTipsOpenTip] => inHashes.includes(val[0]) && !!val[1])
|
||||
.sort((a, b) =>
|
||||
a[1].closes.isNone
|
||||
? b[1].closes.isNone
|
||||
? 0
|
||||
: -1
|
||||
: b[1].closes.isSome
|
||||
? b[1].closes.unwrap().cmp(a[1].closes.unwrap())
|
||||
: 1
|
||||
);
|
||||
}
|
||||
|
||||
function Tips ({ className = '', defaultId, hashes, isMember, members, onSelectTip }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [onlyUntipped, setOnlyUntipped] = useState(false);
|
||||
const bestNumber = useBestNumber();
|
||||
const tipsWithHashes = useCall<[[string[]], Option<PalletTipsOpenTip>[]]>(hashes && (api.query.tips || api.query.treasury).tips.multi, [hashes], TIP_OPTS);
|
||||
|
||||
const tips = useMemo(
|
||||
() => extractTips(tipsWithHashes, hashes),
|
||||
[hashes, tipsWithHashes]
|
||||
);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('tips'), 'start'],
|
||||
[t('finder'), 'address media--1400'],
|
||||
[t('reason'), 'start'],
|
||||
[undefined, 'media--1100'],
|
||||
[],
|
||||
[undefined, 'badge media--1700'],
|
||||
[],
|
||||
[undefined, 'media--1700']
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledTable
|
||||
className={className}
|
||||
empty={tips && t('No open tips')}
|
||||
filter={isMember && (
|
||||
<div className='tipsFilter'>
|
||||
<Toggle
|
||||
label={t('show only untipped/closing')}
|
||||
onChange={setOnlyUntipped}
|
||||
value={onlyUntipped}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{tips?.map(([hash, tip]): React.ReactNode => (
|
||||
<Tip
|
||||
bestNumber={bestNumber}
|
||||
defaultId={defaultId}
|
||||
hash={hash}
|
||||
isMember={isMember}
|
||||
key={hash}
|
||||
members={members}
|
||||
onSelect={onSelectTip}
|
||||
onlyUntipped={onlyUntipped}
|
||||
tip={tip}
|
||||
/>
|
||||
))}
|
||||
</StyledTable>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
.tipsFilter {
|
||||
text-align: right;
|
||||
|
||||
.ui--Toggle {
|
||||
margin-right: 1rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Tips);
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useTxBatch } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import TipCreate from './TipCreate.js';
|
||||
import Tips from './Tips.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
hashes?: string[] | null;
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
}
|
||||
|
||||
interface QuickTipsState {
|
||||
quickTips: Record<string, BN | null>;
|
||||
quickTxs: SubmittableExtrinsic<'promise'>[];
|
||||
}
|
||||
|
||||
const DEFAULT_TIPS = { quickTips: {}, quickTxs: [] };
|
||||
|
||||
function TipsEntry ({ className, hashes, isMember, members }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { allAccounts } = useAccounts();
|
||||
const { api } = useApi();
|
||||
const [{ quickTxs }, setQuickTips] = useState<QuickTipsState>(DEFAULT_TIPS);
|
||||
const batchTxs = useTxBatch(quickTxs);
|
||||
|
||||
const defaultId = useMemo(
|
||||
() => members.find((memberId) => allAccounts.includes(memberId)) || null,
|
||||
[allAccounts, members]
|
||||
);
|
||||
|
||||
const _selectTip = useCallback(
|
||||
(hash: string, isSelected: boolean, value: BN) => setQuickTips(({ quickTips }): QuickTipsState => {
|
||||
quickTips[hash] = isSelected ? value : null;
|
||||
|
||||
return {
|
||||
quickTips,
|
||||
quickTxs: Object
|
||||
.entries(quickTips)
|
||||
.map(([hash, value]) => value && (api.tx.tips || api.tx.treasury).tip(hash, value))
|
||||
.filter((value): value is SubmittableExtrinsic<'promise'> => !!value)
|
||||
};
|
||||
}),
|
||||
[api]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Button.Group>
|
||||
<TipCreate members={members} />
|
||||
<TxButton
|
||||
accountId={defaultId}
|
||||
extrinsic={batchTxs}
|
||||
icon='fighter-jet'
|
||||
isDisabled={!isMember || !batchTxs}
|
||||
label={t('Median tip selected')}
|
||||
/>
|
||||
</Button.Group>
|
||||
<Tips
|
||||
defaultId={defaultId}
|
||||
hashes={hashes}
|
||||
isMember={isMember}
|
||||
members={members}
|
||||
onSelectTip={_selectTip}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TipsEntry);
|
||||
Reference in new issue
Block a user