From 36b255942b07bfc85a648f8c0a871dd8e4b8f147 Mon Sep 17 00:00:00 2001 From: AdityaKhatri Date: Mon, 6 Jul 2026 11:05:55 +0545 Subject: [PATCH] feat: update group fetching logic --- app/(auth)/project/[id]/map/index.tsx | 138 ++++++++++++++++++++------ 1 file changed, 109 insertions(+), 29 deletions(-) diff --git a/app/(auth)/project/[id]/map/index.tsx b/app/(auth)/project/[id]/map/index.tsx index 5d97918..d7a887c 100644 --- a/app/(auth)/project/[id]/map/index.tsx +++ b/app/(auth)/project/[id]/map/index.tsx @@ -17,15 +17,20 @@ import { isNotDefined, } from '@togglecorp/fujs'; import { - limitToLast, orderByChild, query, + startAfter, } from 'firebase/database'; import BlockListView from '@/components/BlockListView'; +import Button from '@/components/Button'; +import Icon from '@/components/Icon'; import Page from '@/components/Page'; import Text from '@/components/Text'; +import useAuth from '@/hooks/useAuth'; +import useFirebaseDatabase from '@/hooks/useFirebaseDatabase'; import useFirebaseDatabaseList from '@/hooks/useFirebaseDatabaseList'; +import useTheme from '@/hooks/useTheme'; import { firebaseRef } from '@/utils/firebase'; const styles = StyleSheet.create({ @@ -34,9 +39,16 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + completedText: { + textAlign: 'center', + }, }); + function MapProjectIndex() { const navigation = useNavigation(); + const theme = useTheme(); + const { user } = useAuth(); + const userId = user?.uid; const { id: projectId, @@ -50,76 +62,144 @@ function MapProjectIndex() { previousGroupId: string; }>(); - const leastMappedGroupQuery = useMemo( + // This screen only picks a group; a direct taskGroupId means that's done. + const skipSelection = isDefined(taskGroupId); + + // Groups that still need contributions (requiredCount > 0) — i.e. fewer than + // `verificationNumber` unique users have mapped them yet. Once a group has + // enough contributors its requiredCount is 0 and it drops out here. + const availableGroupsQuery = useMemo( () => query( firebaseRef(`v2/groups/${projectId}`), orderByChild('requiredCount'), - limitToLast(10), + startAfter(0), ), [projectId], ); const { - list: leastMappedTaskGroups, - pending, - } = useFirebaseDatabaseList<{ groupId: string }>({ - query: leastMappedGroupQuery, - skip: isDefined(taskGroupId), + list: availableGroups, + pending: groupsPending, + } = useFirebaseDatabaseList<{ requiredCount?: number }>({ + query: availableGroupsQuery, + skip: skipSelection, }); - useEffect(() => { - if (isDefined(taskGroupId)) { - return; - } - if (isNotDefined(leastMappedTaskGroups) || leastMappedTaskGroups.length === 0) { - return; - } + // Groups this user has already contributed to in this project (keyed by + // groupId), so we never hand them the same group twice. + const contributionsQuery = useMemo( + () => (isDefined(userId) + ? firebaseRef(`v2/users/${userId}/contributions/${projectId}`) + : undefined), + [userId, projectId], + ); + + const { + data: userContributions, + pending: contributionsPending, + } = useFirebaseDatabase>({ + query: contributionsQuery, + skip: skipSelection || isNotDefined(userId), + }); - const candidates = previousGroupId - ? leastMappedTaskGroups.filter((g) => g.groupId !== previousGroupId) - : leastMappedTaskGroups; + const pending = groupsPending || contributionsPending; - const pool = candidates.length > 0 ? candidates : leastMappedTaskGroups; - const index = Math.floor(Math.random() * pool.length); - const selectedTaskGroupId = pool[index].groupId; + // Eligible = still needs contributions (query) AND not already mapped by this + // user AND not the group we just finished — that group's contribution and + // requiredCount are updated by the backend and may not have propagated yet. + const eligibleGroups = useMemo(() => { + const mappedGroupIds = new Set(Object.keys(userContributions ?? {})); + return availableGroups.filter((group) => ( + !mappedGroupIds.has(group.key) + && group.key !== previousGroupId + )); + }, [availableGroups, userContributions, previousGroupId]); + useEffect(() => { + if (skipSelection || pending || eligibleGroups.length === 0) { + return; + } + const index = Math.floor(Math.random() * eligibleGroups.length); router.replace({ pathname: '/project/[id]/map/[taskGroupId]', params: { id: projectId, - taskGroupId: selectedTaskGroupId, + taskGroupId: eligibleGroups[index].key, projectInstruction, }, }); - }, [projectId, taskGroupId, leastMappedTaskGroups, previousGroupId, projectInstruction]); + }, [skipSelection, pending, eligibleGroups, projectId, projectInstruction]); useLayoutEffect(() => { navigation.setOptions({ title: String(projectInstruction), }); - }, [navigation, projectInstruction, pending]); + }, [navigation, projectInstruction]); - if (pending) { + if (skipSelection) { + return null; + } + + if (!pending && eligibleGroups.length === 0) { return ( - - - Loading groups... + + + All done! + + + You've completed all available groups for this project. Thank you! +