Skip to content

Commit

Permalink
upvote and downvote comments (#3112)
Browse files Browse the repository at this point in the history
  • Loading branch information
ingawei authored Nov 13, 2024
1 parent 5c1dfe2 commit db68ff5
Show file tree
Hide file tree
Showing 16 changed files with 353 additions and 169 deletions.
61 changes: 43 additions & 18 deletions backend/api/src/reaction.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { APIError, APIHandler } from './helpers/endpoint'
import { createLikeNotification } from 'shared/create-notification'
import { assertUnreachable } from 'common/util/types'
import { createLikeNotification } from 'shared/create-notification'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { log } from 'shared/utils'
import { APIError, APIHandler } from './helpers/endpoint'

export const addOrRemoveReaction: APIHandler<'react'> = async (props, auth) => {
const { contentId, contentType, remove } = props
const { contentId, contentType, remove, reactionType = 'like' } = props
const userId = auth.uid

const pg = createSupabaseDirectClient()

if (remove) {
const deleteReaction = async (deleteReactionType: string) => {
await pg.none(
`delete from user_reactions
where user_id = $1 and content_id = $2 and content_type = $3`,
[userId, contentId, contentType]
where user_id = $1 and content_id = $2 and content_type = $3 and reaction_type = $4`,
[userId, contentId, contentType, deleteReactionType]
)
}

if (remove) {
await deleteReaction(reactionType)
} else {
// get the id of the person this content belongs to, to denormalize the owner
let ownerId: string
Expand Down Expand Up @@ -54,36 +58,57 @@ export const addOrRemoveReaction: APIHandler<'react'> = async (props, auth) => {
)

if (existingReactions.length > 0) {
log('Reaction already exists, do nothing')
return { result: { success: true }, continue: async () => {} }
const existingReactionType = existingReactions[0].reaction_type
// if it's the same reaction type, do nothing
if (existingReactionType === reactionType) {
return { result: { success: true }, continue: async () => {} }
} else {
// otherwise, remove the other reaction type
await deleteReaction(existingReactionType)
}
}

// actually do the insert
const reactionRow = await pg.one(
`insert into user_reactions
(content_id, content_type, content_owner_id, user_id)
values ($1, $2, $3, $4)
(content_id, content_type, content_owner_id, user_id, reaction_type)
values ($1, $2, $3, $4, $5)
returning *`,
[contentId, contentType, ownerId, userId]
[contentId, contentType, ownerId, userId, reactionType]
)

await createLikeNotification(reactionRow)
if (reactionType === 'like') {
await createLikeNotification(reactionRow)
}
}

return {
result: { success: true },
continue: async () => {
if (contentType === 'comment') {
const count = await pg.one(
const likeCount = await pg.one(
`select count(*) from user_reactions
where content_id = $1 and content_type = $2 and reaction_type = $3`,
[contentId, contentType, 'like'],
(r) => r.count
)
const dislikeCount = await pg.one(
`select count(*) from user_reactions
where content_id = $1 and content_type = $2`,
[contentId, contentType],
where content_id = $1 and content_type = $2 and reaction_type = $3`,
[contentId, contentType, 'dislike'],
(r) => r.count
)
log('new like count ' + count)

log('new like count ' + likeCount)
log('new dislike count ' + dislikeCount)

await pg.none(
`update contract_comments set likes = $1 where comment_id = $2`,
[count, contentId]
[likeCount, contentId]
)
await pg.none(
`update contract_comments set dislikes = $1 where comment_id = $2`,
[dislikeCount, contentId]
)
}
},
Expand Down
2 changes: 1 addition & 1 deletion backend/shared/src/supabase/contract-comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function getCommentsDirect(
const { userId, contractId, limit = 5000, page = 0 } = filters
return await pg.map(
`
select cc.data, likes from contract_comments cc
select cc.data, likes, dislikes from contract_comments cc
join contracts on cc.contract_id = contracts.id
where contracts.visibility = 'public'
and ($3 is null or contract_id = $3)
Expand Down
1 change: 1 addition & 0 deletions common/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ export const API = (_apiTypeCheck = {
contentId: z.string(),
contentType: z.enum(['comment', 'contract']),
remove: z.boolean().optional(),
reactionType: z.enum(['like', 'dislike']).optional().default('like'),
})
.strict(),
returns: { success: true },
Expand Down
2 changes: 1 addition & 1 deletion common/src/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type Comment<T extends AnyCommentType = AnyCommentType> = {
userAvatarUrl?: string
/** @deprecated Not actually deprecated, only in supabase column, and not in data column */
likes?: number

dislikes?: number
hidden?: boolean
hiddenTime?: number
hiderId?: string
Expand Down
2 changes: 2 additions & 0 deletions common/src/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export type Reaction = Row<'user_reactions'>

export type ReactionContentTypes = 'contract' | 'comment'

export type ReactionType = 'like' | 'dislike'

// export type ReactionTypes = 'like'
89 changes: 68 additions & 21 deletions web/components/comments/comment-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ import { ReplyIcon } from '@heroicons/react/solid'
import clsx from 'clsx'
import { ContractComment } from 'common/comment'
import { Contract } from 'common/contract'
import { TRADE_TERM } from 'common/envs/constants'
import { richTextToString } from 'common/util/parse'
import { useState } from 'react'
import { FaArrowTrendUp, FaArrowTrendDown } from 'react-icons/fa6'
import { useUser, usePrivateUser, isBlocked } from 'web/hooks/use-user'
import { FaArrowTrendDown, FaArrowTrendUp } from 'react-icons/fa6'
import { isBlocked, usePrivateUser, useUser } from 'web/hooks/use-user'
import { track } from 'web/lib/service/analytics'
import { BuyPanel } from '../bet/bet-panel'
import { IconButton } from '../buttons/button'
import { LikeButton } from '../contract/like-button'
import { AwardBountyButton } from '../contract/bountied-question'
import { ReactButton } from '../contract/react-button'
import { Col } from '../layout/col'
import { Modal, MODAL_CLASS } from '../layout/modal'
import { Row } from '../layout/row'
import { Tooltip } from '../widgets/tooltip'
import { track } from 'web/lib/service/analytics'
import { AwardBountyButton } from '../contract/bountied-question'
import { TRADE_TERM } from 'common/envs/constants'
import { PrivateUser, User } from 'common/user'

export function CommentActions(props: {
onReplyClick?: (comment: ContractComment) => void
Expand Down Expand Up @@ -46,14 +47,20 @@ export function CommentActions(props: {

return (
<Row className="grow items-center justify-end">
<LikeAndDislikeComment
comment={comment}
trackingLocation={trackingLocation}
privateUser={privateUser}
user={user}
/>
{canGiveBounty && (
<AwardBountyButton
contract={liveContract}
comment={comment}
onAward={onAward}
user={user}
disabled={liveContract.bountyLeft <= 0}
buttonClassName={'mr-1'}
buttonClassName={'mr-1 min-w-[60px]'}
/>
)}
{user && liveContract.outcomeType === 'BINARY' && !isCashContract && (
Expand All @@ -67,19 +74,20 @@ export function CommentActions(props: {
setShowBetModal(true)
}}
size={'xs'}
className={'min-w-[60px]'}
>
<Tooltip text={`Reply with a ${TRADE_TERM}`} placement="bottom">
<Row className="gap-1">
{diff != 0 && (
<span className="">{Math.round(Math.abs(diff))}</span>
)}
{diff > 0 ? (
<FaArrowTrendUp className={'h-5 w-5 text-teal-500'} />
) : diff < 0 ? (
<FaArrowTrendDown className={'text-scarlet-500 h-5 w-5'} />
) : (
<FaArrowTrendUp className={'h-5 w-5'} />
)}
{diff != 0 && (
<span className="">{Math.round(Math.abs(diff))}</span>
)}
</Row>
</Tooltip>
</IconButton>
Expand All @@ -92,23 +100,14 @@ export function CommentActions(props: {
e.stopPropagation()
onReplyClick(comment)
}}
className={'text-ink-500'}
className={'text-ink-500 min-w-[60px]'}
>
<Tooltip text="Reply with a comment" placement="bottom">
<ReplyIcon className="h-5 w-5 " />
</Tooltip>
</IconButton>
)}
<LikeButton
contentCreatorId={comment.userId}
contentId={comment.id}
user={user}
contentType={'comment'}
size={'xs'}
contentText={richTextToString(comment.content)}
disabled={isBlocked(privateUser, comment.userId)}
trackingLocation={trackingLocation}
/>

{showBetModal && (
<Modal
open={showBetModal}
Expand Down Expand Up @@ -137,3 +136,51 @@ export function CommentActions(props: {
</Row>
)
}

export function LikeAndDislikeComment(props: {
comment: ContractComment
trackingLocation: string
privateUser: PrivateUser | null | undefined
user: User | null | undefined
}) {
const { comment, trackingLocation, privateUser, user } = props
const [userReactedWith, setUserReactedWith] = useState<
'like' | 'dislike' | 'none'
>('none')
return (
<>
<ReactButton
contentCreatorId={comment.userId}
contentId={comment.id}
user={user}
contentType={'comment'}
size={'xs'}
contentText={richTextToString(comment.content)}
disabled={isBlocked(privateUser, comment.userId)}
trackingLocation={trackingLocation}
iconType={'thumb'}
reactionType={'like'}
userReactedWith={userReactedWith}
onReact={() => setUserReactedWith('like')}
onUnreact={() => setUserReactedWith('none')}
className={'min-w-[60px]'}
/>
<ReactButton
contentCreatorId={comment.userId}
contentId={comment.id}
user={user}
contentType={'comment'}
size={'xs'}
contentText={richTextToString(comment.content)}
disabled={isBlocked(privateUser, comment.userId)}
trackingLocation={trackingLocation}
iconType={'thumb'}
reactionType={'dislike'}
userReactedWith={userReactedWith}
onReact={() => setUserReactedWith('dislike')}
onUnreact={() => setUserReactedWith('none')}
className={'min-w-[60px]'}
/>
</>
)
}
8 changes: 7 additions & 1 deletion web/components/comments/comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,14 @@ export const ParentFeedComment = memo(function ParentFeedComment(props: {
function HideableContent(props: { comment: ContractComment }) {
const { comment } = props
const { text, content } = comment
//hides if enough dislikes
const dislikes = comment.dislikes ?? 0
const likes = comment.likes ?? 0
const majorityDislikes = dislikes > 10 && dislikes / (likes + dislikes) >= 0.8
const initiallyHidden = majorityDislikes || comment.hidden
const [showHidden, setShowHidden] = useState(false)
return comment.hidden && !showHidden ? (

return initiallyHidden && !showHidden ? (
<div
className="hover text-ink-600 text-sm font-thin italic hover:cursor-pointer"
onClick={() => {
Expand Down
6 changes: 3 additions & 3 deletions web/components/contract/contract-summary-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Contract } from 'common/contract'
import { formatWithToken, shortFormatNumber } from 'common/util/format'
import { Row } from 'web/components/layout/row'
import { isBlocked, usePrivateUser, useUser } from 'web/hooks/use-user'
import { MoneyDisplay } from '../bet/money-display'
import { TierTooltip } from '../tiers/tier-tooltip'
import { Tooltip } from '../widgets/tooltip'
import { BountyLeft } from './bountied-question'
import { CloseOrResolveTime } from './contract-details'
import { LikeButton } from './like-button'
import { MoneyDisplay } from '../bet/money-display'
import { ReactButton } from './react-button'

export function ContractSummaryStats(props: {
contractId: string
Expand Down Expand Up @@ -42,7 +42,7 @@ export function ContractSummaryStats(props: {
<Row className="ml-auto gap-4">
{marketTier && <TierTooltip tier={marketTier} contract={contract} />}
{!isBlocked(privateUser, contract.creatorId) && (
<LikeButton
<ReactButton
user={user}
size={'2xs'}
contentId={contractId}
Expand Down
6 changes: 5 additions & 1 deletion web/components/contract/contract-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,11 @@ export const CommentsTabContent = memo(function CommentsTabContent(props: {
? -Infinity
: c.hidden
? Infinity
: -((c.bountyAwarded ?? 0) * 1000 + (c.likes ?? 0))
: -(
(c.bountyAwarded ?? 0) * 1000 +
(c.likes ?? 0) -
(c.dislikes ?? 0)
)
: sort === 'Yes bets'
? (c: ContractComment) => -(c.betReplyAmountsByOutcome?.['YES'] ?? 0)
: sort === 'No bets'
Expand Down
4 changes: 2 additions & 2 deletions web/components/contract/feed-contract-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { PollPanel } from '../poll/poll-panel'
import { TierTooltip } from '../tiers/tier-tooltip'
import { UserHovercard } from '../user/user-hovercard'
import { ClickFrame } from '../widgets/click-frame'
import { LikeButton } from './like-button'
import { ReactButton } from './react-button'
import { TradesButton } from './trades-button'

const DEBUG_FEED_CARDS =
Expand Down Expand Up @@ -456,7 +456,7 @@ const BottomActionRow = (props: {
<CommentsButton contract={contract} user={user} className={'h-full'} />
</BottomRowButtonWrapper>
<BottomRowButtonWrapper>
<LikeButton
<ReactButton
contentId={contract.id}
contentCreatorId={contract.creatorId}
user={user}
Expand Down
Loading

0 comments on commit db68ff5

Please sign in to comment.