Create Top Followed Users leaderboard (#531)

* Create Top Followed Users leaderboard

* Switch to increment/decrement approach for caching user follower counts

* Backfill script for user follow counts

* Appease ESLint

* Address review comment

Co-authored-by: James Grugett <jahooma@gmail.com>
This commit is contained in:
Ben Congdon 2022-06-22 09:05:54 -07:00 committed by GitHub
parent 7a09365f00
commit 67d0a6c0c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 4 deletions

4
common/follow.ts Normal file
View File

@ -0,0 +1,4 @@
export type Follow = {
userId: string
timestamp: number
}

View File

@ -18,6 +18,8 @@ export type User = {
totalPnLCached: number
creatorVolumeCached: number
followerCountCached: number
followedCategories?: string[]
}

View File

@ -72,6 +72,7 @@ export const createUser = functions
createdTime: Date.now(),
totalPnLCached: 0,
creatorVolumeCached: 0,
followerCountCached: 0,
followedCategories: DEFAULT_CATEGORIES,
}

View File

@ -25,6 +25,7 @@ export * from './on-create-answer'
export * from './on-update-contract'
export * from './on-create-contract'
export * from './on-follow-user'
export * from './on-unfollow-user'
export * from './on-create-liquidity-provision'
// v2

View File

@ -1,12 +1,16 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { getUser } from './utils'
import { createNotification } from './create-notification'
import { FieldValue } from 'firebase-admin/firestore'
export const onFollowUser = functions.firestore
.document('users/{userId}/follows/{followedUserId}')
.onCreate(async (change, context) => {
const { userId } = context.params as {
const { userId, followedUserId } = context.params as {
userId: string
followedUserId: string
}
const { eventId } = context
@ -15,6 +19,10 @@ export const onFollowUser = functions.firestore
const followingUser = await getUser(userId)
if (!followingUser) throw new Error('Could not find following user')
await firestore.doc(`users/${followedUserId}`).update({
followerCountCached: FieldValue.increment(1),
})
await createNotification(
followingUser.id,
'follow',
@ -27,3 +35,5 @@ export const onFollowUser = functions.firestore
follow.userId
)
})
const firestore = admin.firestore()

View File

@ -0,0 +1,18 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { FieldValue } from 'firebase-admin/firestore'
export const onUnfollowUser = functions.firestore
.document('users/{userId}/follows/{followedUserId}')
.onDelete(async (change, context) => {
const { followedUserId } = context.params as {
followedUserId: string
}
await firestore.doc(`users/${followedUserId}`).update({
followerCountCached: FieldValue.increment(-1),
})
})
const firestore = admin.firestore()

View File

@ -0,0 +1,46 @@
import * as admin from 'firebase-admin'
import { initAdmin } from './script-init'
initAdmin()
import { getValues } from '../utils'
import { User } from 'common/user'
import { Follow } from 'common/follow'
const firestore = admin.firestore()
async function backfillFollowers() {
console.log('Backfilling user follower counts')
const followerCounts: { [userId: string]: number } = {}
const users = await getValues<User>(firestore.collection('users'))
console.log(`Loaded ${users.length} users. Calculating follower counts...`)
for (const [idx, user] of users.entries()) {
console.log(`Querying user ${user.id} (${idx + 1}/${users.length})`)
const follows = await getValues<Follow>(
firestore.collection('users').doc(user.id).collection('follows')
)
for (const follow of follows) {
followerCounts[follow.userId] = (followerCounts[follow.userId] || 0) + 1
}
}
console.log(
`Finished calculating follower counts. Persisting cached follower counts...`
)
for (const [idx, user] of users.entries()) {
console.log(`Persisting user ${user.id} (${idx + 1}/${users.length})`)
const followerCount = followerCounts[user.id] || 0
await firestore
.collection('users')
.doc(user.id)
.update({ followerCountCached: followerCount })
}
}
if (require.main === module) {
backfillFollowers()
.then(() => process.exit())
.catch(console.log)
}

View File

@ -195,6 +195,17 @@ const topCreatorsQuery = query(
limit(20)
)
export async function getTopFollowed() {
const users = await getValues<User>(topFollowedQuery)
return users.slice(0, 20)
}
const topFollowedQuery = query(
collection(db, 'users'),
orderBy('followerCountCached', 'desc'),
limit(20)
)
export function getTopCreators() {
return getValues<User>(topCreatorsQuery)
}

View File

@ -1,22 +1,29 @@
import { Col } from 'web/components/layout/col'
import { Leaderboard } from 'web/components/leaderboard'
import { Page } from 'web/components/page'
import { getTopCreators, getTopTraders, User } from 'web/lib/firebase/users'
import {
getTopCreators,
getTopTraders,
getTopFollowed,
User,
} from 'web/lib/firebase/users'
import { formatMoney } from 'common/util/format'
import { fromPropz, usePropz } from 'web/hooks/use-propz'
import { useTracking } from 'web/hooks/use-tracking'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz() {
const [topTraders, topCreators] = await Promise.all([
const [topTraders, topCreators, topFollowed] = await Promise.all([
getTopTraders().catch(() => {}),
getTopCreators().catch(() => {}),
getTopFollowed().catch(() => {}),
])
return {
props: {
topTraders,
topCreators,
topFollowed,
},
revalidate: 60, // regenerate after a minute
@ -26,12 +33,14 @@ export async function getStaticPropz() {
export default function Leaderboards(props: {
topTraders: User[]
topCreators: User[]
topFollowed: User[]
}) {
props = usePropz(props, getStaticPropz) ?? {
topTraders: [],
topCreators: [],
topFollowed: [],
}
const { topTraders, topCreators } = props
const { topTraders, topCreators, topFollowed } = props
useTracking('view leaderboards')
@ -59,6 +68,18 @@ export default function Leaderboards(props: {
]}
/>
</Col>
<Col className="mx-4 my-10 w-1/2 items-center gap-10 lg:mx-0 lg:flex-row">
<Leaderboard
title="👀 Most followed"
users={topFollowed}
columns={[
{
header: 'Number of followers',
renderCell: (user) => user.followerCountCached,
},
]}
/>
</Col>
</Page>
)
}