Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upvote and downvote comments #3112

Merged
merged 16 commits into from
Nov 13, 2024
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
Loading