Allow to follow/unfollow markets, backfill as well (#794)
* Allow to follow/unfollow markets, backfill as well * remove yarn script edit * add decrement comment * Lint * Decrement follow count on unfollow * Follow/unfollow button logic * Unfollow/follow => heart * Add user to followers in place-bet and sell-shares * Add tracking * Show contract follow modal for first time following * Increment follower count as well * Remove add follow from bet trigger * restore on-create-bet * Add pubsub to dev.sh, show heart on FR, remove from answer triggerpull/796/head
parent
78780a9219
commit
f50b4775a1
@ -0,0 +1,36 @@
|
||||
import * as admin from 'firebase-admin'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const addUserToContractFollowers = async (
|
||||
contractId: string,
|
||||
userId: string
|
||||
) => {
|
||||
const followerDoc = await firestore
|
||||
.collection(`contracts/${contractId}/follows`)
|
||||
.doc(userId)
|
||||
.get()
|
||||
if (followerDoc.exists) return
|
||||
await firestore
|
||||
.collection(`contracts/${contractId}/follows`)
|
||||
.doc(userId)
|
||||
.set({
|
||||
id: userId,
|
||||
createdTime: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
export const removeUserFromContractFollowers = async (
|
||||
contractId: string,
|
||||
userId: string
|
||||
) => {
|
||||
const followerDoc = await firestore
|
||||
.collection(`contracts/${contractId}/follows`)
|
||||
.doc(userId)
|
||||
.get()
|
||||
if (!followerDoc.exists) return
|
||||
await firestore
|
||||
.collection(`contracts/${contractId}/follows`)
|
||||
.doc(userId)
|
||||
.delete()
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { FieldValue } from 'firebase-admin/firestore'
|
||||
|
||||
export const onDeleteContractFollow = functions.firestore
|
||||
.document('contracts/{contractId}/follows/{userId}')
|
||||
.onDelete(async (change, context) => {
|
||||
const { contractId } = context.params as {
|
||||
contractId: string
|
||||
}
|
||||
const firestore = admin.firestore()
|
||||
const contract = await firestore
|
||||
.collection(`contracts`)
|
||||
.doc(contractId)
|
||||
.get()
|
||||
if (!contract.exists) throw new Error('Could not find contract')
|
||||
|
||||
await firestore
|
||||
.collection(`contracts`)
|
||||
.doc(contractId)
|
||||
.update({
|
||||
followerCount: FieldValue.increment(-1),
|
||||
})
|
||||
})
|
||||
|
||||
export const onCreateContractFollow = functions.firestore
|
||||
.document('contracts/{contractId}/follows/{userId}')
|
||||
.onCreate(async (change, context) => {
|
||||
const { contractId } = context.params as {
|
||||
contractId: string
|
||||
}
|
||||
const firestore = admin.firestore()
|
||||
const contract = await firestore
|
||||
.collection(`contracts`)
|
||||
.doc(contractId)
|
||||
.get()
|
||||
if (!contract.exists) throw new Error('Could not find contract')
|
||||
|
||||
await firestore
|
||||
.collection(`contracts`)
|
||||
.doc(contractId)
|
||||
.update({
|
||||
followerCount: FieldValue.increment(1),
|
||||
})
|
||||
})
|
@ -0,0 +1,75 @@
|
||||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin()
|
||||
|
||||
import { getValues } from '../utils'
|
||||
import { Contract } from 'common/lib/contract'
|
||||
import { Comment } from 'common/lib/comment'
|
||||
import { uniq } from 'lodash'
|
||||
import { Bet } from 'common/lib/bet'
|
||||
import {
|
||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
} from 'common/lib/antes'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function backfillContractFollowers() {
|
||||
console.log('Backfilling contract followers')
|
||||
const contracts = await getValues<Contract>(
|
||||
firestore.collection('contracts').where('isResolved', '==', false)
|
||||
)
|
||||
let count = 0
|
||||
for (const contract of contracts) {
|
||||
const comments = await getValues<Comment>(
|
||||
firestore.collection('contracts').doc(contract.id).collection('comments')
|
||||
)
|
||||
const commenterIds = uniq(comments.map((comment) => comment.userId))
|
||||
const betsSnap = await firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.get()
|
||||
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
// filter bets for only users that have an amount invested still
|
||||
const bettorIds = uniq(bets.map((bet) => bet.userId))
|
||||
const liquidityProviders = await firestore
|
||||
.collection(`contracts/${contract.id}/liquidity`)
|
||||
.get()
|
||||
const liquidityProvidersIds = uniq(
|
||||
liquidityProviders.docs.map((doc) => doc.data().userId)
|
||||
// exclude free market liquidity provider
|
||||
).filter(
|
||||
(id) =>
|
||||
id !== HOUSE_LIQUIDITY_PROVIDER_ID ||
|
||||
id !== DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
)
|
||||
const followerIds = uniq([
|
||||
...commenterIds,
|
||||
...bettorIds,
|
||||
...liquidityProvidersIds,
|
||||
contract.creatorId,
|
||||
])
|
||||
for (const followerId of followerIds) {
|
||||
await firestore
|
||||
.collection(`contracts/${contract.id}/follows`)
|
||||
.doc(followerId)
|
||||
.set({ id: followerId, createdTime: Date.now() })
|
||||
}
|
||||
// Perhaps handled by the trigger?
|
||||
// const followerCount = followerIds.length
|
||||
// await firestore
|
||||
// .collection(`contracts`)
|
||||
// .doc(contract.id)
|
||||
// .update({ followerCount: followerCount })
|
||||
count += 1
|
||||
if (count % 100 === 0) {
|
||||
console.log(`${count} contracts processed`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
backfillContractFollowers()
|
||||
.then(() => process.exit())
|
||||
.catch(console.log)
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import { Button } from 'web/components/button'
|
||||
import {
|
||||
Contract,
|
||||
followContract,
|
||||
unFollowContract,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import toast from 'react-hot-toast'
|
||||
import { CheckIcon, HeartIcon } from '@heroicons/react/outline'
|
||||
import clsx from 'clsx'
|
||||
import { User } from 'common/user'
|
||||
import { useContractFollows } from 'web/hooks/use-follows'
|
||||
import { firebaseLogin, updateUser } from 'web/lib/firebase/users'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { FollowMarketModal } from 'web/components/contract/follow-market-modal'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const FollowMarketButton = (props: {
|
||||
contract: Contract
|
||||
user: User | undefined | null
|
||||
}) => {
|
||||
const { contract, user } = props
|
||||
const followers = useContractFollows(contract.id)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<Button
|
||||
size={'lg'}
|
||||
color={'gray-white'}
|
||||
onClick={async () => {
|
||||
if (!user) return firebaseLogin()
|
||||
if (followers?.includes(user.id)) {
|
||||
await unFollowContract(contract.id, user.id)
|
||||
toast('Notifications from this market are now silenced.', {
|
||||
icon: <CheckIcon className={'text-primary h-5 w-5'} />,
|
||||
})
|
||||
track('Unfollow Market', {
|
||||
slug: contract.slug,
|
||||
})
|
||||
} else {
|
||||
await followContract(contract.id, user.id)
|
||||
toast('You are now following this market!', {
|
||||
icon: <CheckIcon className={'text-primary h-5 w-5'} />,
|
||||
})
|
||||
track('Follow Market', {
|
||||
slug: contract.slug,
|
||||
})
|
||||
}
|
||||
if (!user.hasSeenContractFollowModal) {
|
||||
await updateUser(user.id, {
|
||||
hasSeenContractFollowModal: true,
|
||||
})
|
||||
setOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{followers?.includes(user?.id ?? 'nope') ? (
|
||||
<HeartIcon
|
||||
className={clsx('h-6 w-6 fill-red-600 stroke-red-600 xl:h-7 xl:w-7')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<HeartIcon
|
||||
className={clsx('h-6 w-6 xl:h-7 xl:w-7')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<FollowMarketModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title={`You ${
|
||||
followers?.includes(user?.id ?? 'nope') ? 'followed' : 'unfollowed'
|
||||
} a question!`}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
Loading…
Reference in new issue