+
{filterCharities.map((charity) => (
-
-
Notes
-
- - Don't see your favorite charity? Recommend it by emailing
- charity@manifold.markets!
-
- - Manifold is not affiliated with non-Featured charities; we're just
- fans of their work.
-
- - As Manifold itself is a for-profit entity, your contributions will
- not be tax deductible.
-
- Donations + matches are wired once each quarter.
+
+
Notes
+
+
+ Don't see your favorite charity? Recommend it by emailing{' '}
+
+ charity@manifold.markets
+
+ !
+
+
+ Manifold is not affiliated with non-Featured charities; we're just
+ fans of their work.
+
+
+ As Manifold itself is a for-profit entity, your contributions will
+ not be tax deductible.
+
+ Donations + matches are wired once each quarter.
+
From c1287a4a25afa7f9406929eca95420c72ba7c7c9 Mon Sep 17 00:00:00 2001
From: James Grugett
Date: Mon, 12 Sep 2022 00:39:04 -0500
Subject: [PATCH 07/76] Small updates to experimental home (#870)
* Line clamp question in prob change table
* Tweaks
* Expand option for daily movers
* Snap scrolling for carousel
* Add arrows to section headers
* Remove carousel from experimental/home
* React querify fetching your groups
* Edit home is its own page
* Add daily profit and balance
* Merge branch 'main' into new-home
* Make experimental search by your followed groups/creators
* Just submit, allow xs on pills
* Weigh in
* Use next/future/image component to optimize avatar images
* Inga/challenge icon (#857)
* changed challenge icon to custom icon
* fixed tip button alignment
* weighing in and trading "weigh in" for "trade"
* Delete closing soon, mark new as New for you, trending is site-wide
* Delete your trades. Factor out section item
* Don't allow hiding of home sections
* Convert daily movers into a section
* Tweaks for loading daily movers
* Prob change table shows variable number of rows
* Fix double negative
Co-authored-by: Ian Philips
Co-authored-by: Austin Chen
Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros
---
common/user.ts | 2 +-
web/components/arrange-home.tsx | 103 +++++++---------
web/components/contract/prob-change-table.tsx | 113 +++++++++---------
web/pages/experimental/home/edit.tsx | 11 +-
web/pages/experimental/home/index.tsx | 68 +++++++----
5 files changed, 146 insertions(+), 151 deletions(-)
diff --git a/common/user.ts b/common/user.ts
index 0e333278..f15865cf 100644
--- a/common/user.ts
+++ b/common/user.ts
@@ -34,7 +34,7 @@ export type User = {
followerCountCached: number
followedCategories?: string[]
- homeSections?: { visible: string[]; hidden: string[] }
+ homeSections?: string[]
referredByUserId?: string
referredByContractId?: string
diff --git a/web/components/arrange-home.tsx b/web/components/arrange-home.tsx
index ae02e3ea..25e814b8 100644
--- a/web/components/arrange-home.tsx
+++ b/web/components/arrange-home.tsx
@@ -13,19 +13,13 @@ import { Group } from 'common/group'
export function ArrangeHome(props: {
user: User | null | undefined
- homeSections: { visible: string[]; hidden: string[] }
- setHomeSections: (homeSections: {
- visible: string[]
- hidden: string[]
- }) => void
+ homeSections: string[]
+ setHomeSections: (sections: string[]) => void
}) {
const { user, homeSections, setHomeSections } = props
const groups = useMemberGroups(user?.id) ?? []
- const { itemsById, visibleItems, hiddenItems } = getHomeItems(
- groups,
- homeSections
- )
+ const { itemsById, sections } = getHomeItems(groups, homeSections)
return (
item.id),
- hidden: hiddenItems.map((item) => item.id),
- }
+ const newHomeSections = sections.map((section) => section.id)
- const sourceSection = source.droppableId as 'visible' | 'hidden'
- newHomeSections[sourceSection].splice(source.index, 1)
-
- const destSection = destination.droppableId as 'visible' | 'hidden'
- newHomeSections[destSection].splice(destination.index, 0, item.id)
+ newHomeSections.splice(source.index, 1)
+ newHomeSections.splice(destination.index, 0, item.id)
setHomeSections(newHomeSections)
}}
>
-
-
-
+
+
)
@@ -64,16 +51,13 @@ function DraggableList(props: {
const { title, items } = props
return (
- {(provided, snapshot) => (
+ {(provided) => (
-
+
{items.map((item, index) => (
{(provided, snapshot) => (
@@ -82,16 +66,13 @@ function DraggableList(props: {
{...provided.draggableProps}
{...provided.dragHandleProps}
style={provided.draggableProps.style}
- className={clsx(
- 'flex flex-row items-center gap-4 rounded bg-gray-50 p-2',
- snapshot.isDragging && 'z-[9000] bg-gray-300'
- )}
>
- {' '}
- {item.label}
+
)}
@@ -103,15 +84,33 @@ function DraggableList(props: {
)
}
-export const getHomeItems = (
- groups: Group[],
- homeSections: { visible: string[]; hidden: string[] }
-) => {
+const SectionItem = (props: {
+ item: { id: string; label: string }
+ className?: string
+}) => {
+ const { item, className } = props
+
+ return (
+
+ {' '}
+ {item.label}
+
+ )
+}
+
+export const getHomeItems = (groups: Group[], sections: string[]) => {
const items = [
+ { label: 'Daily movers', id: 'daily-movers' },
{ label: 'Trending', id: 'score' },
- { label: 'Newest', id: 'newest' },
- { label: 'Close date', id: 'close-date' },
- { label: 'Your trades', id: 'your-bets' },
+ { label: 'New for you', id: 'newest' },
...groups.map((g) => ({
label: g.name,
id: g.id,
@@ -119,23 +118,13 @@ export const getHomeItems = (
]
const itemsById = keyBy(items, 'id')
- const { visible, hidden } = homeSections
+ const sectionItems = filterDefined(sections.map((id) => itemsById[id]))
- const [visibleItems, hiddenItems] = [
- filterDefined(visible.map((id) => itemsById[id])),
- filterDefined(hidden.map((id) => itemsById[id])),
- ]
-
- // Add unmentioned items to the visible list.
- visibleItems.push(
- ...items.filter(
- (item) => !visibleItems.includes(item) && !hiddenItems.includes(item)
- )
- )
+ // Add unmentioned items to the end.
+ sectionItems.push(...items.filter((item) => !sectionItems.includes(item)))
return {
- visibleItems,
- hiddenItems,
+ sections: sectionItems,
itemsById,
}
}
diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx
index f973d260..49216b88 100644
--- a/web/components/contract/prob-change-table.tsx
+++ b/web/components/contract/prob-change-table.tsx
@@ -2,74 +2,69 @@ import clsx from 'clsx'
import { contractPath } from 'web/lib/firebase/contracts'
import { CPMMContract } from 'common/contract'
import { formatPercent } from 'common/util/format'
-import { useProbChanges } from 'web/hooks/use-prob-changes'
-import { linkClass, SiteLink } from '../site-link'
+import { SiteLink } from '../site-link'
import { Col } from '../layout/col'
import { Row } from '../layout/row'
-import { useState } from 'react'
+import { LoadingIndicator } from '../loading-indicator'
-export function ProbChangeTable(props: { userId: string | undefined }) {
- const { userId } = props
+export function ProbChangeTable(props: {
+ changes:
+ | { positiveChanges: CPMMContract[]; negativeChanges: CPMMContract[] }
+ | undefined
+}) {
+ const { changes } = props
- const changes = useProbChanges(userId ?? '')
- const [expanded, setExpanded] = useState(false)
-
- if (!changes) {
- return null
- }
-
- const count = expanded ? 16 : 4
+ if (!changes) return
const { positiveChanges, negativeChanges } = changes
- const filteredPositiveChanges = positiveChanges.slice(0, count / 2)
- const filteredNegativeChanges = negativeChanges.slice(0, count / 2)
- const filteredChanges = [
- ...filteredPositiveChanges,
- ...filteredNegativeChanges,
- ]
+
+ const threshold = 0.075
+ const countOverThreshold = Math.max(
+ positiveChanges.findIndex((c) => c.probChanges.day < threshold) + 1,
+ negativeChanges.findIndex((c) => c.probChanges.day > -threshold) + 1
+ )
+ const maxRows = Math.min(positiveChanges.length, negativeChanges.length)
+ const rows = Math.min(3, Math.min(maxRows, countOverThreshold))
+
+ const filteredPositiveChanges = positiveChanges.slice(0, rows)
+ const filteredNegativeChanges = negativeChanges.slice(0, rows)
+
+ if (rows === 0) return
None
return (
-
-
-
- {filteredChanges.slice(0, count / 2).map((contract) => (
-
-
-
- {contract.question}
-
-
- ))}
-
-
- {filteredChanges.slice(count / 2).map((contract) => (
-
-
-
- {contract.question}
-
-
- ))}
-
+
+
+ {filteredPositiveChanges.map((contract) => (
+
+
+
+ {contract.question}
+
+
+ ))}
+
+
+ {filteredNegativeChanges.map((contract) => (
+
+
+
+ {contract.question}
+
+
+ ))}
-
setExpanded(!expanded)}
- >
- {expanded ? 'Show less' : 'Show more'}
-
)
}
diff --git a/web/pages/experimental/home/edit.tsx b/web/pages/experimental/home/edit.tsx
index 2cba3f19..2ed9d2dd 100644
--- a/web/pages/experimental/home/edit.tsx
+++ b/web/pages/experimental/home/edit.tsx
@@ -16,14 +16,9 @@ export default function Home() {
useTracking('edit home')
- const [homeSections, setHomeSections] = useState(
- user?.homeSections ?? { visible: [], hidden: [] }
- )
+ const [homeSections, setHomeSections] = useState(user?.homeSections ?? [])
- const updateHomeSections = (newHomeSections: {
- visible: string[]
- hidden: string[]
- }) => {
+ const updateHomeSections = (newHomeSections: string[]) => {
if (!user) return
updateUser(user.id, { homeSections: newHomeSections })
setHomeSections(newHomeSections)
@@ -31,7 +26,7 @@ export default function Home() {
return (
-
+
diff --git a/web/pages/experimental/home/index.tsx b/web/pages/experimental/home/index.tsx
index 90b4f888..08f502b6 100644
--- a/web/pages/experimental/home/index.tsx
+++ b/web/pages/experimental/home/index.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react'
+import React from 'react'
import Router from 'next/router'
import {
PencilIcon,
@@ -28,6 +28,7 @@ import { groupPath } from 'web/lib/firebase/groups'
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
import { calculatePortfolioProfit } from 'common/calculate-metrics'
import { formatMoney } from 'common/util/format'
+import { useProbChanges } from 'web/hooks/use-prob-changes'
const Home = () => {
const user = useUser()
@@ -38,10 +39,7 @@ const Home = () => {
const groups = useMemberGroups(user?.id) ?? []
- const [homeSections] = useState(
- user?.homeSections ?? { visible: [], hidden: [] }
- )
- const { visibleItems } = getHomeItems(groups, homeSections)
+ const { sections } = getHomeItems(groups, user?.homeSections ?? [])
return (
@@ -54,29 +52,19 @@ const Home = () => {
- Daily movers
-
-
- {visibleItems.map((item) => {
+ {sections.map((item) => {
const { id } = item
- if (id === 'your-bets') {
- return (
-
- )
+ if (id === 'daily-movers') {
+ return
}
const sort = SORTS.find((sort) => sort.value === id)
if (sort)
return (
)
@@ -103,11 +91,12 @@ const Home = () => {
function SearchSection(props: {
label: string
- user: User | null | undefined
+ user: User | null | undefined | undefined
sort: Sort
yourBets?: boolean
+ followed?: boolean
}) {
- const { label, user, sort, yourBets } = props
+ const { label, user, sort, yourBets, followed } = props
const href = `/home?s=${sort}`
return (
@@ -122,7 +111,13 @@ function SearchSection(props: {
+
+ Daily movers{' '}
+
+
+
+
+ )
+}
+
function EditButton(props: { className?: string }) {
const { className } = props
@@ -186,14 +202,14 @@ function DailyProfitAndBalance(props: {
return (
= 0 ? 'text-green-500' : 'text-red-500')}>
- {profit >= 0 ? '+' : '-'}
+ {profit >= 0 && '+'}
{formatMoney(profit)}
{' '}
profit and{' '}
= 0 ? 'text-green-500' : 'text-red-500')}
>
- {balanceChange >= 0 ? '+' : '-'}
+ {balanceChange >= 0 && '+'}
{formatMoney(balanceChange)}
{' '}
balance today
From a6ed8c92282dd26310ee6b7c029365065ec53fdd Mon Sep 17 00:00:00 2001
From: FRC
Date: Mon, 12 Sep 2022 16:44:24 +0100
Subject: [PATCH 08/76] Fix "500 internal error" in large groups (#856)
* Members to memberIds
* Moved to update-metrics
---
common/group.ts | 15 ++
functions/src/update-metrics.ts | 72 +++++++-
web/lib/firebase/groups.ts | 5 +-
web/pages/group/[...slugs]/index.tsx | 263 +++++++--------------------
4 files changed, 145 insertions(+), 210 deletions(-)
diff --git a/common/group.ts b/common/group.ts
index 19f3b7b8..36654101 100644
--- a/common/group.ts
+++ b/common/group.ts
@@ -12,7 +12,22 @@ export type Group = {
aboutPostId?: string
chatDisabled?: boolean
mostRecentContractAddedTime?: number
+ /** @deprecated - members and contracts now stored as subcollections*/
+ memberIds?: string[] // Deprecated
+ /** @deprecated - members and contracts now stored as subcollections*/
+ contractIds?: string[] // Deprecated
+ cachedLeaderboard?: {
+ topTraders: {
+ userId: string
+ score: number
+ }[]
+ topCreators: {
+ userId: string
+ score: number
+ }[]
+ }
}
+
export const MAX_GROUP_NAME_LENGTH = 75
export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60
diff --git a/functions/src/update-metrics.ts b/functions/src/update-metrics.ts
index 430f3d33..273cd098 100644
--- a/functions/src/update-metrics.ts
+++ b/functions/src/update-metrics.ts
@@ -4,9 +4,11 @@ import { groupBy, isEmpty, keyBy, last, sortBy } from 'lodash'
import { getValues, log, logMemory, writeAsync } from './utils'
import { Bet } from '../../common/bet'
import { Contract, CPMM } from '../../common/contract'
+
import { PortfolioMetrics, User } from '../../common/user'
import { DAY_MS } from '../../common/util/time'
import { getLoanUpdates } from '../../common/loans'
+import { scoreTraders, scoreCreators } from '../../common/scoring'
import {
calculateCreatorVolume,
calculateNewPortfolioMetrics,
@@ -15,6 +17,7 @@ import {
computeVolume,
} from '../../common/calculate-metrics'
import { getProbability } from '../../common/calculate'
+import { Group } from 'common/group'
const firestore = admin.firestore()
@@ -24,16 +27,29 @@ export const updateMetrics = functions
.onRun(updateMetricsCore)
export async function updateMetricsCore() {
- const [users, contracts, bets, allPortfolioHistories] = await Promise.all([
- getValues(firestore.collection('users')),
- getValues(firestore.collection('contracts')),
- getValues(firestore.collectionGroup('bets')),
- getValues(
- firestore
- .collectionGroup('portfolioHistory')
- .where('timestamp', '>', Date.now() - 31 * DAY_MS) // so it includes just over a month ago
- ),
- ])
+ const [users, contracts, bets, allPortfolioHistories, groups] =
+ await Promise.all([
+ getValues(firestore.collection('users')),
+ getValues(firestore.collection('contracts')),
+ getValues(firestore.collectionGroup('bets')),
+ getValues(
+ firestore
+ .collectionGroup('portfolioHistory')
+ .where('timestamp', '>', Date.now() - 31 * DAY_MS) // so it includes just over a month ago
+ ),
+ getValues(firestore.collection('groups')),
+ ])
+
+ const contractsByGroup = await Promise.all(
+ groups.map((group) => {
+ return getValues(
+ firestore
+ .collection('groups')
+ .doc(group.id)
+ .collection('groupContracts')
+ )
+ })
+ )
log(
`Loaded ${users.length} users, ${contracts.length} contracts, and ${bets.length} bets.`
)
@@ -162,4 +178,40 @@ export async function updateMetricsCore() {
'set'
)
log(`Updated metrics for ${users.length} users.`)
+
+ const groupUpdates = groups.map((group, index) => {
+ const groupContractIds = contractsByGroup[index] as GroupContractDoc[]
+ const groupContracts = groupContractIds.map(
+ (e) => contractsById[e.contractId]
+ )
+ const bets = groupContracts.map((e) => {
+ return betsByContract[e.id] ?? []
+ })
+
+ const creatorScores = scoreCreators(groupContracts)
+ const traderScores = scoreTraders(groupContracts, bets)
+
+ const topTraderScores = topUserScores(traderScores)
+ const topCreatorScores = topUserScores(creatorScores)
+
+ return {
+ doc: firestore.collection('groups').doc(group.id),
+ fields: {
+ cachedLeaderboard: {
+ topTraders: topTraderScores,
+ topCreators: topCreatorScores,
+ },
+ },
+ }
+ })
+ await writeAsync(firestore, groupUpdates)
}
+
+const topUserScores = (scores: { [userId: string]: number }) => {
+ const top50 = Object.entries(scores)
+ .sort(([, scoreA], [, scoreB]) => scoreB - scoreA)
+ .slice(0, 50)
+ return top50.map(([userId, score]) => ({ userId, score }))
+}
+
+type GroupContractDoc = { contractId: string; createdTime: number }
diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts
index 7a372d9a..f27460d9 100644
--- a/web/lib/firebase/groups.ts
+++ b/web/lib/firebase/groups.ts
@@ -24,7 +24,6 @@ import { Contract } from 'common/contract'
import { getContractFromId, updateContract } from 'web/lib/firebase/contracts'
import { db } from 'web/lib/firebase/init'
import { filterDefined } from 'common/util/array'
-import { getUser } from 'web/lib/firebase/users'
export const groups = coll('groups')
export const groupMembers = (groupId: string) =>
@@ -253,7 +252,7 @@ export function getGroupLinkToDisplay(contract: Contract) {
return groupToDisplay
}
-export async function listMembers(group: Group) {
+export async function listMemberIds(group: Group) {
const members = await getValues(groupMembers(group.id))
- return await Promise.all(members.map((m) => m.userId).map(getUser))
+ return members.map((m) => m.userId)
}
diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx
index 768e2f82..f5d68e57 100644
--- a/web/pages/group/[...slugs]/index.tsx
+++ b/web/pages/group/[...slugs]/index.tsx
@@ -1,28 +1,28 @@
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
-import { debounce, sortBy, take } from 'lodash'
-import { SearchIcon } from '@heroicons/react/outline'
import { toast } from 'react-hot-toast'
import { Group, GROUP_CHAT_SLUG } from 'common/group'
import { Page } from 'web/components/page'
-import { listAllBets } from 'web/lib/firebase/bets'
import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts'
import {
addContractToGroup,
getGroupBySlug,
groupPath,
joinGroup,
- listMembers,
+ listMemberIds,
updateGroup,
} from 'web/lib/firebase/groups'
import { Row } from 'web/components/layout/row'
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
import { Col } from 'web/components/layout/col'
import { useUser } from 'web/hooks/use-user'
-import { useGroup, useGroupContractIds, useMembers } from 'web/hooks/use-group'
-import { scoreCreators, scoreTraders } from 'common/scoring'
+import {
+ useGroup,
+ useGroupContractIds,
+ useMemberIds,
+} from 'web/hooks/use-group'
import { Leaderboard } from 'web/components/leaderboard'
import { formatMoney } from 'common/util/format'
import { EditGroupButton } from 'web/components/groups/edit-group-button'
@@ -35,9 +35,7 @@ import { LoadingIndicator } from 'web/components/loading-indicator'
import { Modal } from 'web/components/layout/modal'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { ContractSearch } from 'web/components/contract-search'
-import { FollowList } from 'web/components/follow-list'
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
-import { searchInAny } from 'common/util/parse'
import { CopyLinkButton } from 'web/components/copy-link-button'
import { ENV_CONFIG } from 'common/envs/constants'
import { useSaveReferral } from 'web/hooks/use-save-referral'
@@ -59,7 +57,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
const { slugs } = props.params
const group = await getGroupBySlug(slugs[0])
- const members = group && (await listMembers(group))
+ const memberIds = group && (await listMemberIds(group))
const creatorPromise = group ? getUser(group.creatorId) : null
const contracts =
@@ -71,19 +69,15 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
: 'open'
const aboutPost =
group && group.aboutPostId != null && (await getPost(group.aboutPostId))
- const bets = await Promise.all(
- contracts.map((contract: Contract) => listAllBets(contract.id))
- )
const messages = group && (await listAllCommentsOnGroup(group.id))
- const creatorScores = scoreCreators(contracts)
- const traderScores = scoreTraders(contracts, bets)
- const [topCreators, topTraders] =
- (members && [
- toTopUsers(creatorScores, members),
- toTopUsers(traderScores, members),
- ]) ??
- []
+ const cachedTopTraderIds =
+ (group && group.cachedLeaderboard?.topTraders) ?? []
+ const cachedTopCreatorIds =
+ (group && group.cachedLeaderboard?.topCreators) ?? []
+ const topTraders = await toTopUsers(cachedTopTraderIds)
+
+ const topCreators = await toTopUsers(cachedTopCreatorIds)
const creator = await creatorPromise
// Only count unresolved markets
@@ -93,11 +87,9 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
props: {
contractsCount,
group,
- members,
+ memberIds,
creator,
- traderScores,
topTraders,
- creatorScores,
topCreators,
messages,
aboutPost,
@@ -107,19 +99,6 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
revalidate: 60, // regenerate after a minute
}
}
-
-function toTopUsers(userScores: { [userId: string]: number }, users: User[]) {
- const topUserPairs = take(
- sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
- 10
- ).filter(([_, score]) => score >= 0.5)
-
- const topUsers = topUserPairs.map(
- ([userId]) => users.filter((user) => user.id === userId)[0]
- )
- return topUsers.filter((user) => user)
-}
-
export async function getStaticPaths() {
return { paths: [], fallback: 'blocking' }
}
@@ -134,12 +113,10 @@ const groupSubpages = [
export default function GroupPage(props: {
contractsCount: number
group: Group | null
- members: User[]
+ memberIds: string[]
creator: User
- traderScores: { [userId: string]: number }
- topTraders: User[]
- creatorScores: { [userId: string]: number }
- topCreators: User[]
+ topTraders: { user: User; score: number }[]
+ topCreators: { user: User; score: number }[]
messages: GroupComment[]
aboutPost: Post
suggestedFilter: 'open' | 'all'
@@ -147,24 +124,15 @@ export default function GroupPage(props: {
props = usePropz(props, getStaticPropz) ?? {
contractsCount: 0,
group: null,
- members: [],
+ memberIds: [],
creator: null,
- traderScores: {},
topTraders: [],
- creatorScores: {},
topCreators: [],
messages: [],
suggestedFilter: 'open',
}
- const {
- contractsCount,
- creator,
- traderScores,
- topTraders,
- creatorScores,
- topCreators,
- suggestedFilter,
- } = props
+ const { contractsCount, creator, topTraders, topCreators, suggestedFilter } =
+ props
const router = useRouter()
const { slugs } = router.query as { slugs: string[] }
@@ -175,7 +143,7 @@ export default function GroupPage(props: {
const user = useUser()
const isAdmin = useAdmin()
- const members = useMembers(group?.id) ?? props.members
+ const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds
useSaveReferral(user, {
defaultReferrerUsername: creator.username,
@@ -186,18 +154,25 @@ export default function GroupPage(props: {
return
}
const isCreator = user && group && user.id === group.creatorId
- const isMember = user && members.map((m) => m.id).includes(user.id)
+ const isMember = user && memberIds.includes(user.id)
+ const maxLeaderboardSize = 50
const leaderboard = (
-
+
+
+
+
)
@@ -216,7 +191,7 @@ export default function GroupPage(props: {
creator={creator}
isCreator={!!isCreator}
user={user}
- members={members}
+ memberIds={memberIds}
/>
)
@@ -312,9 +287,9 @@ function GroupOverview(props: {
creator: User
user: User | null | undefined
isCreator: boolean
- members: User[]
+ memberIds: string[]
}) {
- const { group, creator, isCreator, user, members } = props
+ const { group, creator, isCreator, user, memberIds } = props
const anyoneCanJoinChoices: { [key: string]: string } = {
Closed: 'false',
Open: 'true',
@@ -333,7 +308,7 @@ function GroupOverview(props: {
const shareUrl = `https://${ENV_CONFIG.domain}${groupPath(
group.slug
)}${postFix}`
- const isMember = user ? members.map((m) => m.id).includes(user.id) : false
+ const isMember = user ? memberIds.includes(user.id) : false
return (
<>
@@ -399,155 +374,37 @@ function GroupOverview(props: {
/>
)}
-
-
- Members
-
-
>
)
}
-function SearchBar(props: { setQuery: (query: string) => void }) {
- const { setQuery } = props
- const debouncedQuery = debounce(setQuery, 50)
- return (
-
-
- debouncedQuery(e.target.value)}
- placeholder="Find a member"
- className="input input-bordered mb-4 w-full pl-12"
- />
-
- )
-}
-
-function GroupMemberSearch(props: { members: User[]; group: Group }) {
- const [query, setQuery] = useState('')
- const { group } = props
- let { members } = props
-
- // Use static members on load, but also listen to member changes:
- const listenToMembers = useMembers(group.id)
- if (listenToMembers) {
- members = listenToMembers
- }
-
- // TODO use find-active-contracts to sort by?
- const matches = sortBy(members, [(member) => member.name]).filter((m) =>
- searchInAny(query, m.name, m.username)
- )
- const matchLimit = 25
-
- return (
-
-
-
- {matches.length > 0 && (
-
m.id)} />
- )}
- {matches.length > 25 && (
-
- And {matches.length - matchLimit} more...
-
- )}
-
-
- )
-}
-
-function SortedLeaderboard(props: {
- users: User[]
- scoreFunction: (user: User) => number
+function GroupLeaderboard(props: {
+ topUsers: { user: User; score: number }[]
title: string
+ maxToShow: number
header: string
- maxToShow?: number
}) {
- const { users, scoreFunction, title, header, maxToShow } = props
- const sortedUsers = users.sort((a, b) => scoreFunction(b) - scoreFunction(a))
+ const { topUsers, title, maxToShow, header } = props
+
+ const scoresByUser = topUsers.reduce((acc, { user, score }) => {
+ acc[user.id] = score
+ return acc
+ }, {} as { [key: string]: number })
+
return (
t.user)}
title={title}
columns={[
- { header, renderCell: (user) => formatMoney(scoreFunction(user)) },
+ { header, renderCell: (user) => formatMoney(scoresByUser[user.id]) },
]}
maxToShow={maxToShow}
/>
)
}
-function GroupLeaderboards(props: {
- traderScores: { [userId: string]: number }
- creatorScores: { [userId: string]: number }
- topTraders: User[]
- topCreators: User[]
- members: User[]
- user: User | null | undefined
-}) {
- const { traderScores, creatorScores, members, topTraders, topCreators } =
- props
- const maxToShow = 50
- // Consider hiding M$0
- // If it's just one member (curator), show all bettors, otherwise just show members
- return (
-
-
- {members.length > 1 ? (
- <>
- traderScores[user.id] ?? 0}
- title="🏅 Top traders"
- header="Profit"
- maxToShow={maxToShow}
- />
- creatorScores[user.id] ?? 0}
- title="🏅 Top creators"
- header="Market volume"
- maxToShow={maxToShow}
- />
- >
- ) : (
- <>
- formatMoney(traderScores[user.id] ?? 0),
- },
- ]}
- maxToShow={maxToShow}
- />
-
- formatMoney(creatorScores[user.id] ?? 0),
- },
- ]}
- maxToShow={maxToShow}
- />
- >
- )}
-
-
- )
-}
-
function AddContractButton(props: { group: Group; user: User }) {
const { group, user } = props
const [open, setOpen] = useState(false)
@@ -684,3 +541,15 @@ function JoinGroupButton(props: {
)
}
+
+const toTopUsers = async (
+ cachedUserIds: { userId: string; score: number }[]
+): Promise<{ user: User; score: number }[]> =>
+ (
+ await Promise.all(
+ cachedUserIds.map(async (e) => {
+ const user = await getUser(e.userId)
+ return { user, score: e.score ?? 0 }
+ })
+ )
+ ).filter((e) => e.user != null)
From 28f0c6b1f8f14bd210d02b6032967822d1779b3b Mon Sep 17 00:00:00 2001
From: FRC
Date: Mon, 12 Sep 2022 17:26:46 +0100
Subject: [PATCH 09/76] Revert "Fix "500 internal error" in large groups
(#856)" (#871)
This reverts commit a6ed8c92282dd26310ee6b7c029365065ec53fdd.
---
common/group.ts | 15 --
functions/src/update-metrics.ts | 72 +-------
web/lib/firebase/groups.ts | 5 +-
web/pages/group/[...slugs]/index.tsx | 263 ++++++++++++++++++++-------
4 files changed, 210 insertions(+), 145 deletions(-)
diff --git a/common/group.ts b/common/group.ts
index 36654101..19f3b7b8 100644
--- a/common/group.ts
+++ b/common/group.ts
@@ -12,22 +12,7 @@ export type Group = {
aboutPostId?: string
chatDisabled?: boolean
mostRecentContractAddedTime?: number
- /** @deprecated - members and contracts now stored as subcollections*/
- memberIds?: string[] // Deprecated
- /** @deprecated - members and contracts now stored as subcollections*/
- contractIds?: string[] // Deprecated
- cachedLeaderboard?: {
- topTraders: {
- userId: string
- score: number
- }[]
- topCreators: {
- userId: string
- score: number
- }[]
- }
}
-
export const MAX_GROUP_NAME_LENGTH = 75
export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60
diff --git a/functions/src/update-metrics.ts b/functions/src/update-metrics.ts
index 273cd098..430f3d33 100644
--- a/functions/src/update-metrics.ts
+++ b/functions/src/update-metrics.ts
@@ -4,11 +4,9 @@ import { groupBy, isEmpty, keyBy, last, sortBy } from 'lodash'
import { getValues, log, logMemory, writeAsync } from './utils'
import { Bet } from '../../common/bet'
import { Contract, CPMM } from '../../common/contract'
-
import { PortfolioMetrics, User } from '../../common/user'
import { DAY_MS } from '../../common/util/time'
import { getLoanUpdates } from '../../common/loans'
-import { scoreTraders, scoreCreators } from '../../common/scoring'
import {
calculateCreatorVolume,
calculateNewPortfolioMetrics,
@@ -17,7 +15,6 @@ import {
computeVolume,
} from '../../common/calculate-metrics'
import { getProbability } from '../../common/calculate'
-import { Group } from 'common/group'
const firestore = admin.firestore()
@@ -27,29 +24,16 @@ export const updateMetrics = functions
.onRun(updateMetricsCore)
export async function updateMetricsCore() {
- const [users, contracts, bets, allPortfolioHistories, groups] =
- await Promise.all([
- getValues(firestore.collection('users')),
- getValues(firestore.collection('contracts')),
- getValues(firestore.collectionGroup('bets')),
- getValues(
- firestore
- .collectionGroup('portfolioHistory')
- .where('timestamp', '>', Date.now() - 31 * DAY_MS) // so it includes just over a month ago
- ),
- getValues(firestore.collection('groups')),
- ])
-
- const contractsByGroup = await Promise.all(
- groups.map((group) => {
- return getValues(
- firestore
- .collection('groups')
- .doc(group.id)
- .collection('groupContracts')
- )
- })
- )
+ const [users, contracts, bets, allPortfolioHistories] = await Promise.all([
+ getValues(firestore.collection('users')),
+ getValues(firestore.collection('contracts')),
+ getValues(firestore.collectionGroup('bets')),
+ getValues(
+ firestore
+ .collectionGroup('portfolioHistory')
+ .where('timestamp', '>', Date.now() - 31 * DAY_MS) // so it includes just over a month ago
+ ),
+ ])
log(
`Loaded ${users.length} users, ${contracts.length} contracts, and ${bets.length} bets.`
)
@@ -178,40 +162,4 @@ export async function updateMetricsCore() {
'set'
)
log(`Updated metrics for ${users.length} users.`)
-
- const groupUpdates = groups.map((group, index) => {
- const groupContractIds = contractsByGroup[index] as GroupContractDoc[]
- const groupContracts = groupContractIds.map(
- (e) => contractsById[e.contractId]
- )
- const bets = groupContracts.map((e) => {
- return betsByContract[e.id] ?? []
- })
-
- const creatorScores = scoreCreators(groupContracts)
- const traderScores = scoreTraders(groupContracts, bets)
-
- const topTraderScores = topUserScores(traderScores)
- const topCreatorScores = topUserScores(creatorScores)
-
- return {
- doc: firestore.collection('groups').doc(group.id),
- fields: {
- cachedLeaderboard: {
- topTraders: topTraderScores,
- topCreators: topCreatorScores,
- },
- },
- }
- })
- await writeAsync(firestore, groupUpdates)
}
-
-const topUserScores = (scores: { [userId: string]: number }) => {
- const top50 = Object.entries(scores)
- .sort(([, scoreA], [, scoreB]) => scoreB - scoreA)
- .slice(0, 50)
- return top50.map(([userId, score]) => ({ userId, score }))
-}
-
-type GroupContractDoc = { contractId: string; createdTime: number }
diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts
index f27460d9..7a372d9a 100644
--- a/web/lib/firebase/groups.ts
+++ b/web/lib/firebase/groups.ts
@@ -24,6 +24,7 @@ import { Contract } from 'common/contract'
import { getContractFromId, updateContract } from 'web/lib/firebase/contracts'
import { db } from 'web/lib/firebase/init'
import { filterDefined } from 'common/util/array'
+import { getUser } from 'web/lib/firebase/users'
export const groups = coll('groups')
export const groupMembers = (groupId: string) =>
@@ -252,7 +253,7 @@ export function getGroupLinkToDisplay(contract: Contract) {
return groupToDisplay
}
-export async function listMemberIds(group: Group) {
+export async function listMembers(group: Group) {
const members = await getValues(groupMembers(group.id))
- return members.map((m) => m.userId)
+ return await Promise.all(members.map((m) => m.userId).map(getUser))
}
diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx
index f5d68e57..768e2f82 100644
--- a/web/pages/group/[...slugs]/index.tsx
+++ b/web/pages/group/[...slugs]/index.tsx
@@ -1,28 +1,28 @@
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
+import { debounce, sortBy, take } from 'lodash'
+import { SearchIcon } from '@heroicons/react/outline'
import { toast } from 'react-hot-toast'
import { Group, GROUP_CHAT_SLUG } from 'common/group'
import { Page } from 'web/components/page'
+import { listAllBets } from 'web/lib/firebase/bets'
import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts'
import {
addContractToGroup,
getGroupBySlug,
groupPath,
joinGroup,
- listMemberIds,
+ listMembers,
updateGroup,
} from 'web/lib/firebase/groups'
import { Row } from 'web/components/layout/row'
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
import { Col } from 'web/components/layout/col'
import { useUser } from 'web/hooks/use-user'
-import {
- useGroup,
- useGroupContractIds,
- useMemberIds,
-} from 'web/hooks/use-group'
+import { useGroup, useGroupContractIds, useMembers } from 'web/hooks/use-group'
+import { scoreCreators, scoreTraders } from 'common/scoring'
import { Leaderboard } from 'web/components/leaderboard'
import { formatMoney } from 'common/util/format'
import { EditGroupButton } from 'web/components/groups/edit-group-button'
@@ -35,7 +35,9 @@ import { LoadingIndicator } from 'web/components/loading-indicator'
import { Modal } from 'web/components/layout/modal'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { ContractSearch } from 'web/components/contract-search'
+import { FollowList } from 'web/components/follow-list'
import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button'
+import { searchInAny } from 'common/util/parse'
import { CopyLinkButton } from 'web/components/copy-link-button'
import { ENV_CONFIG } from 'common/envs/constants'
import { useSaveReferral } from 'web/hooks/use-save-referral'
@@ -57,7 +59,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
const { slugs } = props.params
const group = await getGroupBySlug(slugs[0])
- const memberIds = group && (await listMemberIds(group))
+ const members = group && (await listMembers(group))
const creatorPromise = group ? getUser(group.creatorId) : null
const contracts =
@@ -69,15 +71,19 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
: 'open'
const aboutPost =
group && group.aboutPostId != null && (await getPost(group.aboutPostId))
+ const bets = await Promise.all(
+ contracts.map((contract: Contract) => listAllBets(contract.id))
+ )
const messages = group && (await listAllCommentsOnGroup(group.id))
- const cachedTopTraderIds =
- (group && group.cachedLeaderboard?.topTraders) ?? []
- const cachedTopCreatorIds =
- (group && group.cachedLeaderboard?.topCreators) ?? []
- const topTraders = await toTopUsers(cachedTopTraderIds)
-
- const topCreators = await toTopUsers(cachedTopCreatorIds)
+ const creatorScores = scoreCreators(contracts)
+ const traderScores = scoreTraders(contracts, bets)
+ const [topCreators, topTraders] =
+ (members && [
+ toTopUsers(creatorScores, members),
+ toTopUsers(traderScores, members),
+ ]) ??
+ []
const creator = await creatorPromise
// Only count unresolved markets
@@ -87,9 +93,11 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
props: {
contractsCount,
group,
- memberIds,
+ members,
creator,
+ traderScores,
topTraders,
+ creatorScores,
topCreators,
messages,
aboutPost,
@@ -99,6 +107,19 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
revalidate: 60, // regenerate after a minute
}
}
+
+function toTopUsers(userScores: { [userId: string]: number }, users: User[]) {
+ const topUserPairs = take(
+ sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
+ 10
+ ).filter(([_, score]) => score >= 0.5)
+
+ const topUsers = topUserPairs.map(
+ ([userId]) => users.filter((user) => user.id === userId)[0]
+ )
+ return topUsers.filter((user) => user)
+}
+
export async function getStaticPaths() {
return { paths: [], fallback: 'blocking' }
}
@@ -113,10 +134,12 @@ const groupSubpages = [
export default function GroupPage(props: {
contractsCount: number
group: Group | null
- memberIds: string[]
+ members: User[]
creator: User
- topTraders: { user: User; score: number }[]
- topCreators: { user: User; score: number }[]
+ traderScores: { [userId: string]: number }
+ topTraders: User[]
+ creatorScores: { [userId: string]: number }
+ topCreators: User[]
messages: GroupComment[]
aboutPost: Post
suggestedFilter: 'open' | 'all'
@@ -124,15 +147,24 @@ export default function GroupPage(props: {
props = usePropz(props, getStaticPropz) ?? {
contractsCount: 0,
group: null,
- memberIds: [],
+ members: [],
creator: null,
+ traderScores: {},
topTraders: [],
+ creatorScores: {},
topCreators: [],
messages: [],
suggestedFilter: 'open',
}
- const { contractsCount, creator, topTraders, topCreators, suggestedFilter } =
- props
+ const {
+ contractsCount,
+ creator,
+ traderScores,
+ topTraders,
+ creatorScores,
+ topCreators,
+ suggestedFilter,
+ } = props
const router = useRouter()
const { slugs } = router.query as { slugs: string[] }
@@ -143,7 +175,7 @@ export default function GroupPage(props: {
const user = useUser()
const isAdmin = useAdmin()
- const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds
+ const members = useMembers(group?.id) ?? props.members
useSaveReferral(user, {
defaultReferrerUsername: creator.username,
@@ -154,25 +186,18 @@ export default function GroupPage(props: {
return
}
const isCreator = user && group && user.id === group.creatorId
- const isMember = user && memberIds.includes(user.id)
- const maxLeaderboardSize = 50
+ const isMember = user && members.map((m) => m.id).includes(user.id)
const leaderboard = (
-
-
-
-
+
)
@@ -191,7 +216,7 @@ export default function GroupPage(props: {
creator={creator}
isCreator={!!isCreator}
user={user}
- memberIds={memberIds}
+ members={members}
/>
)
@@ -287,9 +312,9 @@ function GroupOverview(props: {
creator: User
user: User | null | undefined
isCreator: boolean
- memberIds: string[]
+ members: User[]
}) {
- const { group, creator, isCreator, user, memberIds } = props
+ const { group, creator, isCreator, user, members } = props
const anyoneCanJoinChoices: { [key: string]: string } = {
Closed: 'false',
Open: 'true',
@@ -308,7 +333,7 @@ function GroupOverview(props: {
const shareUrl = `https://${ENV_CONFIG.domain}${groupPath(
group.slug
)}${postFix}`
- const isMember = user ? memberIds.includes(user.id) : false
+ const isMember = user ? members.map((m) => m.id).includes(user.id) : false
return (
<>
@@ -374,37 +399,155 @@ function GroupOverview(props: {
/>
)}
+
+
+ Members
+
+
>
)
}
-function GroupLeaderboard(props: {
- topUsers: { user: User; score: number }[]
+function SearchBar(props: { setQuery: (query: string) => void }) {
+ const { setQuery } = props
+ const debouncedQuery = debounce(setQuery, 50)
+ return (
+
+
+ debouncedQuery(e.target.value)}
+ placeholder="Find a member"
+ className="input input-bordered mb-4 w-full pl-12"
+ />
+
+ )
+}
+
+function GroupMemberSearch(props: { members: User[]; group: Group }) {
+ const [query, setQuery] = useState('')
+ const { group } = props
+ let { members } = props
+
+ // Use static members on load, but also listen to member changes:
+ const listenToMembers = useMembers(group.id)
+ if (listenToMembers) {
+ members = listenToMembers
+ }
+
+ // TODO use find-active-contracts to sort by?
+ const matches = sortBy(members, [(member) => member.name]).filter((m) =>
+ searchInAny(query, m.name, m.username)
+ )
+ const matchLimit = 25
+
+ return (
+
+
+
+ {matches.length > 0 && (
+
m.id)} />
+ )}
+ {matches.length > 25 && (
+
+ And {matches.length - matchLimit} more...
+
+ )}
+
+
+ )
+}
+
+function SortedLeaderboard(props: {
+ users: User[]
+ scoreFunction: (user: User) => number
title: string
- maxToShow: number
header: string
+ maxToShow?: number
}) {
- const { topUsers, title, maxToShow, header } = props
-
- const scoresByUser = topUsers.reduce((acc, { user, score }) => {
- acc[user.id] = score
- return acc
- }, {} as { [key: string]: number })
-
+ const { users, scoreFunction, title, header, maxToShow } = props
+ const sortedUsers = users.sort((a, b) => scoreFunction(b) - scoreFunction(a))
return (
t.user)}
+ users={sortedUsers}
title={title}
columns={[
- { header, renderCell: (user) => formatMoney(scoresByUser[user.id]) },
+ { header, renderCell: (user) => formatMoney(scoreFunction(user)) },
]}
maxToShow={maxToShow}
/>
)
}
+function GroupLeaderboards(props: {
+ traderScores: { [userId: string]: number }
+ creatorScores: { [userId: string]: number }
+ topTraders: User[]
+ topCreators: User[]
+ members: User[]
+ user: User | null | undefined
+}) {
+ const { traderScores, creatorScores, members, topTraders, topCreators } =
+ props
+ const maxToShow = 50
+ // Consider hiding M$0
+ // If it's just one member (curator), show all bettors, otherwise just show members
+ return (
+
+
+ {members.length > 1 ? (
+ <>
+ traderScores[user.id] ?? 0}
+ title="🏅 Top traders"
+ header="Profit"
+ maxToShow={maxToShow}
+ />
+ creatorScores[user.id] ?? 0}
+ title="🏅 Top creators"
+ header="Market volume"
+ maxToShow={maxToShow}
+ />
+ >
+ ) : (
+ <>
+ formatMoney(traderScores[user.id] ?? 0),
+ },
+ ]}
+ maxToShow={maxToShow}
+ />
+
+ formatMoney(creatorScores[user.id] ?? 0),
+ },
+ ]}
+ maxToShow={maxToShow}
+ />
+ >
+ )}
+
+
+ )
+}
+
function AddContractButton(props: { group: Group; user: User }) {
const { group, user } = props
const [open, setOpen] = useState(false)
@@ -541,15 +684,3 @@ function JoinGroupButton(props: {
)
}
-
-const toTopUsers = async (
- cachedUserIds: { userId: string; score: number }[]
-): Promise<{ user: User; score: number }[]> =>
- (
- await Promise.all(
- cachedUserIds.map(async (e) => {
- const user = await getUser(e.userId)
- return { user, score: e.score ?? 0 }
- })
- )
- ).filter((e) => e.user != null)
From 5c6328ffc263fb2bcbe8cbb6862b6bc488208fb5 Mon Sep 17 00:00:00 2001
From: Ian Philips