Tipping in posts (#1045)

* Tipping in posts

* Rm itemType field
This commit is contained in:
FRC 2022-10-14 13:05:07 +01:00 committed by GitHub
parent 0a70652667
commit 4d214c01b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 71 additions and 53 deletions

View File

@ -10,6 +10,7 @@ export type AnyOutcomeType =
| PseudoNumeric | PseudoNumeric
| FreeResponse | FreeResponse
| Numeric | Numeric
export type AnyContractType = export type AnyContractType =
| (CPMM & Binary) | (CPMM & Binary)
| (CPMM & PseudoNumeric) | (CPMM & PseudoNumeric)

View File

@ -1,7 +1,7 @@
export type Like = { export type Like = {
id: string // will be id of the object liked, i.e. contract.id id: string // will be id of the object liked, i.e. contract.id
userId: string userId: string
type: 'contract' type: 'contract' | 'post'
createdTime: number createdTime: number
tipTxnId?: string // only holds most recent tip txn id tipTxnId?: string // only holds most recent tip txn id
} }

View File

@ -13,6 +13,9 @@ export type Post = {
creatorName: string creatorName: string
creatorUsername: string creatorUsername: string
creatorAvatarUrl?: string creatorAvatarUrl?: string
likedByUserIds?: string[]
likedByUserCount?: number
} }
export type DateDoc = Post & { export type DateDoc = Post & {

View File

@ -103,6 +103,7 @@ export const createpost = newEndpoint({}, async (req, auth) => {
creatorName: creator.name, creatorName: creator.name,
creatorUsername: creator.username, creatorUsername: creator.username,
creatorAvatarUrl: creator.avatarUrl, creatorAvatarUrl: creator.avatarUrl,
itemType: 'post',
}) })
await postRef.create(post) await postRef.create(post)

View File

@ -6,7 +6,7 @@ import { IconButton } from 'web/components/button'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { ShareModal } from './share-modal' import { ShareModal } from './share-modal'
import { FollowMarketButton } from 'web/components/follow-market-button' import { FollowMarketButton } from 'web/components/follow-market-button'
import { LikeMarketButton } from 'web/components/contract/like-market-button' import { LikeItemButton } from 'web/components/contract/like-item-button'
import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog' import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog'
import { Tooltip } from '../tooltip' import { Tooltip } from '../tooltip'
@ -19,7 +19,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
<Row className="gap-1"> <Row className="gap-1">
<FollowMarketButton contract={contract} user={user} /> <FollowMarketButton contract={contract} user={user} />
<LikeMarketButton contract={contract} user={user} /> <LikeItemButton item={contract} user={user} itemType={'contract'} />
<Tooltip text="Share" placement="bottom" noTap noFade> <Tooltip text="Share" placement="bottom" noTap noFade>
<IconButton <IconButton

View File

