Allow free comments with optional bets

This commit is contained in:
Ian Philips 2022-04-21 08:20:58 -06:00
parent 0390ec0f47
commit 8291a05f32
6 changed files with 185 additions and 47 deletions

View File

@ -3,7 +3,7 @@
export type Comment = { export type Comment = {
id: string id: string
contractId: string contractId: string
betId: string betId?: string
userId: string userId: string
text: string text: string

View File

@ -1,5 +1,6 @@
# Secrets # Secrets
.env* .env*
.runtimeconfig.json
# Compiled JavaScript files # Compiled JavaScript files
lib/**/*.js lib/**/*.js

View File

@ -12,6 +12,7 @@ import {
} from '../../../common/contract' } from '../../../common/contract'
import { User } from '../../../common/user' import { User } from '../../../common/user'
import { mapCommentsByBetId } from '../../lib/firebase/comments' import { mapCommentsByBetId } from '../../lib/firebase/comments'
import { useUser } from '../../hooks/use-user'
export type ActivityItem = export type ActivityItem =
| DescriptionItem | DescriptionItem
@ -22,12 +23,19 @@ export type ActivityItem =
| AnswerGroupItem | AnswerGroupItem
| CloseItem | CloseItem
| ResolveItem | ResolveItem
| CommentInputItem
type BaseActivityItem = { type BaseActivityItem = {
id: string id: string
contract: Contract contract: Contract
} }
export type CommentInputItem = BaseActivityItem & {
type: 'commentInput'
bets: Bet[]
comments: Comment[]
}
export type DescriptionItem = BaseActivityItem & { export type DescriptionItem = BaseActivityItem & {
type: 'description' type: 'description'
} }
@ -48,7 +56,7 @@ export type BetItem = BaseActivityItem & {
export type CommentItem = BaseActivityItem & { export type CommentItem = BaseActivityItem & {
type: 'comment' type: 'comment'
comment: Comment comment: Comment
bet: Bet bet: Bet | undefined
hideOutcome: boolean hideOutcome: boolean
truncate: boolean truncate: boolean
smallAvatar: boolean smallAvatar: boolean
@ -279,9 +287,41 @@ export function getAllContractActivityItems(
] ]
: [{ type: 'description', id: '0', contract }] : [{ type: 'description', id: '0', contract }]
// for each comment. turn it into an activity item and add it to the items:
const commentWithoutBetActivityItems = comments
.filter((comment) => !comment.betId)
.map((comment) => ({
type: 'comment' as const,
id: comment.id,
contract: contract,
comment,
bet: undefined,
truncate: false,
hideOutcome: true,
smallAvatar: false,
}))
const groupedBets = groupBets(bets, comments, contract, user?.id, {
hideOutcome: false,
abbreviated,
smallAvatar: false,
reversed: false,
})
// iterate through the bets and comment acitivity items and add them to the items in order of comment creation time:
const allActivityItems = [...commentWithoutBetActivityItems, ...groupedBets]
const sortedActivityItems = _.sortBy(allActivityItems, (item) => {
if (item.type === 'comment') {
return item.comment.createdTime
} else if (item.type === 'bet') {
return item.bet.createdTime
} else if (item.type === 'betgroup') {
return item.bets[0].createdTime
}
})
items.push(...sortedActivityItems)
if (outcomeType === 'FREE_RESPONSE') {
items.push( items.push(
...(outcomeType === 'FREE_RESPONSE' ...getAnswerGroups(
? getAnswerGroups(
contract as FullContract<DPM, FreeResponse>, contract as FullContract<DPM, FreeResponse>,
bets, bets,
comments, comments,
@ -292,13 +332,8 @@ export function getAllContractActivityItems(
reversed, reversed,
} }
) )
: groupBets(bets, comments, contract, user?.id, {
hideOutcome: false,
abbreviated,
smallAvatar: false,
reversed: false,
}))
) )
}
if (contract.closeTime && contract.closeTime <= Date.now()) { if (contract.closeTime && contract.closeTime <= Date.now()) {
items.push({ type: 'close', id: `${contract.closeTime}`, contract }) items.push({ type: 'close', id: `${contract.closeTime}`, contract })
@ -306,6 +341,13 @@ export function getAllContractActivityItems(
if (contract.resolution) { if (contract.resolution) {
items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract }) items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract })
} }
items.push({
type: 'commentInput',
id: 'commentInput',
bets,
comments,
contract,
})
if (reversed) items.reverse() if (reversed) items.reverse()

View File

@ -104,24 +104,30 @@ function FeedItem(props: { item: ActivityItem }) {
return <FeedClose {...item} /> return <FeedClose {...item} />
case 'resolve': case 'resolve':
return <FeedResolve {...item} /> return <FeedResolve {...item} />
case 'commentInput':
return <CommentInput {...item} />
} }
} }
export function FeedComment(props: { export function FeedComment(props: {
contract: Contract contract: Contract
comment: Comment comment: Comment
bet: Bet bet: Bet | undefined
hideOutcome: boolean hideOutcome: boolean
truncate: boolean truncate: boolean
smallAvatar: boolean smallAvatar: boolean
}) { }) {
const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props
const { amount, outcome } = bet let money: string | undefined
let outcome: string | undefined
let bought: string | undefined
if (bet) {
outcome = bet.outcome
bought = bet.amount >= 0 ? 'bought' : 'sold'
money = formatMoney(Math.abs(bet.amount))
}
const { text, userUsername, userName, userAvatarUrl, createdTime } = comment const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
const bought = amount >= 0 ? 'bought' : 'sold'
const money = formatMoney(Math.abs(amount))
return ( return (
<> <>
<Avatar <Avatar
@ -144,7 +150,7 @@ export function FeedComment(props: {
{' '} {' '}
of{' '} of{' '}
<OutcomeLabel <OutcomeLabel
outcome={outcome} outcome={outcome ? outcome : ''}
contract={contract} contract={contract}
truncate="short" truncate="short"
/> />
@ -174,6 +180,77 @@ function RelativeTimestamp(props: { time: number }) {
) )
} }
export function CommentInput(props: {
contract: Contract
comments: Comment[]
bets: Bet[]
}) {
// see if we can comment input on any bet:
const { contract, bets, comments } = props
const user = useUser()
let canCommentOnBet = false
bets.forEach((bet) => {
// make sure there is not already a comment with a mathcing bet id:
const matchingComment = comments.find((comment) => comment.betId === bet.id)
if (matchingComment) {
return
}
const { createdTime, userId } = bet
const isSelf = user?.id === userId
// You can comment if your bet was posted in the last hour
const canComment = isSelf && Date.now() - createdTime < 60 * 60 * 1000
if (canComment) {
// if you can comment on this bet, then you can comment on the contract
canCommentOnBet = true
}
})
const [comment, setComment] = useState('')
async function submitComment() {
if (!user || !comment) return
await createComment(contract.id, comment, user)
setComment('')
}
if (canCommentOnBet) return <div />
return (
<>
<div>
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
</div>
<div className={'min-w-0 flex-1 py-1.5'}>
<div className="text-sm text-gray-500">
{/*<span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span>{' '}*/}
<div className="mt-2">
<Textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
className="textarea textarea-bordered w-full resize-none"
placeholder="Add a comment..."
rows={3}
maxLength={MAX_COMMENT_LENGTH}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
submitComment()
}
}}
/>
<button
className="btn btn-outline btn-sm mt-1"
onClick={submitComment}
>
Comment
</button>
</div>
</div>
</div>
</>
)
}
export function FeedBet(props: { export function FeedBet(props: {
contract: Contract contract: Contract
bet: Bet bet: Bet
@ -192,7 +269,7 @@ export function FeedBet(props: {
const [comment, setComment] = useState('') const [comment, setComment] = useState('')
async function submitComment() { async function submitComment() {
if (!user || !comment || !canComment) return if (!user || !comment || !canComment) return
await createComment(contract.id, id, comment, user) await createComment(contract.id, comment, user, id)
} }
const bought = amount >= 0 ? 'bought' : 'sold' const bought = amount >= 0 ? 'bought' : 'sold'

View File

@ -19,16 +19,16 @@ export const MAX_COMMENT_LENGTH = 10000
export async function createComment( export async function createComment(
contractId: string, contractId: string,
betId: string,
text: string, text: string,
commenter: User commenter: User,
betId?: string
) { ) {
if (betId) {
const ref = doc(getCommentsCollection(contractId), betId) const ref = doc(getCommentsCollection(contractId), betId)
const comment: Comment = { const comment: Comment = {
id: ref.id, id: ref.id,
betId: betId,
contractId, contractId,
betId,
userId: commenter.id, userId: commenter.id,
text: text.slice(0, MAX_COMMENT_LENGTH), text: text.slice(0, MAX_COMMENT_LENGTH),
createdTime: Date.now(), createdTime: Date.now(),
@ -36,8 +36,21 @@ export async function createComment(
userUsername: commenter.username, userUsername: commenter.username,
userAvatarUrl: commenter.avatarUrl, userAvatarUrl: commenter.avatarUrl,
} }
return await setDoc(ref, comment) return await setDoc(ref, comment)
} else {
const newCommentRef = doc(getCommentsCollection(contractId))
let comment: Comment = {
id: newCommentRef.id,
contractId,
userId: commenter.id,
text: text.slice(0, MAX_COMMENT_LENGTH),
createdTime: Date.now(),
userName: commenter.name,
userUsername: commenter.username,
userAvatarUrl: commenter.avatarUrl,
}
return await setDoc(newCommentRef, comment)
}
} }
function getCommentsCollection(contractId: string) { function getCommentsCollection(contractId: string) {
@ -67,8 +80,10 @@ export function listenForComments(
export function mapCommentsByBetId(comments: Comment[]) { export function mapCommentsByBetId(comments: Comment[]) {
const map: Record<string, Comment> = {} const map: Record<string, Comment> = {}
for (const comment of comments) { for (const comment of comments) {
if (comment.betId) {
map[comment.betId] = comment map[comment.betId] = comment
} }
}
return map return map
} }

View File

@ -250,7 +250,10 @@ function ContractTopTrades(props: {
const topBettor = useUserById(betsById[topBetId]?.userId) const topBettor = useUserById(betsById[topBetId]?.userId)
// And also the commentId of the comment with the highest profit // And also the commentId of the comment with the highest profit
const topCommentId = _.sortBy(comments, (c) => -profitById[c.betId])[0]?.id const topCommentId = _.sortBy(
comments,
(c) => c.betId && -profitById[c.betId]
)[0]?.id
return ( return (
<div className="mt-12 max-w-sm"> <div className="mt-12 max-w-sm">