Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/timer refactor #291

Merged
merged 6 commits into from
May 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 73 additions & 59 deletions packages/react-app/src/components/claim-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { Accordion, Box } from '@1hive/1hive-ui';
import { ClaimModel } from 'src/models/claim.model';
import styled from 'styled-components';
import { GUpx } from 'src/utils/style.util';
import { ENUM_CLAIM_STATE, ENUM_TRANSACTION_STATUS } from 'src/constants';
import { ENUM_TRANSACTION_STATUS } from 'src/constants';
import { TokenAmountModel } from 'src/models/token-amount.model';
import { QuestModel } from 'src/models/quest.model';
import { useEffect, useState } from 'react';
import { getObjectFromIpfsSafe } from 'src/services/ipfs.service';
import { useEffect, useMemo, useState } from 'react';
import { getObjectFromIpfs, ipfsTheGraph } from 'src/services/ipfs.service';
import { useTransactionContext } from 'src/contexts/transaction.context';
import { useIsMountedRef } from 'src/hooks/use-mounted.hook';
import { HelpTooltip } from './field-input/help-tooltip';
import * as QuestService from '../services/quest.service';
import Claim from './claim';
Expand Down Expand Up @@ -46,23 +47,70 @@ type Props = {
isLoading?: boolean;
};

