From e839152abafd519f9f9b361efdd7d124728bb868 Mon Sep 17 00:00:00 2001 From: Rico Date: Wed, 29 Jun 2022 17:23:42 +0800 Subject: [PATCH] react-app: Integrate paginated proposal votes and deposits to detail screen API refs #128 --- .../ProposalDetailScreen.graphql | 106 ++++++- .../ProposalDetailScreen.tsx | 7 +- .../ProposalDetailScreenAPI.ts | 271 +++++++++++++++++- .../ProposalDetailScreenModel.ts | 29 +- 4 files changed, 405 insertions(+), 8 deletions(-) diff --git a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.graphql b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.graphql index 696f533f9..cd8499281 100644 --- a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.graphql +++ b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.graphql @@ -1,3 +1,54 @@ +fragment ProposalDetailVoterDepositorValidator on Validator { + id + operatorAddress + moniker + identity + securityContact +} + +fragment ProposalDetailVoterDepositorAddress on StringObject { + value +} + +fragment ProposalDetailProposalVoteVoter on ProposalVoter { + ... on StringObject { + ...ProposalDetailVoterDepositorAddress + } + ... on Validator { + ...ProposalDetailVoterDepositorValidator + } +} + +fragment ProposalDetailProposalDepositDepositor on ProposalDepositor { + ... on StringObject { + ...ProposalDetailVoterDepositorAddress + } + ... on Validator { + ...ProposalDetailVoterDepositorValidator + } +} + +fragment ProposalDetailProposalVote on ProposalVote { + id + proposalId + voter { + ...ProposalDetailProposalVoteVoter + } + option +} + +fragment ProposalDetailProposalDeposit on ProposalDeposit { + id + proposalId + depositor { + ...ProposalDetailProposalDepositDepositor + } + amount { + denom + amount + } +} + fragment ProposalDetailScreenProposal on Proposal { id proposalId @@ -24,9 +75,62 @@ fragment ProposalDetailScreenProposal on Proposal { } myReaction + + votes(input: $voteInput) { + totalCount + edges { + node { + ...ProposalDetailProposalVote + } + } + } + deposits(input: $depositInput) { + totalCount + edges { + node { + ...ProposalDetailProposalDeposit + } + } + } +} + +query ProposalDetailVotesPanelQuery( + $proposalId: ID! + $input: QueryProposalVotesInput! +) { + proposalByID(id: $proposalId) { + votes(input: $input) { + totalCount + edges { + node { + ...ProposalDetailProposalVote + } + } + } + } +} + +query ProposalDetailDepositsPanelQuery( + $proposalId: ID! + $input: QueryProposalDepositsInput! +) { + proposalByID(id: $proposalId) { + deposits(input: $input) { + totalCount + edges { + node { + ...ProposalDetailProposalDeposit + } + } + } + } } -query ProposalDetailScreenQuery($id: ID!) { +query ProposalDetailScreenQuery( + $id: ID! + $voteInput: QueryProposalVotesInput! + $depositInput: QueryProposalDepositsInput! +) { proposalByID(id: $id) { ...ProposalDetailScreenProposal } diff --git a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.tsx b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.tsx index a35ee495e..1bdc7d52b 100644 --- a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.tsx +++ b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.tsx @@ -19,9 +19,14 @@ import ProposalDescription from "./ProposalDescription"; import { useProposalQuery } from "./ProposalDetailScreenAPI"; import { ProposalData } from "./ProposalData"; +const PROPOSAL_DETAIL_DATA_PAGE_SIZE = 5; + const ProposalDetailScreen: React.FC = () => { const { id } = useParams(); - const { fetch, requestState } = useProposalQuery(); + const { fetch, requestState } = useProposalQuery( + 0, + PROPOSAL_DETAIL_DATA_PAGE_SIZE + ); const { translate } = useLocale(); const reactionAPI = useReactionAPI(); const navigate = useNavigate(); diff --git a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreenAPI.ts b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreenAPI.ts index 0099f1cc5..abb22b5fc 100644 --- a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreenAPI.ts +++ b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreenAPI.ts @@ -1,16 +1,31 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import BigNumber from "bignumber.js"; import { differenceInDays } from "date-fns"; import { ProposalDetailScreenQuery, ProposalDetailScreenQueryQuery, ProposalDetailScreenQueryQueryVariables, + ProposalVoteSort, + ProposalDetailVotesPanelQuery, + ProposalDetailVotesPanelQueryQuery, + ProposalDetailVotesPanelQueryQueryVariables, + ProposalDetailDepositsPanelQuery, + ProposalDetailDepositsPanelQueryQuery, + ProposalDetailDepositsPanelQueryQueryVariables, + ProposalDepositSort, } from "../../generated/graphql"; import { useLazyGraphQLQuery } from "../../hooks/graphql"; import { mapRequestData, RequestState } from "../../models/RequestState"; import { convertMinimalTokenToToken } from "../../utils/coin"; import { getReactionType } from "../reactions/ReactionModel"; -import { Proposal, ReactionItem } from "./ProposalDetailScreenModel"; +import { useQueryClient } from "../../providers/QueryClientProvider"; +import { ConnectionStatus, useWallet } from "../../providers/WalletProvider"; +import { + PaginatedProposalVotes, + PaginatedProposalDeposits, + Proposal, + ReactionItem, +} from "./ProposalDetailScreenModel"; const calculateTurnout = (tallyResult: Proposal["tallyResult"]) => { if (!tallyResult) { @@ -26,10 +41,214 @@ const calculateTurnout = (tallyResult: Proposal["tallyResult"]) => { return turnout.toNumber(); }; -export function useProposalQuery(): { +interface UseProposalVotesQuery { + (proposalId: string, initialOffset: number, pageSize: number): { + requestState: RequestState; + fetch: (variables: { + proposalId: string; + first: number; + after: number; + pinnedValidators: string[]; + order: ProposalVoteSort; + }) => void; + }; +} + +export const useProposalVotesQuery: UseProposalVotesQuery = ( + proposalId, + initialOffset, + pageSize +) => { + const { query } = useQueryClient(); + const wallet = useWallet(); + const [delegatedValidators, setDelegatedValidators] = useState([]); + const [fetch, { requestState }] = useLazyGraphQLQuery< + ProposalDetailVotesPanelQueryQuery, + ProposalDetailVotesPanelQueryQueryVariables + >(ProposalDetailVotesPanelQuery, { + variables: { + proposalId, + input: { + after: initialOffset, + first: pageSize, + order: {}, + }, + }, + fetchPolicy: "cache-and-network", + nextFetchPolicy: "cache-first", + }); + + const callFetch = useCallback( + ({ + first, + after, + order, + }: { + first: number; + after: number; + order: ProposalVoteSort; + }) => { + // Errors are handled by the requestState + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetch({ + variables: { + proposalId, + input: { + first, + after, + order, + pinnedValidators: delegatedValidators, + }, + }, + }); + }, + [fetch, proposalId, delegatedValidators] + ); + + const data = useMemo(() => { + return mapRequestData< + ProposalDetailVotesPanelQueryQuery, + PaginatedProposalVotes + >(requestState, (r) => ({ + proposalVotes: r.proposalByID?.votes.edges.map((v) => v.node) ?? [], + totalCount: r.proposalByID?.votes.totalCount ?? 0, + })); + }, [requestState]); + + useEffect(() => { + if (wallet.status !== ConnectionStatus.Connected) { + return; + } + + query.staking + .delegatorDelegations(wallet.account.address) + .then((delegations) => { + setDelegatedValidators( + delegations.delegationResponses + .filter((r) => r.delegation != null) + .map((r) => r.delegation!.validatorAddress) + ); + }) + .catch((err) => { + console.error("Failed to fetch user delegations = ", err); + }); + }, [query, wallet]); + + return { + requestState: data, + fetch: callFetch, + }; +}; + +interface UseProposalDepositsQuery { + (proposalId: string, initialOffset: number, pageSize: number): { + requestState: RequestState; + fetch: (variables: { + proposalId: string; + first: number; + after: number; + pinnedValidators: string[]; + order: ProposalDepositSort; + }) => void; + }; +} + +export const useProposalDepositsQuery: UseProposalDepositsQuery = ( + proposalId, + initialOffset, + pageSize +) => { + const { query } = useQueryClient(); + const wallet = useWallet(); + const [delegatedValidators, setDelegatedValidators] = useState([]); + const [fetch, { requestState }] = useLazyGraphQLQuery< + ProposalDetailDepositsPanelQueryQuery, + ProposalDetailDepositsPanelQueryQueryVariables + >(ProposalDetailDepositsPanelQuery, { + variables: { + proposalId, + input: { + after: initialOffset, + first: pageSize, + order: {}, + }, + }, + fetchPolicy: "cache-and-network", + nextFetchPolicy: "cache-first", + }); + + const callFetch = useCallback( + ({ + first, + after, + order, + }: { + first: number; + after: number; + order: ProposalDepositSort; + }) => { + // Errors are handled by the requestState + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetch({ + variables: { + proposalId, + input: { + first, + after, + order, + pinnedValidators: delegatedValidators, + }, + }, + }); + }, + [fetch, proposalId, delegatedValidators] + ); + + const data = useMemo(() => { + return mapRequestData< + ProposalDetailDepositsPanelQueryQuery, + PaginatedProposalDeposits + >(requestState, (r) => ({ + proposalDeposits: r.proposalByID?.deposits.edges.map((v) => v.node) ?? [], + totalCount: r.proposalByID?.deposits.totalCount ?? 0, + })); + }, [requestState]); + + useEffect(() => { + if (wallet.status !== ConnectionStatus.Connected) { + return; + } + + query.staking + .delegatorDelegations(wallet.account.address) + .then((delegations) => { + setDelegatedValidators( + delegations.delegationResponses + .filter((r) => r.delegation != null) + .map((r) => r.delegation!.validatorAddress) + ); + }) + .catch((err) => { + console.error("Failed to fetch user delegations = ", err); + }); + }, [query, wallet]); + + return { + requestState: data, + fetch: callFetch, + }; +}; + +export function useProposalQuery( + initialProposalDataOffset: number, + proposalDataPageSize: number +): { requestState: RequestState; fetch: (id: string) => void; } { + const { query } = useQueryClient(); + const wallet = useWallet(); + const [delegatedValidators, setDelegatedValidators] = useState([]); const [fetch, { requestState }] = useLazyGraphQLQuery< ProposalDetailScreenQueryQuery, ProposalDetailScreenQueryQueryVariables @@ -44,10 +263,27 @@ export function useProposalQuery(): { fetch({ variables: { id: `proposal_${id}`, + voteInput: { + first: proposalDataPageSize, + after: initialProposalDataOffset, + order: {}, + pinnedValidators: delegatedValidators, + }, + depositInput: { + first: proposalDataPageSize, + after: initialProposalDataOffset, + order: {}, + pinnedValidators: delegatedValidators, + }, }, }); }, - [fetch] + [ + fetch, + delegatedValidators, + initialProposalDataOffset, + proposalDataPageSize, + ] ); const data = useMemo(() => { @@ -90,11 +326,38 @@ export function useProposalQuery(): { count: r.count, })) .filter((r): r is ReactionItem => r.type != null), + votes: { + proposalVotes: r.proposalByID.votes.edges.map((v) => v.node), + totalCount: r.proposalByID.votes.totalCount, + }, + deposits: { + proposalDeposits: r.proposalByID.deposits.edges.map((d) => d.node), + totalCount: r.proposalByID.deposits.totalCount, + }, }; } ); }, [requestState]); + useEffect(() => { + if (wallet.status !== ConnectionStatus.Connected) { + return; + } + + query.staking + .delegatorDelegations(wallet.account.address) + .then((delegations) => { + setDelegatedValidators( + delegations.delegationResponses + .filter((r) => r.delegation != null) + .map((r) => r.delegation!.validatorAddress) + ); + }) + .catch((err) => { + console.error("Failed to fetch user delegations = ", err); + }); + }, [query, wallet]); + return { requestState: data, fetch: callFetch, diff --git a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreenModel.ts b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreenModel.ts index a78c7197f..d45fe4ea1 100644 --- a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreenModel.ts +++ b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreenModel.ts @@ -1,4 +1,10 @@ -import { ProposalDetailScreenProposalFragment } from "../../generated/graphql"; +import { + ProposalDetailScreenProposalFragment, + ProposalDetailProposalVoteFragment as ProposalVoteFragment, + ProposalDetailProposalDepositFragment as ProposalDepositFragment, + ProposalDetailProposalVoteVoterFragment as ProposalVoteVoterFragment, + ProposalDetailProposalDepositDepositorFragment as ProposalDepositDepositorFragment, +} from "../../generated/graphql"; import { ReactionType } from "../reactions/ReactionModel"; export interface ReactionItem { @@ -6,7 +12,10 @@ export interface ReactionItem { count: number; } export interface Proposal - extends Omit { + extends Omit< + ProposalDetailScreenProposalFragment, + "reactions" | "votes" | "deposits" + > { /** * Turn out rate in percentages */ @@ -18,4 +27,20 @@ export interface Proposal remainingVotingDays: number | null; reactions: ReactionItem[]; + votes: PaginatedProposalVotes; + deposits: PaginatedProposalDeposits; +} + +export type ProposalVoteVoter = ProposalVoteVoterFragment; +export type ProposalDepositDepositor = ProposalDepositDepositorFragment; +export type ProposalVote = ProposalVoteFragment; +export type ProposalDeposit = ProposalDepositFragment; +export interface PaginatedProposalVotes { + proposalVotes: ProposalVoteFragment[]; + totalCount: number; +} + +export interface PaginatedProposalDeposits { + proposalDeposits: ProposalDepositFragment[]; + totalCount: number; }