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:
parent
7a09365f00
commit
67d0a6c0c2
4
common/follow.ts
Normal file
4
common/follow.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export type Follow = {
|
||||
userId: string
|
||||
timestamp: number
|
||||
}
|
|
@ -18,6 +18,8 @@ export type User = {
|
|||
totalPnLCached: number
|
||||
creatorVolumeCached: number
|
||||
|
||||
followerCountCached: number
|
||||
|
||||
followedCategories?: string[]
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ export const createUser = functions
|
|||
createdTime: Date.now(),
|
||||
totalPnLCached: 0,
|
||||
creatorVolumeCached: 0,
|
||||
followerCountCached: 0,
|
||||
followedCategories: DEFAULT_CATEGORIES,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
18
functions/src/on-unfollow-user.ts
Normal file
18
functions/src/on-unfollow-user.ts
Normal 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()
|
46
functions/src/scripts/backfill-followers.ts
Normal file
46
functions/src/scripts/backfill-followers.ts
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user