Skip to content

Commit

Permalink
[Fellowship] Show eligible voters on fellowship referendum detail page,
Browse files Browse the repository at this point in the history
#5236 (#5266)

* init EligibleVoters structure, #5236

* init No Data container, #5236

* add List Table, #5239

* only collectives show EligibleVoters, #5236

* voted table data, #5239

* useCollectiveEligibleVoters, #5239

* remove useless key, #5239

* modify scroll container, #5239

* optimize columns style, #5239

* remove useless code, #5236

* Add votes for eligible voters, #5236 (#5290)

* Add votes to eligible voters, #5236

* Improve header prompt text, #5236

* Fix eligible voter vote result, #5236

* add rank into allVotes, #5236

* Refactor, #5236

* add memo for allVotes & eligibleVoters, #5236

* revert code, #5236

* revert code, #5236

---------

Co-authored-by: Yongfeng LI <[email protected]>
  • Loading branch information
leocs2417 and wliyongfeng authored Dec 26, 2024
1 parent 79fb8ec commit 25f9311
Show file tree
Hide file tree
Showing 14 changed files with 390 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// used on ranked collective referendum detail page
import { useTrack } from "next-common/context/post/gov2/track";
import { useRankedCollectivePallet } from "next-common/context/collectives/collectives";
import { getMinRankOfClass } from "next-common/context/post/fellowship/useMaxVoters";

export default function useRankedCollectiveMinRank() {
const { id: trackId } = useTrack();
const collectivePallet = useRankedCollectivePallet();
return getMinRankOfClass(trackId, collectivePallet);
}
26 changes: 7 additions & 19 deletions packages/next-common/hooks/fellowship/useFellowshipMemberRank.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import { useContextApi } from "next-common/context/api";
import { useEffect, useState } from "react";
import useSubStorage from "next-common/hooks/common/useSubStorage";