@ -1,23 +1,25 @@
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { Contract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { useUserLikes } from 'web/hooks/use-likes' import { useUserLikes } from 'web/hooks/use-likes'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { likeContract } from 'web/lib/firebase/likes' import { likeItem } from 'web/lib/firebase/likes'
import { LIKE_TIP_AMOUNT, TIP_UNDO_DURATION } from 'common/like' import { LIKE_TIP_AMOUNT, TIP_UNDO_DURATION } from 'common/like'
import { firebaseLogin } from 'web/lib/firebase/users' import { firebaseLogin } from 'web/lib/firebase/users'
import { useMarketTipTxns } from 'web/hooks/use-tip-txns' import { useItemTipTxns } from 'web/hooks/use-tip-txns'
import { sum } from 'lodash' import { sum } from 'lodash'
import { TipButton } from './tip-button' import { TipButton } from './tip-button'
import { Contract } from 'common/contract'
import { Post } from 'common/post'
import { TipToast } from '../tipper' import { TipToast } from '../tipper'
export function LikeMarketButton(props: { export function LikeItemButton(props: {
contract: Contract item: Contract | Post
user: User | null | undefined user: User | null | undefined
itemType: string
}) { }) {
const { contract, user } = props const { item, user, itemType } = props
const tips = useMarketTipTxns(contract.id) const tips = useItemTipTxns(item.id)
const totalTipped = useMemo(() => { const totalTipped = useMemo(() => {
return sum(tips.map((tip) => tip.amount)) return sum(tips.map((tip) => tip.amount))
@ -27,21 +29,22 @@ export function LikeMarketButton(props: {
const [isLiking, setIsLiking] = useState(false) const [isLiking, setIsLiking] = useState(false)
const userLikedContractIds = likes const userLikedItemIds = likes
?.filter((l) => l.type === 'contract') ?.filter((l) => l.type === 'contract' || l.type === 'post')
.map((l) => l.id) .map((l) => l.id)
const onLike = async () => { const onLike = async () => {
if (!user) return firebaseLogin() if (!user) return firebaseLogin()
setIsLiking(true) setIsLiking(true)
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
likeContract(user, contract).catch(() => setIsLiking(false)) likeItem(user, item, itemType).catch(() => setIsLiking(false))
}, 3000) }, 3000)
toast.custom( toast.custom(
() => ( () => (
<TipToast <TipToast
userName={contract.creatorUsername} userName={item.creatorUsername}
onUndoClick={() => { onUndoClick={() => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
}} }}
@ -59,10 +62,10 @@ export function LikeMarketButton(props: {
userTipped={ userTipped={
!!user && !!user &&
(isLiking || (isLiking ||
userLikedContractIds?.includes(contract.id) || userLikedItemIds?.includes(item.id) ||
(!likes && !!contract.likedByUserIds?.includes(user.id))) (!likes && !!item.likedByUserIds?.includes(user.id)))
} }
disabled={contract.creatorId === user?.id} disabled={item.creatorId === user?.id}
/> />
) )
} }

View File

@ -7,7 +7,7 @@ import { useUserLikedContracts } from 'web/hooks/use-likes'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { XIcon } from '@heroicons/react/outline' import { XIcon } from '@heroicons/react/outline'
import { unLikeContract } from 'web/lib/firebase/likes' import { unLikeItem } from 'web/lib/firebase/likes'
import { contractPath } from 'web/lib/firebase/contracts' import { contractPath } from 'web/lib/firebase/contracts'
export function UserLikesButton(props: { user: User; className?: string }) { export function UserLikesButton(props: { user: User; className?: string }) {
@ -36,7 +36,7 @@ export function UserLikesButton(props: { user: User; className?: string }) {
</SiteLink> </SiteLink>
<XIcon <XIcon
className="ml-2 h-5 w-5 shrink-0 cursor-pointer" className="ml-2 h-5 w-5 shrink-0 cursor-pointer"
onClick={() => unLikeContract(user.id, likedContract.id)} onClick={() => unLikeItem(user.id, likedContract.id)}
/> />
</Row> </Row>
))} ))}

View File

@ -33,14 +33,14 @@ export function useTipTxns(on: {
}, [txns]) }, [txns])
} }
export function useMarketTipTxns(contractId: string): TipTxn[] { export function useItemTipTxns(itemId: string): TipTxn[] {
const [txns, setTxns] = useState<TipTxn[]>([]) const [txns, setTxns] = useState<TipTxn[]>([])
useEffect(() => { useEffect(() => {
return listenForTipTxns(contractId, (txns) => { return listenForTipTxns(itemId, (txns) => {
setTxns(txns.filter((txn) => !txn.data.commentId)) setTxns(txns.filter((txn) => !txn.data.commentId))
}) })
}, [contractId]) }, [itemId])
return txns return txns
} }

View File

@ -6,18 +6,23 @@ import { removeUndefinedProps } from 'common/util/object'
import { Like, LIKE_TIP_AMOUNT } from 'common/like' import { Like, LIKE_TIP_AMOUNT } 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 { Post } from 'common/post'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
function getLikesCollection(userId: string) { function getLikesCollection(userId: string) {
return collection(db, 'users', userId, 'likes') return collection(db, 'users', userId, 'likes')
} }
export const unLikeContract = async (userId: string, contractId: string) => { export const unLikeItem = async (userId: string, itemId: string) => {
const ref = await doc(getLikesCollection(userId), contractId) const ref = await doc(getLikesCollection(userId), itemId)
return await deleteDoc(ref) return await deleteDoc(ref)
} }
export const likeContract = async (user: User, contract: Contract) => { export const likeItem = async (
user: User,
item: Contract | Post,
itemType: string
) => {
if (user.balance < LIKE_TIP_AMOUNT) { if (user.balance < LIKE_TIP_AMOUNT) {
toast('You do not have enough M$ to like this market!') toast('You do not have enough M$ to like this market!')
return return
@ -28,27 +33,27 @@ export const likeContract = async (user: User, contract: Contract) => {
amount: LIKE_TIP_AMOUNT, amount: LIKE_TIP_AMOUNT,
fromId: user.id, fromId: user.id,
fromType: 'USER', fromType: 'USER',
toId: contract.creatorId, toId: item.creatorId,
toType: 'USER', toType: 'USER',
token: 'M$', token: 'M$',
category: 'TIP', category: 'TIP',
data: { contractId: contract.id }, data: { contractId: item.id },
description: `${user.name} liked contract ${contract.id} for M$ ${LIKE_TIP_AMOUNT} to ${contract.creatorId} `, description: `${user.name} liked ${itemType}${item.id} for M$ ${LIKE_TIP_AMOUNT} to ${item.creatorId} `,
}) })
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), contract.id) const ref = doc(getLikesCollection(user.id), item.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(),
type: 'contract', type: itemType,
tipTxnId: result.txn.id, tipTxnId: result.txn.id,
} as Like) } as Like)
track('like', { track('like', {
contractId: contract.id, itemId: item.id,
}) })
await setDoc(ref, like) await setDoc(ref, like)
} }

