diff --git a/common/contract.ts b/common/contract.ts index 2b330201..5dc4b696 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -59,6 +59,8 @@ export type Contract = { popularityScore?: number followerCount?: number featuredOnHomeRank?: number + likedByUserIds?: string[] + likedByUserCount?: number } & T export type BinaryContract = Contract & Binary diff --git a/common/like.ts b/common/like.ts index 1b9ce481..2b677ba5 100644 --- a/common/like.ts +++ b/common/like.ts @@ -1,7 +1,7 @@ export type Like = { - id: string + id: string // will be id of the object liked, i.e. contract.id userId: string - contractId: string + type: 'contract' createdTime: number tipTxnId?: string } diff --git a/functions/src/index.ts b/functions/src/index.ts index a5909748..6ede39a0 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -32,6 +32,7 @@ export * from './reset-betting-streaks' export * from './reset-weekly-emails-flag' export * from './on-update-contract-follow' export * from './on-create-like' +export * from './on-delete-like' // v2 export * from './health' diff --git a/functions/src/on-create-like.ts b/functions/src/on-create-like.ts index 80fc88a2..8c5885b0 100644 --- a/functions/src/on-create-like.ts +++ b/functions/src/on-create-like.ts @@ -4,6 +4,7 @@ import { Like } from '../../common/like' import { getContract, getUser, log } from './utils' import { createLikeNotification } from './create-notification' import { TipTxn } from '../../common/txn' +import { uniq } from 'lodash' const firestore = admin.firestore() @@ -12,11 +13,28 @@ export const onCreateLike = functions.firestore .onCreate(async (change, context) => { const like = change.data() as Like 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 contract = await getContract(like.contractId) +const updateContractLikes = async (like: Like) => { + 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) { log('Could not find contract') return diff --git a/functions/src/on-delete-like.ts b/functions/src/on-delete-like.ts new file mode 100644 index 00000000..151614b0 --- /dev/null +++ b/functions/src/on-delete-like.ts @@ -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, + }) +} diff --git a/functions/src/on-update-contract-follow.ts b/functions/src/on-update-contract-follow.ts index f7d54fe8..20ef8e30 100644 --- a/functions/src/on-update-contract-follow.ts +++ b/functions/src/on-update-contract-follow.ts @@ -2,6 +2,7 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' 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 .document('contracts/{contractId}/follows/{userId}') .onDelete(async (change, context) => { diff --git a/web/components/contract/like-market-button.tsx b/web/components/contract/like-market-button.tsx index 080f7c77..244baca5 100644 --- a/web/components/contract/like-market-button.tsx +++ b/web/components/contract/like-market-button.tsx @@ -19,7 +19,9 @@ export function LikeMarketButton(props: { const { contract, user } = props 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
const onLike = async () => { diff --git a/web/components/profile/user-likes-button.tsx b/web/components/profile/user-likes-button.tsx index e3d171fc..9a8a9653 100644 --- a/web/components/profile/user-likes-button.tsx +++ b/web/components/profile/user-likes-button.tsx @@ -35,10 +35,8 @@ export function UserLikesButton(props: { user: User }) { {likedContract.question} async () => { - await unLikeContract(user.id, likedContract.id) - }} + className="ml-2 h-5 w-5 shrink-0 cursor-pointer" + onClick={() => unLikeContract(user.id, likedContract.id)} /> ))} diff --git a/web/hooks/use-likes.ts b/web/hooks/use-likes.ts index 56230c66..dcd225df 100644 --- a/web/hooks/use-likes.ts +++ b/web/hooks/use-likes.ts @@ -15,21 +15,24 @@ export const useUserLikes = (userId: string | undefined) => { return contractIds } export const useUserLikedContracts = (userId: string | undefined) => { - const [contractIds, setContractIds] = useState() + const [likes, setLikes] = useState() const [contracts, setContracts] = useState() useEffect(() => { - if (userId) return listenForLikes(userId, setContractIds) + if (userId) + return listenForLikes(userId, (likes) => { + setLikes(likes.filter((l) => l.type === 'contract')) + }) }, [userId]) useEffect(() => { - if (contractIds) + if (likes) Promise.all( - contractIds.map(async (like) => { - return await getContractFromId(like.contractId) + likes.map(async (like) => { + return await getContractFromId(like.id) }) ).then((contracts) => setContracts(filterDefined(contracts))) - }, [contractIds]) + }, [likes]) return contracts } diff --git a/web/lib/firebase/likes.ts b/web/lib/firebase/likes.ts index 62620e2a..0e169b0b 100644 --- a/web/lib/firebase/likes.ts +++ b/web/lib/firebase/likes.ts @@ -1,19 +1,12 @@ -import { - collection, - deleteDoc, - doc, - query, - setDoc, - where, -} from 'firebase/firestore' +import { collection, deleteDoc, doc, setDoc } from 'firebase/firestore' import { db } from 'web/lib/firebase/init' import toast from 'react-hot-toast' import { transact } from 'web/lib/firebase/api' -import { removeUndefinedProps } from 'common/lib/util/object' -import { Like } from 'common/lib/like' +import { removeUndefinedProps } from 'common/util/object' +import { Like } from 'common/like' import { track } from '@amplitude/analytics-browser' import { User } from 'common/user' -import { Contract } from 'common/lib/contract' +import { Contract } from 'common/contract' export const LIKE_TIP_AMOUNT = 5 @@ -22,11 +15,7 @@ function getLikesCollection(userId: string) { } export const unLikeContract = async (userId: string, contractId: string) => { - const ref = await query( - getLikesCollection(userId), - where('contractId', '==', contractId) - ) - const snapshot = await ref.get() + const ref = await doc(getLikesCollection(userId), contractId) return await deleteDoc(ref) } @@ -51,13 +40,13 @@ export const likeContract = async (user: User, contract: Contract) => { console.log('result', result) } // 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 const like = removeUndefinedProps({ id: ref.id, userId: user.id, createdTime: Date.now(), - contractId: contract.id, + type: 'contract', tipTxnId: result.txn.id, } as Like) track('like', {