const loadingClaim = { state: ENUM_CLAIM_STATE.None } as ClaimModel;
const EvidenceRender = ({ claim, isLoading }: { claim: ClaimModel; isLoading: boolean }) => {
const [evidence, setEvidence] = useState<string>();
const isMountedRef = useIsMountedRef();

export default function ClaimList({ questData, challengeDeposit, isLoading = false }: Props) {
const [claims, setClaims] = useState<ClaimModel[]>([loadingClaim]);
const [isLoadingState, setIsLoading] = useState(isLoading);
const { transaction } = useTransactionContext();
let isMounted = true;
useEffect(
() => () => {
isMounted = false;
},
[],
useEffect(() => {
if (!evidence) fetchEvidence();
}, [claim.evidenceIpfsHash, evidence]);

const fetchEvidence = async () => {
const evidenceResult = claim.evidenceIpfsHash
? await getObjectFromIpfs(claim.evidenceIpfsHash, ipfsTheGraph)
: 'No evidence';
if (isMountedRef.current) {
setEvidence(evidenceResult);
}
};

return (
<Outset gu16>
<TextFieldInput
id="evidence"
value={evidence}
isMarkDown
wide
label="Evidence of completion"
isLoading={isLoading || !evidence}
/>
</Outset>
);
};

useEffect(() => {
setIsLoading(isLoading);
}, [isLoading]);
export default function ClaimList({ questData, challengeDeposit, isLoading = false }: Props) {
const [claims, setClaims] = useState<ClaimModel[]>([]);
const [loadingClaim, setLoadingClaim] = useState(true);
const { transaction } = useTransactionContext();
const isMountedRef = useIsMountedRef();
const skeletonClaim = useMemo(() => {
const fakeClaim = {} as ClaimModel;
return [
<Claim
isLoading
claim={fakeClaim}
questData={questData}
challengeDeposit={challengeDeposit}
/>,
<EvidenceRender isLoading claim={fakeClaim} />,
];
}, []);

const accordionItems = useMemo(() => {
const items = claims.map((claim: ClaimModel) => [
<Claim
claim={claim}
challengeDeposit={challengeDeposit}
questData={questData}
isLoading={isLoading}
/>,
<EvidenceRender claim={claim} isLoading={isLoading} />,
]);
if (loadingClaim) {
items.unshift(skeletonClaim);
}
return items;
}, [claims, loadingClaim, questData.bounty, isLoading, challengeDeposit]);

useEffect(() => {
if (questData.address) {
Expand All @@ -83,42 +131,27 @@ export default function ClaimList({ questData, challengeDeposit, isLoading = fal

const fetchClaimsUntilNew = (claimsCount?: number) => {
if (!claimsCount) {
setClaims([loadingClaim, ...claims]);
setLoadingClaim(true);
claimsCount = claims.length;
}
setTimeout(async () => {
const results = await QuestService.fetchQuestClaims(questData);
if (!isMounted) return;
if (!isMountedRef.current) return;
if (results.length === claimsCount) {
fetchClaimsUntilNew(claimsCount);
} else {
setClaims(results);
setIsLoading(false);
fetchEvidenceOfCompletions(results);
setLoadingClaim(false);
}
}, 1000);
};

const fetchClaims = async () => {
setIsLoading(true);
setLoadingClaim(true);
const results = await QuestService.fetchQuestClaims(questData);
if (!isMounted) return;
if (!isMountedRef.current) return;
setClaims(results); // Fetch visible data
setIsLoading(false);
await fetchEvidenceOfCompletions(results);
};

const fetchEvidenceOfCompletions = async (result: ClaimModel[]) => {
const claimsRes = await Promise.all(
result.map(async (claim) => ({
...claim,
evidence: claim.evidenceIpfsHash
? await getObjectFromIpfsSafe(claim.evidenceIpfsHash)
: 'No evidence',
})),
);
if (!isMounted) return;
setClaims(claimsRes); // Fetch evidence wich is currently hidden in accordion
setLoadingClaim(false);
};

return (
Expand All @@ -127,27 +160,8 @@ export default function ClaimList({ questData, challengeDeposit, isLoading = fal
<HeaderStyled>Claims</HeaderStyled>
<HelpTooltip tooltip="A claim includes the proof of the quest's completion." />
</ClaimHeaderStyled>
{claims?.length || isLoadingState ? (
<Accordion
items={claims.map((claim: ClaimModel) => [
<Claim
claim={claim}
challengeDeposit={challengeDeposit}
isLoading={isLoadingState}
questData={questData}
/>,
<Outset gu8>
<TextFieldInput
id="evidence"
value={claim.evidence}
isMarkDown
wide
label="Evidence of completion"
isLoading={isLoading || !claim.evidence}
/>
</Outset>,
])}
/>
{claims?.length || loadingClaim ? (
<Accordion items={accordionItems} />
) : (
<BoxStyled>
<i>No claims</i>
Expand Down
91 changes: 57 additions & 34 deletions packages/react-app/src/components/claim.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useViewport } from '@1hive/1hive-ui';
import { ReactNode, useEffect, useState } from 'react';
import { useViewport, Timer } from '@1hive/1hive-ui';
import { ReactNode, useEffect, useMemo, useState } from 'react';
import { ENUM_CLAIM_STATE, ENUM_DISPUTE_STATES, ENUM_TRANSACTION_STATUS } from 'src/constants';
import { useTransactionContext } from 'src/contexts/transaction.context';
import { useWallet } from 'src/contexts/wallet.context';
import { useIsMountedRef } from 'src/hooks/use-mounted.hook';
import { useNow } from 'src/hooks/use-now.hook';
import { ClaimModel } from 'src/models/claim.model';
import { QuestModel } from 'src/models/quest.model';
import { TokenAmountModel } from 'src/models/token-amount.model';
import { compareCaseInsensitive } from 'src/utils/string.util';
import styled, { css } from 'styled-components';
import { AddressFieldInput } from './field-input/address-field-input';
import AmountFieldInput from './field-input/amount-field-input';
Expand Down Expand Up @@ -36,43 +39,28 @@ type Props = {
};

export default function Claim({ claim, isLoading, challengeDeposit, questData }: Props) {
const { walletAddress } = useWallet();
const { walletAddress, walletConnected } = useWallet();
const { transaction } = useTransactionContext();
const [state, setState] = useState(claim.state);
const { below } = useViewport();
const [waitForClose, setWaitForClose] = useState(false);
const [actionButton, setActionButton] = useState<ReactNode>();
const isMountedRef = useIsMountedRef();
const now = useNow();
const claimable = useMemo(() => !!claim.executionTimeMs && claim.executionTimeMs <= now, [now]);

useEffect(() => {
setState(claim.state);
}, [claim.state]);

useEffect(() => {
if (waitForClose || !state) return;
if (state === ENUM_CLAIM_STATE.Scheduled) {
if (walletAddress === claim.playerAddress) {
setActionButton(
<ExecuteClaimModal
claim={claim}
questTotalBounty={questData.bounty}
onClose={onActionClose}
/>,
);
} else {
setActionButton(
<ChallengeModal
claim={claim}
challengeDeposit={challengeDeposit}
onClose={onActionClose}
/>,
);
}
} else if (state === ENUM_CLAIM_STATE.Challenged) {
setActionButton(<ResolveChallengeModal claim={claim} onClose={onActionClose} />);
} else {
setActionButton(undefined);
}
}, [state, walletAddress, waitForClose]);
const timer = useMemo(
() => !claimable && claim.executionTimeMs && <Timer end={new Date(claim.executionTimeMs)} />,
[claim.executionTimeMs, claimable],
);

const onActionClose = () => {
setWaitForClose(false);
};

useEffect(() => {
// If tx completion impact Claims, update them
Expand Down Expand Up @@ -100,17 +88,52 @@ export default function Claim({ claim, isLoading, challengeDeposit, questData }:
setState(ENUM_CLAIM_STATE.Executed);
break;
case 'ClaimChallenge':
setState(ENUM_CLAIM_STATE.Challenged);
setTimeout(() => {
setState(ENUM_CLAIM_STATE.Challenged);
}, 1000); // Wait for subgrapph to index challenge event
break;
default:
}
}
}
}, [transaction?.status, transaction?.type, transaction?.[0], claim.container]);

const onActionClose = () => {
setWaitForClose(false);
};
useEffect(() => {
if (waitForClose || !state || !isMountedRef.current) return;
if (state === ENUM_CLAIM_STATE.Scheduled) {
if (compareCaseInsensitive(walletAddress, claim.playerAddress) || claimable) {
setActionButton(
<>
<ExecuteClaimModal
claim={claim}
questTotalBounty={questData.bounty}
onClose={onActionClose}
claimable={claimable}
/>
{timer}
</>,
);
return;
}
setActionButton(
<>
<ChallengeModal
claim={claim}
challengeDeposit={challengeDeposit}
onClose={onActionClose}
/>
{timer}
</>,
);
return;
}

if (state === ENUM_CLAIM_STATE.Challenged) {
setActionButton(<ResolveChallengeModal claim={claim} onClose={onActionClose} />);
return;
}
setActionButton(undefined);
}, [state, walletAddress, waitForClose, claim, questData.bounty, challengeDeposit, claimable]);

return (
<div className="wide">
Expand Down Expand Up @@ -148,7 +171,7 @@ export default function Claim({ claim, isLoading, challengeDeposit, questData }:
isLoading={isLoading || state === ENUM_CLAIM_STATE.None}
/>
)}
{walletAddress && actionButton}
{walletConnected && state && actionButton}
</ChildSpacer>
</Outset>
</div>
Expand Down
6 changes: 3 additions & 3 deletions packages/react-app/src/components/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getDashboardInfo } from 'src/services/quest.service';
import { GUpx } from 'src/utils/style.util';
import styled from 'styled-components';
import { FieldInput } from './field-input/field-input';
import QuestModal from './modals/quest-modal';
import QuestModal from './modals/create-quest-modal';
import { ChildSpacer } from './utils/spacer-util';

// #region StyledComponents
Expand Down Expand Up @@ -47,7 +47,7 @@ const LabelStyled = styled.div`
export default function Dashboard() {
const theme = useTheme();
const [dashboardModel, setDashboardModel] = useState<DashboardModel>();
const { walletAddress } = useWallet();
const { walletConnected } = useWallet();

useEffect(() => {
let isSubscribed = true;
Expand Down Expand Up @@ -84,7 +84,7 @@ export default function Dashboard() {
<TextStyled theme={theme}>{dashboardModel?.questCount.toLocaleString()}</TextStyled>
</FieldInput>
<SpacerStyled>
{walletAddress && <QuestModal questMode={ENUM_QUEST_VIEW_MODE.Create} />}
{walletConnected && <QuestModal questMode={ENUM_QUEST_VIEW_MODE.Create} />}
</SpacerStyled>
</ChildSpacer>
</BoxStyled>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { isAddress } from 'src/utils/web3.utils';
import styled from 'styled-components';
import { useCopyToClipboard } from 'src/hooks/use-copy-to-clipboard.hook';
import { NETWORK_TOKENS } from 'src/constants';
import { useIsMountedRef } from 'src/hooks/use-mounted.hook';
import { FieldInput } from './field-input';
import { ConditionalWrapper } from '../utils/util';

Expand Down Expand Up @@ -189,7 +190,7 @@ function AmountFieldInput({
showUsd = false,
error,
}: Props) {
let mounted = true;
const isMountedRef = useIsMountedRef();
const { networkId } = getNetwork();
const [tokens, setTokens] = useState<TokenModel[]>([]);
const [searchTerm, setSearchTerm] = useState<string>();
Expand All @@ -210,9 +211,6 @@ function AmountFieldInput({

useEffect(() => {
if (isEdit && !availableTokens.length) fetchAvailableTokens();
return () => {
mounted = false;
};
}, [isEdit]);

useEffect(() => {
Expand All @@ -226,7 +224,8 @@ function AmountFieldInput({
setTokens([]);
getTokenInfo(searchTerm)
.then((tokenInfo) => {
if (typeof tokenInfo !== 'string') if (tokenInfo && mounted) setTokens([tokenInfo]);
if (typeof tokenInfo !== 'string')
if (tokenInfo && isMountedRef.current) setTokens([tokenInfo]);
})
.catch(Logger.exception);
} else {
Expand Down Expand Up @@ -261,7 +260,7 @@ function AmountFieldInput({
const fetchAvailableTokens = async () => {
const networkDefaultTokens = (NETWORK_TOKENS[networkId] as TokenModel[]) ?? [];
const questsUsedTokens = await fetchRewardTokens();
if (mounted) {
if (isMountedRef.current) {
setAvailableTokens(
arrayDistinctBy([...networkDefaultTokens, ...questsUsedTokens], (x) => x.token),
);
Expand Down
Loading