View File

@ -26,6 +26,7 @@ import { useUser } from 'web/hooks/use-user'
import { usePost } from 'web/hooks/use-post' import { usePost } from 'web/hooks/use-post'
import { SEO } from 'web/components/SEO' import { SEO } from 'web/components/SEO'
import { Subtitle } from 'web/components/subtitle' import { Subtitle } from 'web/components/subtitle'
import { LikeItemButton } from 'web/components/contract/like-item-button'
export async function getStaticProps(props: { params: { slugs: string[] } }) { export async function getStaticProps(props: { params: { slugs: string[] } }) {
const { slugs } = props.params const { slugs } = props.params
@ -81,7 +82,7 @@ export default function PostPage(props: {
<br /> <br />
<Subtitle className="!mt-2 px-2 pb-4" text={post.subtitle} /> <Subtitle className="!mt-2 px-2 pb-4" text={post.subtitle} />
</div> </div>
<Row> <Row className="items-center">
<Col className="flex-1 px-2"> <Col className="flex-1 px-2">
<div className={'inline-flex'}> <div className={'inline-flex'}>
<div className="mr-1 text-gray-500">Created by</div> <div className="mr-1 text-gray-500">Created by</div>
@ -92,6 +93,9 @@ export default function PostPage(props: {
/> />
</div> </div>
</Col> </Col>
<Row className="items-center">
<LikeItemButton item={post} user={user} itemType={'post'} />
<Col className="px-2"> <Col className="px-2">
<Button <Button
size="lg" size="lg"
@ -114,6 +118,7 @@ export default function PostPage(props: {
</Button> </Button>
</Col> </Col>
</Row> </Row>
</Row>
<Spacer h={2} /> <Spacer h={2} />
<div className="rounded-lg bg-white px-6 py-4 sm:py-0"> <div className="rounded-lg bg-white px-6 py-4 sm:py-0">