Try new way of calculating rankings for large groups
This commit is contained in:
parent
eb6b1b9f89
commit
4eba3c8124
|
@ -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) => {
|
||||||
|
|
|
@ -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[]> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user