Try new way of calculating rankings for large groups

This commit is contained in:
Ian Philips 2022-07-14 09:09:12 -06:00
parent eb6b1b9f89
commit 4eba3c8124
5 changed files with 63 additions and 67 deletions

View File

@ -7,7 +7,7 @@ import {
listenForGroups, listenForGroups,
listenForMemberGroups, listenForMemberGroups,
} from 'web/lib/firebase/groups' } from 'web/lib/firebase/groups'
import { getUser } from 'web/lib/firebase/users' import { getUser, getUsers } from 'web/lib/firebase/users'
import { filterDefined } from 'common/util/array' import { filterDefined } from 'common/util/array'
export const useGroup = (groupId: string | undefined) => { export const useGroup = (groupId: string | undefined) => {
@ -85,9 +85,14 @@ export function useMembers(group: Group, max?: number) {
} }
export async function listMembers(group: Group, max?: number) { export async function listMembers(group: Group, max?: number) {
return await Promise.all( const { memberIds } = group
group.memberIds.slice(0, max ? max : group.memberIds.length).map(getUser) const numToRetrieve = max ?? memberIds.length
if (memberIds.length === 0) return []
if (numToRetrieve)
return (await getUsers()).filter((user) =>
group.memberIds.includes(user.id)
) )
return await Promise.all(group.memberIds.slice(0, numToRetrieve).map(getUser))
} }
export const useGroupsWithContract = (contractId: string | undefined) => { export const useGroupsWithContract = (contractId: string | undefined) => {

View File

@ -124,6 +124,14 @@ export async function listContracts(creatorId: string): Promise<Contract[]> {
return snapshot.docs.map((doc) => doc.data()) return snapshot.docs.map((doc) => doc.data())
} }
export async function listContractsByGroupSlug(
slug: string
): Promise<Contract[]> {
const q = query(contracts, where('groupSlugs', 'array-contains', slug))
const snapshot = await getDocs(q)
return snapshot.docs.map((doc) => doc.data())
}
export async function listTaggedContractsCaseInsensitive( export async function listTaggedContractsCaseInsensitive(
tag: string tag: string
): Promise<Contract[]> { ): Promise<Contract[]> {

View File

@ -8,7 +8,7 @@ import {
} from 'firebase/firestore' } from 'firebase/firestore'
import { sortBy, uniq } from 'lodash' import { sortBy, uniq } from 'lodash'
import { Group } from 'common/group' import { Group } from 'common/group'
import { getContractFromId, updateContract } from './contracts' import { updateContract } from './contracts'
import { import {
coll, coll,
getValue, getValue,
@ -16,7 +16,6 @@ import {
listenForValue, listenForValue,
listenForValues, listenForValues,
} from './utils' } from './utils'
import { filterDefined } from 'common/util/array'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
export const groups = coll<Group>('groups') export const groups = coll<Group>('groups')
@ -54,21 +53,6 @@ export async function getGroupBySlug(slug: string) {
return docs.length === 0 ? null : docs[0].data() return docs.length === 0 ? null : docs[0].data()
} }
export async function getGroupContracts(group: Group) {
const { contractIds } = group
const contracts =
filterDefined(
await Promise.all(
contractIds.map(async (contractId) => {
return await getContractFromId(contractId)
})
)
) ?? []
return [...contracts]
}
export function listenForGroup( export function listenForGroup(
groupId: string, groupId: string,
setGroup: (group: Group | null) => void setGroup: (group: Group | null) => void

View File

@ -3,14 +3,13 @@ import { take, sortBy, debounce } from 'lodash'
import { Group } from 'common/group' import { Group } from 'common/group'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { listAllBets } from 'web/lib/firebase/bets' import { listAllBets } from 'web/lib/firebase/bets'
import { Contract } from 'web/lib/firebase/contracts' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts'
import { import {
groupPath, groupPath,
getGroupBySlug, getGroupBySlug,
updateGroup, updateGroup,
addUserToGroup, addUserToGroup,
addContractToGroup, addContractToGroup,
getGroupContracts,
} from 'web/lib/firebase/groups' } from 'web/lib/firebase/groups'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { UserLink } from 'web/components/user-page' import { UserLink } from 'web/components/user-page'
@ -22,7 +21,7 @@ import {
} from 'web/lib/firebase/users' } from 'web/lib/firebase/users'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { listMembers, useGroup } from 'web/hooks/use-group'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { scoreCreators, scoreTraders } from 'common/scoring' import { scoreCreators, scoreTraders } from 'common/scoring'
import { Leaderboard } from 'web/components/leaderboard' import { Leaderboard } from 'web/components/leaderboard'
@ -62,14 +61,11 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
const { slugs } = props.params const { slugs } = props.params
const group = await getGroupBySlug(slugs[0]) const group = await getGroupBySlug(slugs[0])
const members = const members = group && (await listMembers(group))
group && group.memberIds.length < 100 ? await listMembers(group) : []
const creatorPromise = group ? getUser(group.creatorId) : null const creatorPromise = group ? getUser(group.creatorId) : null
const contracts = const contracts =
group && group.contractIds.length < 100 (group && (await listContractsByGroupSlug(group.slug))) ?? []
? await getGroupContracts(group).catch((_) => [])
: []
const bets = await Promise.all( const bets = await Promise.all(
contracts.map((contract: Contract) => listAllBets(contract.id)) contracts.map((contract: Contract) => listAllBets(contract.id))
@ -77,10 +73,12 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
const creatorScores = scoreCreators(contracts) const creatorScores = scoreCreators(contracts)
const traderScores = scoreTraders(contracts, bets) const traderScores = scoreTraders(contracts, bets)
const [topCreators, topTraders] = await Promise.all([ const [topCreators, topTraders] =
toTopUsers(creatorScores), (members && [
toTopUsers(traderScores), toTopUsers(creatorScores, members),
]) toTopUsers(traderScores, members),
]) ??
[]
const creator = await creatorPromise const creator = await creatorPromise
@ -99,14 +97,14 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
} }
} }
async function toTopUsers(userScores: { [userId: string]: number }) { function toTopUsers(userScores: { [userId: string]: number }, users: User[]) {
const topUserPairs = take( const topUserPairs = take(
sortBy(Object.entries(userScores), ([_, score]) => -1 * score), sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
10 10
).filter(([_, score]) => score >= 0.5) ).filter(([_, score]) => score >= 0.5)
const topUsers = await Promise.all( const topUsers = topUserPairs.map(
topUserPairs.map(([userId]) => getUser(userId)) ([userId]) => users.filter((user) => user.id === userId)[0]
) )
return topUsers.filter((user) => user) return topUsers.filter((user) => user)
} }
@ -199,6 +197,7 @@ export default function GroupPage(props: {
creator={creator} creator={creator}
isCreator={!!isCreator} isCreator={!!isCreator}
user={user} user={user}
members={members}
/> />
</Col> </Col>
) )
@ -327,8 +326,9 @@ function GroupOverview(props: {
creator: User creator: User
user: User | null | undefined user: User | null | undefined
isCreator: boolean isCreator: boolean
members: User[]
}) { }) {
const { group, creator, isCreator, user } = props const { group, creator, isCreator, user, members } = props
const anyoneCanJoinChoices: { [key: string]: string } = { const anyoneCanJoinChoices: { [key: string]: string } = {
Closed: 'false', Closed: 'false',
Open: 'true', Open: 'true',
@ -403,7 +403,7 @@ function GroupOverview(props: {
</Row> </Row>
)} )}
<Col className={'mt-2'}> <Col className={'mt-2'}>
<GroupMemberSearch group={group} /> <GroupMemberSearch members={members} />
</Col> </Col>
</Col> </Col>
</> </>
@ -426,10 +426,9 @@ function SearchBar(props: { setQuery: (query: string) => void }) {
) )
} }
function GroupMemberSearch(props: { group: Group }) { function GroupMemberSearch(props: { members: User[] }) {
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const { group } = props const { members } = props
const members = useMembers(group, 100)
// TODO use find-active-contracts to sort by? // TODO use find-active-contracts to sort by?
const matches = sortBy(members, [(member) => member.name]).filter( const matches = sortBy(members, [(member) => member.name]).filter(
@ -455,29 +454,6 @@ function GroupMemberSearch(props: { group: Group }) {
) )
} }
export function GroupMembersList(props: { group: Group }) {
const { group } = props
const maxMembersToShow = 3
const members = useMembers(group, maxMembersToShow).filter(
(m) => m.id !== group.creatorId
)
if (group.memberIds.length === 1) return <div />
return (
<div className="text-neutral flex flex-wrap gap-1">
<span className={'text-gray-500'}>Other members</span>
{members.slice(0, maxMembersToShow).map((member, i) => (
<div key={member.id} className={'flex-shrink'}>
<UserLink name={member.name} username={member.username} />
{members.length > 1 && i !== members.length - 1 && <span>,</span>}
</div>
))}
{group.memberIds.length > maxMembersToShow && (
<span> & {group.memberIds.length - maxMembersToShow} more</span>
)}
</div>
)
}
function SortedLeaderboard(props: { function SortedLeaderboard(props: {
users: User[] users: User[]
scoreFunction: (user: User) => number scoreFunction: (user: User) => number

View File

@ -1,23 +1,23 @@
import { sortBy, debounce } from 'lodash' import { sortBy, debounce } from 'lodash'
import Link from 'next/link' import Link from 'next/link'
import { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Group } from 'common/group' import { Group } from 'common/group'
import { CreateGroupButton } from 'web/components/groups/create-group-button' import { CreateGroupButton } from 'web/components/groups/create-group-button'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
import { useGroups, useMemberGroupIds } from 'web/hooks/use-group' import { useGroups, useMemberGroupIds, useMembers } from 'web/hooks/use-group'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { groupPath, listAllGroups } from 'web/lib/firebase/groups' import { groupPath, listAllGroups } from 'web/lib/firebase/groups'
import { getUser, User } from 'web/lib/firebase/users' import { getUser, User } from 'web/lib/firebase/users'
import { Tabs } from 'web/components/layout/tabs' import { Tabs } from 'web/components/layout/tabs'
import { GroupMembersList } from 'web/pages/group/[...slugs]'
import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import clsx from 'clsx' import clsx from 'clsx'
import { Avatar } from 'web/components/avatar' import { Avatar } from 'web/components/avatar'
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
import { UserLink } from 'web/components/user-page'
export async function getStaticProps() { export async function getStaticProps() {
const groups = await listAllGroups().catch((_) => []) const groups = await listAllGroups().catch((_) => [])
@ -201,6 +201,29 @@ export function GroupCard(props: { group: Group; creator: User | undefined }) {
) )
} }
function GroupMembersList(props: { group: Group }) {
const { group } = props
const maxMembersToShow = 3
const members = useMembers(group, maxMembersToShow).filter(
(m) => m.id !== group.creatorId
)
if (group.memberIds.length === 1) return <div />
return (
<div className="text-neutral flex flex-wrap gap-1">
<span className={'text-gray-500'}>Other members</span>
{members.slice(0, maxMembersToShow).map((member, i) => (
<div key={member.id} className={'flex-shrink'}>
<UserLink name={member.name} username={member.username} />
{members.length > 1 && i !== members.length - 1 && <span>,</span>}
</div>
))}
{group.memberIds.length > maxMembersToShow && (
<span> & {group.memberIds.length - maxMembersToShow} more</span>
)}
</div>
)
}
export function GroupLink(props: { group: Group; className?: string }) { export function GroupLink(props: { group: Group; className?: string }) {
const { group, className } = props const { group, className } = props