export function useFellowshipMemberRank(
address,
pallet = "fellowshipCollective",
) {
const [rank, setRank] = useState(null);
const api = useContextApi();

useEffect(() => {
if (!api || !api.query[pallet]) {
return;
}

api.query[pallet].members(address).then((resp) => {
if (!resp.isNone) {
const json = resp.value.toJSON();
setRank(json.rank);
}
});
}, [api, address, pallet]);

return rank;
const { result } = useSubStorage(pallet, "members", [address]);
if (result && result.isSome) {
return result.unwrap().rank.toNumber();
} else {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useContextApi } from "next-common/context/api";
import { useReferendumVotingFinishIndexer } from "next-common/context/post/referenda/useReferendumVotingFinishHeight";
import { useEffect, useState } from "react";
import { groupBy, orderBy } from "lodash-es";
import { normalizeRankedCollectiveEntries } from "next-common/utils/rankedCollective/normalize";
import { useSelector } from "react-redux";
import {
fellowshipVotesSelector,
isLoadingFellowshipVotesSelector,
} from "next-common/store/reducers/fellowship/votes";
import useRankedCollectiveMinRank from "next-common/hooks/collectives/useRankedCollectiveMinRank";

async function queryFellowshipCollectiveMembers(api, blockHash) {
let blockApi = api;
if (blockHash) {
blockApi = await api.at(blockHash);
}
return await blockApi.query.fellowshipCollective.members.entries();
}

function getMemberVotes(rank, minRank) {
if (rank < minRank) {
throw new Error(`Rank ${rank} is too low, and minimum rank is ${minRank}`);
}
const excess = rank - minRank;
const v = excess + 1;
return Math.floor((v * (v + 1)) / 2);
}

export default function useCollectiveEligibleVoters() {
const api = useContextApi();

const [voters, setVoters] = useState({
votedMembers: [],
unVotedMembers: [],
});
const [loading, setLoading] = useState(true);

const votingFinishIndexer = useReferendumVotingFinishIndexer();
const minRank = useRankedCollectiveMinRank();

const { allAye, allNay } = useSelector(fellowshipVotesSelector);
const isLoadingVotes = useSelector(isLoadingFellowshipVotesSelector);

useEffect(() => {
if (!api || isLoadingVotes) {
return;
}

(async () => {
setLoading(true);
try {
const memberEntries = await queryFellowshipCollectiveMembers(
api,
votingFinishIndexer?.blockHash,
);
const normalizedMembers =
normalizeRankedCollectiveEntries(memberEntries);
const voters = normalizedMembers.filter(
(member) => member?.rank >= minRank,
);
const sortedVoters = orderBy(voters, ["rank"], ["desc"]);
const votersWithPower = sortedVoters.map((m) => ({
...m,
votes: getMemberVotes(m.rank, minRank),
}));

const allVotes = [...allAye, ...allNay];
const votedSet = new Set(allVotes.map((i) => i.address));
const { true: votedMembers, false: unVotedMembers } = groupBy(
votersWithPower,
(member) => votedSet.has(member.address),
);

setVoters({
votedMembers: votedMembers.map((m) => {
const vote = allVotes.find((i) => i.address === m.address);
return {
...m,
votes: vote.votes,
isAye: vote.isAye,
};
}),
unVotedMembers,
});
} catch (error) {
console.error("Failed to fetch fellowship voters:", error);
} finally {
setLoading(false);
}
})();
}, [api, votingFinishIndexer, isLoadingVotes, allAye, allNay, minRank]);

return {
...voters,
isLoading: loading,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ function normalizeVotingRecord(optionalRecord) {
};
}

async function query(api, targetPollIndex, blockHeight) {
async function query(api, targetPollIndex, blockHash) {
let blockApi = api;
if (blockHeight) {
const blockHash = await api.rpc.chain.getBlockHash(blockHeight);
if (blockHash) {
blockApi = await api.at(blockHash);
}

Expand All @@ -74,7 +73,7 @@ async function query(api, targetPollIndex, blockHeight) {
return normalized;
}

export default function useFellowshipVotes(pollIndex, blockHeight) {
export default function useFellowshipVotes(pollIndex, indexer) {
const api = useContextApi();
const dispatch = useDispatch();
const votesTrigger = useSelector(fellowshipVotesTriggerSelector);
Expand All @@ -88,7 +87,7 @@ export default function useFellowshipVotes(pollIndex, blockHeight) {
dispatch(setIsLoadingFellowshipVotes(true));
}

query(api, pollIndex, blockHeight)
query(api, pollIndex, indexer?.blockHash)
.then((votes) => {
const [allAye = [], allNay = []] = partition(votes, (v) => v.isAye);
dispatch(setFellowshipVotes({ allAye, allNay }));
Expand All @@ -99,5 +98,5 @@ export default function useFellowshipVotes(pollIndex, blockHeight) {
dispatch(clearFellowshipVotes());
dispatch(clearFellowshipVotesTrigger());
};
}, [api, pollIndex, blockHeight, votesTrigger, dispatch]);
}, [api, pollIndex, indexer, votesTrigger, dispatch]);
}
11 changes: 2 additions & 9 deletions packages/next/components/fellowship/referendum/sidebar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ import { isNil, isUndefined, noop } from "lodash-es";
import useRealAddress from "next-common/utils/hooks/useRealAddress";
import useSubCollectiveRank from "next-common/hooks/collectives/useSubCollectiveRank";
import { useRankedCollectivePallet } from "next-common/context/collectives/collectives";
import { getMinRankOfClass } from "next-common/context/post/fellowship/useMaxVoters";
import { useTrack } from "next-common/context/post/gov2/track";
import Tooltip from "next-common/components/tooltip";
import dynamic from "next/dynamic";
import AllSpendsRequest from "./request/allSpendsRequest";
import useRankedCollectiveMinRank from "next-common/hooks/collectives/useRankedCollectiveMinRank";

const MyCollectiveVote = dynamic(
() => import("next-common/components/collectives/referenda/myCollectiveVote"),
Expand All @@ -28,17 +27,11 @@ const MyCollectiveVote = dynamic(
},
);

function useMinRank() {
const { id: trackId } = useTrack();
const collectivePallet = useRankedCollectivePallet();
return getMinRankOfClass(trackId, collectivePallet);
}

function CollectiveVote({ onClick = noop }) {
const address = useRealAddress();
const collectivePallet = useRankedCollectivePallet();
const { rank, loading } = useSubCollectiveRank(address, collectivePallet);
const minRank = useMinRank();
const minRank = useRankedCollectiveMinRank();
const disabled = !address || loading || isNil(rank) || rank < minRank;
const text = loading ? "Checking permissions" : "Vote";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { useState } from "react";
import { useState, memo } from "react";
import { Button } from "components/gov2/sidebar/tally/styled";
import { useSelector } from "react-redux";
import {
fellowshipVotesSelector,
isLoadingFellowshipVotesSelector,
} from "next-common/store/reducers/fellowship/votes";
import dynamicPopup from "next-common/lib/dynamic/popup";
import { orderBy } from "lodash-es";

const AllVotesPopup = dynamicPopup(() => import("./allVotesPopup"));

export default function AllVotes() {
function AllVotes() {
const [showAllVotes, setShowAllVotes] = useState(false);
const { allAye, allNay } = useSelector(fellowshipVotesSelector);
const isLoadingVotes = useSelector(isLoadingFellowshipVotesSelector);
Expand All @@ -20,11 +21,13 @@ export default function AllVotes() {
{showAllVotes && (
<AllVotesPopup
setShowVoteList={setShowAllVotes}
allAye={allAye}
allNay={allNay}
allAye={orderBy(allAye, ["votes"], ["desc"])}
allNay={orderBy(allNay, ["votes"], ["desc"])}
isLoadingVotes={isLoadingVotes}
/>
)}
</>
);
}

export default memo(AllVotes);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Pagination from "next-common/components/pagination";
import PopupListWrapper from "next-common/components/styled/popupListWrapper";
import AddressUser from "next-common/components/user/addressUser";
import DataList from "next-common/components/dataList";
import { FellowshipRankInfo } from "../eligibleVoters/columns";

export default function VotesPopup({
setShowVoteList,
Expand Down Expand Up @@ -59,6 +60,7 @@ export default function VotesPopup({

function VotesList({ items = [], loading }) {
const columns = [
{ name: "", style: { width: 40, textAlign: "center" } },
{
name: "ACCOUNT",
style: { minWidth: 176, textAlign: "left" },
Expand All @@ -70,7 +72,12 @@ function VotesList({ items = [], loading }) {
];

const rows = items.map((item) => {
const row = [
return [
<FellowshipRankInfo
key="rank"
address={item.address}
className="min-w-5"
/>,
<AddressUser
key="user"
add={item.address}
Expand All @@ -80,8 +87,6 @@ function VotesList({ items = [], loading }) {
/>,
item.votes,
];

return row;
});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import AddressUser from "next-common/components/user/addressUser";
import FellowshipRank from "next-common/components/fellowship/rank";
import { isNil } from "lodash-es";
import { useFellowshipMemberRank } from "next-common/hooks/fellowship/useFellowshipMemberRank";
import { useRankedCollectivePallet } from "next-common/context/collectives/collectives";

export function FellowshipRankInfo({ address }) {
const collectivePallet = useRankedCollectivePallet();
const rank = useFellowshipMemberRank(address, collectivePallet);

return <FellowshipRank rank={rank} />;
}

const rankColumn = {
key: "rank",
className: "w-5",
render: (_, row) => (
<div className="w-5 h-5">
<FellowshipRankInfo address={row.address} />
</div>
),
};

const addressColumn = {
key: "address",
className: "text-left min-w-[140px]",
render: (address) => <AddressUser add={address} />,
};

const ayeColumn = {
key: "isAye",
className: "text-left w-[140px]",
render: (isAye) => {
if (isNil(isAye)) {
return <span className="text-textDisabled">-</span>;
}

return isAye ? (
<span className="text-green500">Aye</span>
) : (
<span className="text-red500">Nay</span>
);
},
};

const votesColumn = {
key: "votes",
className: "text-right w-[140px] align-right flex-end",
render: (votes) => <span>{votes}</span>,
};

const columns = [rankColumn, addressColumn, ayeColumn, votesColumn];

export default columns;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { memo, useState } from "react";
import { Button } from "components/gov2/sidebar/tally/styled";
import dynamicPopup from "next-common/lib/dynamic/popup";

const EligibleVotersPopup = dynamicPopup(() => import("./popup"));

function EligibleVoters() {
const [showEligibleVoters, setShowEligibleVoters] = useState(false);

return (
<>
<Button onClick={() => setShowEligibleVoters(true)}>
Eligible Voters
</Button>
{showEligibleVoters && (
<EligibleVotersPopup onClose={() => setShowEligibleVoters(false)} />
)}
</>
);
}

export default memo(EligibleVoters);
Loading

0 comments on commit 25f9311

Please sign in to comment.