diff --git a/packages/next-common/hooks/collectives/useRankedCollectiveMinRank.js b/packages/next-common/hooks/collectives/useRankedCollectiveMinRank.js
new file mode 100644
index 0000000000..d337e58e17
--- /dev/null
+++ b/packages/next-common/hooks/collectives/useRankedCollectiveMinRank.js
@@ -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);
+}
diff --git a/packages/next-common/hooks/fellowship/useFellowshipMemberRank.js b/packages/next-common/hooks/fellowship/useFellowshipMemberRank.js
index d11194cfaf..91835be1d6 100644
--- a/packages/next-common/hooks/fellowship/useFellowshipMemberRank.js
+++ b/packages/next-common/hooks/fellowship/useFellowshipMemberRank.js
@@ -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;
+ }
}
diff --git a/packages/next-common/utils/hooks/collectives/useCollectiveEligibleVoters.js b/packages/next-common/utils/hooks/collectives/useCollectiveEligibleVoters.js
new file mode 100644
index 0000000000..31b32da301
--- /dev/null
+++ b/packages/next-common/utils/hooks/collectives/useCollectiveEligibleVoters.js
@@ -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,
+ };
+}
diff --git a/packages/next-common/utils/hooks/fellowship/useFellowshipVotes.js b/packages/next-common/utils/hooks/fellowship/useFellowshipVotes.js
index 28744c17e4..75915d6736 100644
--- a/packages/next-common/utils/hooks/fellowship/useFellowshipVotes.js
+++ b/packages/next-common/utils/hooks/fellowship/useFellowshipVotes.js
@@ -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);
}
@@ -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);
@@ -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 }));
@@ -99,5 +98,5 @@ export default function useFellowshipVotes(pollIndex, blockHeight) {
dispatch(clearFellowshipVotes());
dispatch(clearFellowshipVotesTrigger());
};
- }, [api, pollIndex, blockHeight, votesTrigger, dispatch]);
+ }, [api, pollIndex, indexer, votesTrigger, dispatch]);
}
diff --git a/packages/next/components/fellowship/referendum/sidebar/index.js b/packages/next/components/fellowship/referendum/sidebar/index.js
index b5a76cb863..4ffba9b0e2 100644
--- a/packages/next/components/fellowship/referendum/sidebar/index.js
+++ b/packages/next/components/fellowship/referendum/sidebar/index.js
@@ -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"),
@@ -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";
diff --git a/packages/next/components/fellowship/referendum/sidebar/tally/allVotes.js b/packages/next/components/fellowship/referendum/sidebar/tally/allVotes.js
index c495b84c76..bc2151292d 100644
--- a/packages/next/components/fellowship/referendum/sidebar/tally/allVotes.js
+++ b/packages/next/components/fellowship/referendum/sidebar/tally/allVotes.js
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, memo } from "react";
import { Button } from "components/gov2/sidebar/tally/styled";
import { useSelector } from "react-redux";
import {
@@ -6,10 +6,11 @@ import {
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);
@@ -20,11 +21,13 @@ export default function AllVotes() {
{showAllVotes && (