Cache likes and liked by user ids on contract

This commit is contained in:
Ian Philips 2022-08-30 08:38:07 -06:00
parent 2e502a774c
commit 73b75c3d9b
10 changed files with 80 additions and 34 deletions

View File

@ -59,6 +59,8 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
popularityScore?: number popularityScore?: number
followerCount?: number followerCount?: number
featuredOnHomeRank?: number featuredOnHomeRank?: number
likedByUserIds?: string[]
likedByUserCount?: number
} & T } & T
export type BinaryContract = Contract & Binary export type BinaryContract = Contract & Binary

View File

@ -1,7 +1,7 @@
export type Like = { export type Like = {
id: string id: string // will be id of the object liked, i.e. contract.id
userId: string userId: string
contractId: string type: 'contract'
createdTime: number createdTime: number
tipTxnId?: string tipTxnId?: string
} }

View File

@ -32,6 +32,7 @@ export * from './reset-betting-streaks'
export * from './reset-weekly-emails-flag' export * from './reset-weekly-emails-flag'
export * from './on-update-contract-follow' export * from './on-update-contract-follow'
export * from './on-create-like' export * from './on-create-like'
export * from './on-delete-like'
// v2 // v2
export * from './health' export * from './health'

View File

@ -4,6 +4,7 @@ import { Like } from '../../common/like'
import { getContract, getUser, log } from './utils' import { getContract, getUser, log } from './utils'
import { createLikeNotification } from './create-notification' import { createLikeNotification } from './create-notification'
import { TipTxn } from '../../common/txn' import { TipTxn } from '../../common/txn'
import { uniq } from 'lodash'
const firestore = admin.firestore() const firestore = admin.firestore()
@ -12,11 +13,28 @@ export const onCreateLike = functions.firestore
.onCreate(async (change, context) => { .onCreate(async (change, context) => {
const like = change.data() as Like const like = change.data() as Like
const { eventId } = context const { eventId } = context
await handleCreateLike(like, eventId) if (like.type === 'contract') {
await handleCreateLikeNotification(like, eventId)
await updateContractLikes(like)
}
}) })
const handleCreateLike = async (like: Like, eventId: string) => { const updateContractLikes = async (like: Like) => {
const contract = await getContract(like.contractId) const contract = await getContract(like.id)
if (!contract) {
log('Could not find contract')
return
}
const likedByUserIds = uniq(contract.likedByUserIds ?? [])
likedByUserIds.push(like.userId)
await firestore
.collection('contracts')
.doc(like.id)
.update({ likedByUserIds, likedByUserCount: likedByUserIds.length })
}
const handleCreateLikeNotification = async (like: Like, eventId: string) => {
const contract = await getContract(like.id)
if (!contract) { if (!contract) {
log('Could not find contract') log('Could not find contract')
return return

View File

@ -0,0 +1,32 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { Like } from '../../common/like'
import { getContract, log } from './utils'
import { uniq } from 'lodash'
const firestore = admin.firestore()
export const onDeleteLike = functions.firestore
.document('users/{userId}/likes/{likeId}')
.onDelete(async (change) => {
const like = change.data() as Like
if (like.type === 'contract') {
await removeContractLike(like)
}
})
const removeContractLike = async (like: Like) => {
const contract = await getContract(like.id)
if (!contract) {
log('Could not find contract')
return
}
const likedByUserIds = uniq(contract.likedByUserIds ?? [])
const newLikedByUserIds = likedByUserIds.filter(
(userId) => userId !== like.userId
)
await firestore.collection('contracts').doc(like.id).update({
likedByUserIds: newLikedByUserIds,
likedByUserCount: newLikedByUserIds.length,
})
}

View File

@ -2,6 +2,7 @@ import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { FieldValue } from 'firebase-admin/firestore' import { FieldValue } from 'firebase-admin/firestore'
// TODO: should cache the follower user ids in the contract as these triggers aren't idempotent
export const onDeleteContractFollow = functions.firestore export const onDeleteContractFollow = functions.firestore
.document('contracts/{contractId}/follows/{userId}') .document('contracts/{contractId}/follows/{userId}')
.onDelete(async (change, context) => { .onDelete(async (change, context) => {

View File

@ -19,7 +19,9 @@ export function LikeMarketButton(props: {
const { contract, user } = props const { contract, user } = props
const likes = useUserLikes(user?.id) const likes = useUserLikes(user?.id)
const likedContractIds = likes?.map((l) => l.contractId) const likedContractIds = likes
?.filter((l) => l.type === 'contract')
.map((l) => l.id)
if (!user) return <div /> if (!user) return <div />
const onLike = async () => { const onLike = async () => {

View File

@ -35,10 +35,8 @@ export function UserLikesButton(props: { user: User }) {
{likedContract.question} {likedContract.question}
</SiteLink> </SiteLink>
<XIcon <XIcon
className="ml-2 h-5 w-5 cursor-pointer" className="ml-2 h-5 w-5 shrink-0 cursor-pointer"
onClick={() => async () => { onClick={() => unLikeContract(user.id, likedContract.id)}
await unLikeContract(user.id, likedContract.id)
}}
/> />
</Row> </Row>
))} ))}

View File

@ -15,21 +15,24 @@ export const useUserLikes = (userId: string | undefined) => {
return contractIds return contractIds
} }
export const useUserLikedContracts = (userId: string | undefined) => { export const useUserLikedContracts = (userId: string | undefined) => {
const [contractIds, setContractIds] = useState<Like[] | undefined>() const [likes, setLikes] = useState<Like[] | undefined>()
const [contracts, setContracts] = useState<Contract[] | undefined>() const [contracts, setContracts] = useState<Contract[] | undefined>()
useEffect(() => { useEffect(() => {
if (userId) return listenForLikes(userId, setContractIds) if (userId)
return listenForLikes(userId, (likes) => {
setLikes(likes.filter((l) => l.type === 'contract'))
})
}, [userId]) }, [userId])
useEffect(() => { useEffect(() => {
if (contractIds) if (likes)
Promise.all( Promise.all(
contractIds.map(async (like) => { likes.map(async (like) => {
return await getContractFromId(like.contractId) return await getContractFromId(like.id)
}) })
).then((contracts) => setContracts(filterDefined(contracts))) ).then((contracts) => setContracts(filterDefined(contracts)))
}, [contractIds]) }, [likes])
return contracts return contracts
} }

View File

@ -1,19 +1,12 @@
import { import { collection, deleteDoc, doc, setDoc } from 'firebase/firestore'
collection,
deleteDoc,
doc,
query,
setDoc,
where,
} from 'firebase/firestore'
import { db } from 'web/lib/firebase/init' import { db } from 'web/lib/firebase/init'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { transact } from 'web/lib/firebase/api' import { transact } from 'web/lib/firebase/api'
import { removeUndefinedProps } from 'common/lib/util/object' import { removeUndefinedProps } from 'common/util/object'
import { Like } from 'common/lib/like' import { Like } from 'common/like'
import { track } from '@amplitude/analytics-browser' import { track } from '@amplitude/analytics-browser'
import { User } from 'common/user' import { User } from 'common/user'
import { Contract } from 'common/lib/contract' import { Contract } from 'common/contract'
export const LIKE_TIP_AMOUNT = 5 export const LIKE_TIP_AMOUNT = 5
@ -22,11 +15,7 @@ function getLikesCollection(userId: string) {
} }
export const unLikeContract = async (userId: string, contractId: string) => { export const unLikeContract = async (userId: string, contractId: string) => {
const ref = await query( const ref = await doc(getLikesCollection(userId), contractId)
getLikesCollection(userId),
where('contractId', '==', contractId)
)
const snapshot = await ref.get()
return await deleteDoc(ref) return await deleteDoc(ref)
} }
@ -51,13 +40,13 @@ export const likeContract = async (user: User, contract: Contract) => {
console.log('result', result) console.log('result', result)
} }
// create new like in db under users collection // create new like in db under users collection
const ref = doc(getLikesCollection(user.id)) const ref = doc(getLikesCollection(user.id), contract.id)
// contract slug and question are set via trigger // contract slug and question are set via trigger
const like = removeUndefinedProps({ const like = removeUndefinedProps({
id: ref.id, id: ref.id,
userId: user.id, userId: user.id,
createdTime: Date.now(), createdTime: Date.now(),
contractId: contract.id, type: 'contract',
tipTxnId: result.txn.id, tipTxnId: result.txn.id,
} as Like) } as Like)
track('like', { track('like', {