Refactor feed code into 5 files under feed directory.
This commit is contained in:
parent
b4c9193ea4
commit
44a093aff4
|
@ -1,135 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
import {
|
||||
ContractActivityFeed,
|
||||
ContractFeed,
|
||||
ContractSummaryFeed,
|
||||
} from './contract-feed'
|
||||
import { Contract } from '../lib/firebase/contracts'
|
||||
import { Comment } from '../lib/firebase/comments'
|
||||
import { Col } from './layout/col'
|
||||
import { Bet } from '../../common/bet'
|
||||
|
||||
const MAX_ACTIVE_CONTRACTS = 75
|
||||
|
||||
// This does NOT include comment times, since those aren't part of the contract atm.
|
||||
// TODO: Maybe store last activity time directly in the contract?
|
||||
// Pros: simplifies this code; cons: harder to tweak "activity" definition later
|
||||
function lastActivityTime(contract: Contract) {
|
||||
return Math.max(
|
||||
contract.resolutionTime || 0,
|
||||
contract.lastUpdatedTime,
|
||||
contract.createdTime
|
||||
)
|
||||
}
|
||||
|
||||
// Types of activity to surface:
|
||||
// - Comment on a market
|
||||
// - New market created
|
||||
// - Market resolved
|
||||
// - Bet on market
|
||||
export function findActiveContracts(
|
||||
allContracts: Contract[],
|
||||
recentComments: Comment[],
|
||||
recentBets: Bet[]
|
||||
) {
|
||||
const idToActivityTime = new Map<string, number>()
|
||||
function record(contractId: string, time: number) {
|
||||
// Only record if the time is newer
|
||||
const oldTime = idToActivityTime.get(contractId)
|
||||
idToActivityTime.set(contractId, Math.max(oldTime ?? 0, time))
|
||||
}
|
||||
|
||||
const contractsById = new Map(allContracts.map((c) => [c.id, c]))
|
||||
|
||||
// Record contract activity.
|
||||
for (const contract of allContracts) {
|
||||
record(contract.id, lastActivityTime(contract))
|
||||
}
|
||||
|
||||
// Add every contract that had a recent comment, too
|
||||
for (const comment of recentComments) {
|
||||
const contract = contractsById.get(comment.contractId)
|
||||
if (contract) record(contract.id, comment.createdTime)
|
||||
}
|
||||
|
||||
// Add contracts by last bet time.
|
||||
const contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||
const contractMostRecentBet = _.mapValues(
|
||||
contractBets,
|
||||
(bets) => _.maxBy(bets, (bet) => bet.createdTime) as Bet
|
||||
)
|
||||
for (const bet of Object.values(contractMostRecentBet)) {
|
||||
const contract = contractsById.get(bet.contractId)
|
||||
if (contract) record(contract.id, bet.createdTime)
|
||||
}
|
||||
|
||||
let activeContracts = allContracts.filter(
|
||||
(contract) => contract.visibility === 'public' && !contract.isResolved
|
||||
)
|
||||
activeContracts = _.sortBy(
|
||||
activeContracts,
|
||||
(c) => -(idToActivityTime.get(c.id) ?? 0)
|
||||
)
|
||||
return activeContracts.slice(0, MAX_ACTIVE_CONTRACTS)
|
||||
}
|
||||
|
||||
export function ActivityFeed(props: {
|
||||
contracts: Contract[]
|
||||
recentBets: Bet[]
|
||||
recentComments: Comment[]
|
||||
loadBetAndCommentHistory?: boolean
|
||||
}) {
|
||||
const { contracts, recentBets, recentComments, loadBetAndCommentHistory } =
|
||||
props
|
||||
|
||||
const groupedBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||
const groupedComments = _.groupBy(
|
||||
recentComments,
|
||||
(comment) => comment.contractId
|
||||
)
|
||||
|
||||
return (
|
||||
<Col className="items-center">
|
||||
<Col className="w-full">
|
||||
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
|
||||
{contracts.map((contract) => (
|
||||
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
||||
{loadBetAndCommentHistory ? (
|
||||
<ContractFeed
|
||||
contract={contract}
|
||||
bets={groupedBets[contract.id] ?? []}
|
||||
comments={groupedComments[contract.id] ?? []}
|
||||
feedType="activity"
|
||||
/>
|
||||
) : (
|
||||
<ContractActivityFeed
|
||||
contract={contract}
|
||||
bets={groupedBets[contract.id] ?? []}
|
||||
comments={groupedComments[contract.id] ?? []}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export function SummaryActivityFeed(props: { contracts: Contract[] }) {
|
||||
const { contracts } = props
|
||||
|
||||
return (
|
||||
<Col className="items-center">
|
||||
<Col className="w-full max-w-3xl">
|
||||
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
|
||||
{contracts.map((contract) => (
|
||||
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
||||
<ContractSummaryFeed contract={contract} />
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
|
@ -13,12 +13,14 @@ import { formatPercent } from '../../../common/util/format'
|
|||
import { getOutcomeProbability } from '../../../common/calculate'
|
||||
import { tradingAllowed } from '../../lib/firebase/contracts'
|
||||
import { AnswerBetPanel } from './answer-bet-panel'
|
||||
import { ContractFeed } from '../contract-feed'
|
||||
import { Linkify } from '../linkify'
|
||||
import { User } from '../../../common/user'
|
||||
import { ContractActivity } from '../feed/contract-activity'
|
||||
|
||||
export function AnswerItem(props: {
|
||||
answer: Answer
|
||||
contract: Contract
|
||||
user: User | null | undefined
|
||||
showChoice: 'radio' | 'checkbox' | undefined
|
||||
chosenProb: number | undefined
|
||||
totalChosenProb?: number
|
||||
|
@ -28,6 +30,7 @@ export function AnswerItem(props: {
|
|||
const {
|
||||
answer,
|
||||
contract,
|
||||
user,
|
||||
showChoice,
|
||||
chosenProb,
|
||||
totalChosenProb,
|
||||
|
@ -82,11 +85,11 @@ export function AnswerItem(props: {
|
|||
</Row>
|
||||
|
||||
{isBetting && (
|
||||
<ContractFeed
|
||||
<ContractActivity
|
||||
contract={contract}
|
||||
bets={[]}
|
||||
comments={[]}
|
||||
feedType="multi"
|
||||
user={user}
|
||||
outcome={answer.id}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -86,6 +86,7 @@ export function AnswersPanel(props: { contract: Contract; answers: Answer[] }) {
|
|||
key={answer.id}
|
||||
answer={answer}
|
||||
contract={contract}
|
||||
user={user}
|
||||
showChoice={showChoice}
|
||||
chosenProb={chosenAnswers[answer.id]}
|
||||
totalChosenProb={chosenTotal}
|
||||
|
|
|
@ -12,13 +12,13 @@ import { Row } from './layout/row'
|
|||
import { Linkify } from './linkify'
|
||||
import clsx from 'clsx'
|
||||
import { ContractDetails, ResolutionOrChance } from './contract-card'
|
||||
import { ContractFeed } from './contract-feed'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { RevealableTagsInput, TagsInput } from './tags-input'
|
||||
import BetRow from './bet-row'
|
||||
import { Fold } from '../../common/fold'
|
||||
import { FoldTagList } from './tags-list'
|
||||
import { ContractActivity } from './feed/contract-activity'
|
||||
|
||||
export const ContractOverview = (props: {
|
||||
contract: Contract
|
||||
|
@ -119,11 +119,11 @@ export const ContractOverview = (props: {
|
|||
|
||||
<Spacer h={12} />
|
||||
|
||||
<ContractFeed
|
||||
<ContractActivity
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
comments={comments}
|
||||
feedType="market"
|
||||
user={user}
|
||||
betRowClassName="!mt-0"
|
||||
/>
|
||||
</Col>
|
||||
|
|
109
web/components/feed/activity-feed.tsx
Normal file
109
web/components/feed/activity-feed.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import _ from 'lodash'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Contract, tradingAllowed } from '../../lib/firebase/contracts'
|
||||
import { Comment } from '../../lib/firebase/comments'
|
||||
import { Col } from '../layout/col'
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { useUser } from '../../hooks/use-user'
|
||||
import BetRow from '../bet-row'
|
||||
import { FeedQuestion } from './feed-items'
|
||||
import { ContractActivity, RecentContractActivity } from './contract-activity'
|
||||
|
||||
export function ActivityFeed(props: {
|
||||
contracts: Contract[]
|
||||
recentBets: Bet[]
|
||||
recentComments: Comment[]
|
||||
loadBetAndCommentHistory?: boolean
|
||||
}) {
|
||||
const { contracts, recentBets, recentComments, loadBetAndCommentHistory } =
|
||||
props
|
||||
|
||||
const user = useUser()
|
||||
|
||||
const groupedBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||
const groupedComments = _.groupBy(
|
||||
recentComments,
|
||||
(comment) => comment.contractId
|
||||
)
|
||||
|
||||
return (
|
||||
<FeedContainer
|
||||
contracts={contracts}
|
||||
renderContract={(contract) =>
|
||||
loadBetAndCommentHistory ? (
|
||||
<ContractActivity
|
||||
user={user}
|
||||
contract={contract}
|
||||
bets={groupedBets[contract.id] ?? []}
|
||||
comments={groupedComments[contract.id] ?? []}
|
||||
abbreviated
|
||||
/>
|
||||
) : (
|
||||
<RecentContractActivity
|
||||
user={user}
|
||||
contract={contract}
|
||||
bets={groupedBets[contract.id] ?? []}
|
||||
comments={groupedComments[contract.id] ?? []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SummaryActivityFeed(props: { contracts: Contract[] }) {
|
||||
const { contracts } = props
|
||||
|
||||
return (
|
||||
<FeedContainer
|
||||
contracts={contracts}
|
||||
renderContract={(contract) => <ContractSummary contract={contract} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedContainer(props: {
|
||||
contracts: Contract[]
|
||||
renderContract: (contract: Contract) => any
|
||||
}) {
|
||||
const { contracts, renderContract } = props
|
||||
|
||||
return (
|
||||
<Col className="items-center">
|
||||
<Col className="w-full max-w-3xl">
|
||||
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
|
||||
{contracts.map((contract) => (
|
||||
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
||||
{renderContract(contract)}
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
function ContractSummary(props: {
|
||||
contract: Contract
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, betRowClassName } = props
|
||||
const { outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
return (
|
||||
<div className="flow-root pr-2 md:pr-0">
|
||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
||||
<div className="relative pb-8">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<FeedQuestion contract={contract} showDescription />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
249
web/components/feed/activity-items.ts
Normal file
249
web/components/feed/activity-items.ts
Normal file
|
@ -0,0 +1,249 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import { Answer } from '../../../common/answer'
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Comment } from '../../../common/comment'
|
||||
import { Contract } from '../../../common/contract'
|
||||
import { User } from '../../../common/user'
|
||||
import { filterDefined } from '../../../common/util/array'
|
||||
import { canAddComment, mapCommentsByBetId } from '../../lib/firebase/comments'
|
||||
import { fromNow } from '../../lib/util/time'
|
||||
|
||||
export type ActivityItem = {
|
||||
id: string
|
||||
type:
|
||||
| 'bet'
|
||||
| 'comment'
|
||||
| 'start'
|
||||
| 'betgroup'
|
||||
| 'answergroup'
|
||||
| 'close'
|
||||
| 'resolve'
|
||||
| 'expand'
|
||||
| undefined
|
||||
}
|
||||
|
||||
export type FeedAnswerGroupItem = ActivityItem & {
|
||||
type: 'answergroup'
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
answer: Answer
|
||||
user: User | null | undefined
|
||||
}
|
||||
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
// Group together bets that are:
|
||||
// - Within `windowMs` of the first in the group
|
||||
// - Do not have a comment
|
||||
// - Were not created by this user or the contract creator
|
||||
// Return a list of ActivityItems
|
||||
function groupBets(
|
||||
bets: Bet[],
|
||||
comments: Comment[],
|
||||
windowMs: number,
|
||||
contract: Contract,
|
||||
userId?: string
|
||||
) {
|
||||
const commentsMap = mapCommentsByBetId(comments)
|
||||
const items: any[] = []
|
||||
let group: Bet[] = []
|
||||
|
||||
// Turn the current group into an ActivityItem
|
||||
function pushGroup() {
|
||||
if (group.length == 1) {
|
||||
items.push(toActivityItem(group[0], false))
|
||||
} else if (group.length > 1) {
|
||||
items.push({ type: 'betgroup', bets: [...group], id: group[0].id })
|
||||
}
|
||||
group = []
|
||||
}
|
||||
|
||||
function toActivityItem(bet: Bet, isPublic: boolean) {
|
||||
const comment = commentsMap[bet.id]
|
||||
return comment ? toFeedComment(bet, comment) : toFeedBet(bet, contract)
|
||||
}
|
||||
|
||||
for (const bet of bets) {
|
||||
const isCreator = userId === bet.userId || contract.creatorId === bet.userId
|
||||
|
||||
if (commentsMap[bet.id] || isCreator) {
|
||||
pushGroup()
|
||||
// Create a single item for this
|
||||
items.push(toActivityItem(bet, true))
|
||||
} else {
|
||||
if (
|
||||
group.length > 0 &&
|
||||
bet.createdTime - group[0].createdTime > windowMs
|
||||
) {
|
||||
// More than `windowMs` has passed; start a new group
|
||||
pushGroup()
|
||||
}
|
||||
group.push(bet)
|
||||
}
|
||||
}
|
||||
if (group.length > 0) {
|
||||
pushGroup()
|
||||
}
|
||||
return items as ActivityItem[]
|
||||
}
|
||||
|
||||
function getAnswerGroups(
|
||||
contract: Contract,
|
||||
bets: Bet[],
|
||||
comments: Comment[],
|
||||
user: User | undefined | null
|
||||
) {
|
||||
// Keep last two comments.
|
||||
comments = comments.slice(-2)
|
||||
const lastBet = bets[bets.length - 1]
|
||||
|
||||
// Include up to 2 outcomes from comments and last bet.
|
||||
const outcomes = filterDefined(
|
||||
_.uniq([
|
||||
...comments.map(
|
||||
(comment) => bets.find((bet) => bet.id === comment.betId)?.outcome
|
||||
),
|
||||
lastBet?.outcome,
|
||||
])
|
||||
).slice(0, 2)
|
||||
|
||||
// Keep bets on selected outcomes.
|
||||
bets = bets.filter((bet) => outcomes.includes(bet.outcome))
|
||||
|
||||
const answerGroups = outcomes.map((outcome) => {
|
||||
const answerBets = bets.filter((bet) => bet.outcome === outcome)
|
||||
const answerComments = comments.filter((comment) =>
|
||||
answerBets.some((bet) => bet.id === comment.betId)
|
||||
)
|
||||
const answer = contract.answers?.find(
|
||||
(answer) => answer.id === outcome
|
||||
) as Answer
|
||||
|
||||
return {
|
||||
id: outcome,
|
||||
type: 'answergroup' as const,
|
||||
contract,
|
||||
answer,
|
||||
bets: answerBets,
|
||||
comments: answerComments,
|
||||
user,
|
||||
}
|
||||
})
|
||||
|
||||
return answerGroups
|
||||
}
|
||||
|
||||
function toFeedBet(bet: Bet, contract: Contract) {
|
||||
return {
|
||||
id: bet.id,
|
||||
contractId: bet.contractId,
|
||||
userId: bet.userId,
|
||||
type: 'bet',
|
||||
amount: bet.sale ? -bet.sale.amount : bet.amount,
|
||||
outcome: bet.outcome,
|
||||
createdTime: bet.createdTime,
|
||||
date: fromNow(bet.createdTime),
|
||||
contract,
|
||||
}
|
||||
}
|
||||
|
||||
function toFeedComment(bet: Bet, comment: Comment) {
|
||||
return {
|
||||
id: bet.id,
|
||||
contractId: bet.contractId,
|
||||
userId: bet.userId,
|
||||
type: 'comment',
|
||||
amount: bet.sale ? -bet.sale.amount : bet.amount,
|
||||
outcome: bet.outcome,
|
||||
createdTime: bet.createdTime,
|
||||
date: fromNow(bet.createdTime),
|
||||
|
||||
// Invariant: bet.comment exists
|
||||
text: comment.text,
|
||||
person: {
|
||||
username: comment.userUsername,
|
||||
name: comment.userName,
|
||||
avatarUrl: comment.userAvatarUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllContractActivityItems(
|
||||
contract: Contract,
|
||||
bets: Bet[],
|
||||
comments: Comment[],
|
||||
user: User | null | undefined,
|
||||
outcome?: string
|
||||
) {
|
||||
const { outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
bets = isBinary
|
||||
? bets.filter((bet) => !bet.isAnte)
|
||||
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
||||
|
||||
if (outcome) {
|
||||
bets = bets.filter((bet) => bet.outcome === outcome)
|
||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||
// Keep bets on comments or your bets where you can comment.
|
||||
const commentBetIds = new Set(comments.map((comment) => comment.betId))
|
||||
bets = bets.filter(
|
||||
(bet) =>
|
||||
commentBetIds.has(bet.id) ||
|
||||
canAddComment(bet.createdTime, user?.id === bet.userId)
|
||||
)
|
||||
}
|
||||
|
||||
const items: ActivityItem[] = outcome ? [] : [{ type: 'start', id: '0' }]
|
||||
|
||||
items.push(...groupBets(bets, comments, DAY_IN_MS, contract, user?.id))
|
||||
|
||||
if (contract.closeTime && contract.closeTime <= Date.now()) {
|
||||
items.push({ type: 'close', id: `${contract.closeTime}` })
|
||||
}
|
||||
if (contract.resolution) {
|
||||
items.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||
}
|
||||
if (outcome) {
|
||||
// Hack to add some more padding above the 'multi' feedType, by adding a null item.
|
||||
items.unshift({ type: undefined, id: '-1' })
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export function getRecentContractActivityItems(
|
||||
contract: Contract,
|
||||
bets: Bet[],
|
||||
comments: Comment[],
|
||||
user: User | null | undefined
|
||||
) {
|
||||
bets = bets.sort((b1, b2) => b1.createdTime - b2.createdTime)
|
||||
comments = comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
||||
|
||||
const items: ActivityItem[] = [{ type: 'start', id: '0' }]
|
||||
items.push(
|
||||
...(contract.outcomeType === 'FREE_RESPONSE'
|
||||
? getAnswerGroups(contract, bets, comments, user)
|
||||
: groupBets(bets, comments, DAY_IN_MS, contract, user?.id))
|
||||
)
|
||||
|
||||
if (contract.closeTime && contract.closeTime <= Date.now()) {
|
||||
items.push({ type: 'close', id: `${contract.closeTime}` })
|
||||
}
|
||||
if (contract.resolution) {
|
||||
items.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||
}
|
||||
|
||||
// Remove all but last bet group.
|
||||
const betGroups = items.filter((item) => item.type === 'betgroup')
|
||||
const lastBetGroup = betGroups[betGroups.length - 1]
|
||||
const filtered = items.filter(
|
||||
(item) => item.type !== 'betgroup' || item.id === lastBetGroup?.id
|
||||
)
|
||||
|
||||
// Only show the first item plus the last three items.
|
||||
return filtered.length > 3 ? [filtered[0], ...filtered.slice(-3)] : filtered
|
||||
}
|
71
web/components/feed/contract-activity.tsx
Normal file
71
web/components/feed/contract-activity.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import { Contract } from '../../lib/firebase/contracts'
|
||||
import { Comment } from '../../lib/firebase/comments'
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { useBets } from '../../hooks/use-bets'
|
||||
import { useComments } from '../../hooks/use-comments'
|
||||
import {
|
||||
getAllContractActivityItems,
|
||||
getRecentContractActivityItems,
|
||||
} from './activity-items'
|
||||
import { FeedItems } from './feed-items'
|
||||
import { User } from '../../../common/user'
|
||||
|
||||
export function ContractActivity(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
user: User | null | undefined
|
||||
outcome?: string // Which multi-category outcome to filter
|
||||
abbreviated?: boolean
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, user, outcome, abbreviated, betRowClassName } = props
|
||||
|
||||
const comments = useComments(contract.id) ?? props.comments
|
||||
const bets = useBets(contract.id) ?? props.bets
|
||||
|
||||
let items = getAllContractActivityItems(
|
||||
contract,
|
||||
bets,
|
||||
comments,
|
||||
user,
|
||||
outcome
|
||||
)
|
||||
|
||||
if (abbreviated) {
|
||||
items = [items[0], ...items.slice(-3)]
|
||||
}
|
||||
|
||||
return (
|
||||
<FeedItems
|
||||
contract={contract}
|
||||
items={items}
|
||||
feedType={abbreviated ? 'activity' : 'market'}
|
||||
betRowClassName={betRowClassName}
|
||||
outcome={outcome}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function RecentContractActivity(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
user: User | null | undefined
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, bets, comments, user, betRowClassName } = props
|
||||
|
||||
const items = getRecentContractActivityItems(contract, bets, comments, user)
|
||||
|
||||
return (
|
||||
<FeedItems
|
||||
contract={contract}
|
||||
items={items}
|
||||
feedType="activity"
|
||||
betRowClassName={betRowClassName}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -14,39 +14,105 @@ import dayjs from 'dayjs'
|
|||
import clsx from 'clsx'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
|
||||
import { OutcomeLabel } from './outcome-label'
|
||||
import { OutcomeLabel } from '../outcome-label'
|
||||
import {
|
||||
contractMetrics,
|
||||
Contract,
|
||||
contractPath,
|
||||
updateContract,
|
||||
tradingAllowed,
|
||||
} from '../lib/firebase/contracts'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { Linkify } from './linkify'
|
||||
import { Row } from './layout/row'
|
||||
import { createComment, MAX_COMMENT_LENGTH } from '../lib/firebase/comments'
|
||||
import { useComments } from '../hooks/use-comments'
|
||||
import { formatMoney } from '../../common/util/format'
|
||||
import { ResolutionOrChance } from './contract-card'
|
||||
import { SiteLink } from './site-link'
|
||||
import { Col } from './layout/col'
|
||||
import { UserLink } from './user-page'
|
||||
import { DateTimeTooltip } from './datetime-tooltip'
|
||||
import { useBets } from '../hooks/use-bets'
|
||||
import { Bet } from '../lib/firebase/bets'
|
||||
import { Comment, mapCommentsByBetId } from '../lib/firebase/comments'
|
||||
import { JoinSpans } from './join-spans'
|
||||
import { fromNow } from '../lib/util/time'
|
||||
import BetRow from './bet-row'
|
||||
import { parseTags } from '../../common/util/parse'
|
||||
import { Avatar } from './avatar'
|
||||
import { useAdmin } from '../hooks/use-admin'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
} from '../../lib/firebase/contracts'
|
||||
import { useUser } from '../../hooks/use-user'
|
||||
import { Linkify } from '../linkify'
|
||||
import { Row } from '../layout/row'
|
||||
import {
|
||||
canAddComment,
|
||||
createComment,
|
||||
MAX_COMMENT_LENGTH,
|
||||
} from '../../lib/firebase/comments'
|
||||
import { formatMoney } from '../../../common/util/format'
|
||||
import { ResolutionOrChance } from '../contract-card'
|
||||
import { SiteLink } from '../site-link'
|
||||
import { Col } from '../layout/col'
|
||||
import { UserLink } from '../user-page'
|
||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||
import { Bet } from '../../lib/firebase/bets'
|
||||
import { JoinSpans } from '../join-spans'
|
||||
import { fromNow } from '../../lib/util/time'
|
||||
import BetRow from '../bet-row'
|
||||
import { parseTags } from '../../../common/util/parse'
|
||||
import { Avatar } from '../avatar'
|
||||
import { useAdmin } from '../../hooks/use-admin'
|
||||
import { Answer } from '../../../common/answer'
|
||||
import { ActivityItem, FeedAnswerGroupItem } from './activity-items'
|
||||
|
||||
const canAddComment = (createdTime: number, isSelf: boolean) => {
|
||||
return isSelf && Date.now() - createdTime < 60 * 60 * 1000
|
||||
export type FeedType =
|
||||
// Main homepage/fold feed,
|
||||
| 'activity'
|
||||
// Comments feed on a market
|
||||
| 'market'
|
||||
// Grouped for a multi-category outcome
|
||||
| 'multi'
|
||||
|
||||
export function FeedItems(props: {
|
||||
contract: Contract
|
||||
items: ActivityItem[]
|
||||
feedType: FeedType
|
||||
outcome?: string // Which multi-category outcome to filter
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, items, feedType, outcome, betRowClassName } = props
|
||||
const { outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
return (
|
||||
<div className="flow-root pr-2 md:pr-0">
|
||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
|
||||
{items.map((activityItem, activityItemIdx) => (
|
||||
<div key={activityItem.id} className="relative pb-6">
|
||||
{activityItemIdx !== items.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-start space-x-3">
|
||||
{activityItem.type === 'start' ? (
|
||||
feedType === 'activity' ? (
|
||||
<FeedQuestion contract={contract} />
|
||||
) : feedType === 'market' ? (
|
||||
<FeedDescription contract={contract} />
|
||||
) : feedType === 'multi' ? (
|
||||
<FeedAnswer contract={contract} outcome={outcome || '0'} />
|
||||
) : null
|
||||
) : activityItem.type === 'comment' ? (
|
||||
<FeedComment
|
||||
activityItem={activityItem}
|
||||
moreHref={contractPath(contract)}
|
||||
feedType={feedType}
|
||||
/>
|
||||
) : activityItem.type === 'bet' ? (
|
||||
<FeedBet activityItem={activityItem} feedType={feedType} />
|
||||
) : activityItem.type === 'betgroup' ? (
|
||||
<FeedBetGroup activityItem={activityItem} feedType={feedType} />
|
||||
) : activityItem.type === 'answergroup' ? (
|
||||
<FeedAnswerGroup
|
||||
activityItem={activityItem as FeedAnswerGroupItem}
|
||||
/>
|
||||
) : activityItem.type === 'close' ? (
|
||||
<FeedClose contract={contract} />
|
||||
) : activityItem.type === 'resolve' ? (
|
||||
<FeedResolve contract={contract} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedComment(props: {
|
||||
|
@ -60,21 +126,21 @@ function FeedComment(props: {
|
|||
const bought = amount >= 0 ? 'bought' : 'sold'
|
||||
const money = formatMoney(Math.abs(amount))
|
||||
|
||||
const answer =
|
||||
feedType !== 'multi' &&
|
||||
(contract.answers?.find((answer: Answer) => answer?.id === outcome) as
|
||||
| Answer
|
||||
| undefined)
|
||||
// const answer =
|
||||
// feedType !== 'multi' &&
|
||||
// (contract.answers?.find((answer: Answer) => answer?.id === outcome) as
|
||||
// | Answer
|
||||
// | undefined)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Avatar username={person.username} avatarUrl={person.avatarUrl} />
|
||||
<div className="min-w-0 flex-1">
|
||||
{answer && (
|
||||
{/* {answer && (
|
||||
<div className="text-neutral mb-2" style={{ fontSize: 15 }}>
|
||||
{answer.text}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
<div>
|
||||
<p className="mt-0.5 text-sm text-gray-500">
|
||||
<UserLink
|
||||
|
@ -341,7 +407,7 @@ function TruncatedComment(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function FeedQuestion(props: {
|
||||
export function FeedQuestion(props: {
|
||||
contract: Contract
|
||||
showDescription?: boolean
|
||||
}) {
|
||||
|
@ -531,98 +597,6 @@ function FeedClose(props: { contract: Contract }) {
|
|||
)
|
||||
}
|
||||
|
||||
function toFeedBet(bet: Bet, contract: Contract) {
|
||||
return {
|
||||
id: bet.id,
|
||||
contractId: bet.contractId,
|
||||
userId: bet.userId,
|
||||
type: 'bet',
|
||||
amount: bet.sale ? -bet.sale.amount : bet.amount,
|
||||
outcome: bet.outcome,
|
||||
createdTime: bet.createdTime,
|
||||
date: fromNow(bet.createdTime),
|
||||
contract,
|
||||
}
|
||||
}
|
||||
|
||||
function toFeedComment(bet: Bet, comment: Comment) {
|
||||
return {
|
||||
id: bet.id,
|
||||
contractId: bet.contractId,
|
||||
userId: bet.userId,
|
||||
type: 'comment',
|
||||
amount: bet.sale ? -bet.sale.amount : bet.amount,
|
||||
outcome: bet.outcome,
|
||||
createdTime: bet.createdTime,
|
||||
date: fromNow(bet.createdTime),
|
||||
|
||||
// Invariant: bet.comment exists
|
||||
text: comment.text,
|
||||
person: {
|
||||
username: comment.userUsername,
|
||||
name: comment.userName,
|
||||
avatarUrl: comment.userAvatarUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
// Group together bets that are:
|
||||
// - Within `windowMs` of the first in the group
|
||||
// - Do not have a comment
|
||||
// - Were not created by this user or the contract creator
|
||||
// Return a list of ActivityItems
|
||||
function groupBets(
|
||||
bets: Bet[],
|
||||
comments: Comment[],
|
||||
windowMs: number,
|
||||
contract: Contract,
|
||||
userId?: string
|
||||
) {
|
||||
const commentsMap = mapCommentsByBetId(comments)
|
||||
const items: any[] = []
|
||||
let group: Bet[] = []
|
||||
|
||||
// Turn the current group into an ActivityItem
|
||||
function pushGroup() {
|
||||
if (group.length == 1) {
|
||||
items.push(toActivityItem(group[0], false))
|
||||
} else if (group.length > 1) {
|
||||
items.push({ type: 'betgroup', bets: [...group], id: group[0].id })
|
||||
}
|
||||
group = []
|
||||
}
|
||||
|
||||
function toActivityItem(bet: Bet, isPublic: boolean) {
|
||||
const comment = commentsMap[bet.id]
|
||||
return comment ? toFeedComment(bet, comment) : toFeedBet(bet, contract)
|
||||
}
|
||||
|
||||
for (const bet of bets) {
|
||||
const isCreator = userId === bet.userId || contract.creatorId === bet.userId
|
||||
|
||||
if (commentsMap[bet.id] || isCreator) {
|
||||
pushGroup()
|
||||
// Create a single item for this
|
||||
items.push(toActivityItem(bet, true))
|
||||
} else {
|
||||
if (
|
||||
group.length > 0 &&
|
||||
bet.createdTime - group[0].createdTime > windowMs
|
||||
) {
|
||||
// More than `windowMs` has passed; start a new group
|
||||
pushGroup()
|
||||
}
|
||||
group.push(bet)
|
||||
}
|
||||
}
|
||||
if (group.length > 0) {
|
||||
pushGroup()
|
||||
}
|
||||
return items as ActivityItem[]
|
||||
}
|
||||
|
||||
function BetGroupSpan(props: {
|
||||
bets: Bet[]
|
||||
outcome: string
|
||||
|
@ -687,6 +661,34 @@ function FeedBetGroup(props: { activityItem: any; feedType: FeedType }) {
|
|||
)
|
||||
}
|
||||
|
||||
function FeedAnswerGroup(props: { activityItem: FeedAnswerGroupItem }) {
|
||||
const { activityItem } = props
|
||||
const { contract, answer, bets, comments, user } = activityItem
|
||||
|
||||
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
||||
const outcomes = Object.keys(betGroups)
|
||||
|
||||
// Use the time of the last bet for the entire group
|
||||
const createdTime = bets[bets.length - 1].createdTime
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
|
||||
<UsersIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-neutral mb-2" style={{ fontSize: 15 }}>
|
||||
{answer.text}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Should highlight the entire Feed segment
|
||||
function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
|
||||
const { setExpanded } = props
|
||||
|
@ -725,272 +727,3 @@ function MaybeOutcomeLabel(props: { outcome: string; feedType: FeedType }) {
|
|||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// Missing feed items:
|
||||
// - Bet sold?
|
||||
type ActivityItem = {
|
||||
id: string
|
||||
type:
|
||||
| 'bet'
|
||||
| 'comment'
|
||||
| 'start'
|
||||
| 'betgroup'
|
||||
| 'answergroup'
|
||||
| 'close'
|
||||
| 'resolve'
|
||||
| 'expand'
|
||||
| undefined
|
||||
}
|
||||
|
||||
type FeedType =
|
||||
// Main homepage/fold feed,
|
||||
| 'activity'
|
||||
// Comments feed on a market
|
||||
| 'market'
|
||||
// Grouped for a multi-category outcome
|
||||
| 'multi'
|
||||
|
||||
function FeedItems(props: {
|
||||
contract: Contract
|
||||
items: ActivityItem[]
|
||||
feedType: FeedType
|
||||
setExpanded: (expanded: boolean) => void
|
||||
outcome?: string // Which multi-category outcome to filter
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, items, feedType, outcome, setExpanded, betRowClassName } =
|
||||
props
|
||||
const { outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
return (
|
||||
<div className="flow-root pr-2 md:pr-0">
|
||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
|
||||
{items.map((activityItem, activityItemIdx) => (
|
||||
<div key={activityItem.id} className="relative pb-6">
|
||||
{activityItemIdx !== items.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-start space-x-3">
|
||||
{activityItem.type === 'start' ? (
|
||||
feedType === 'activity' ? (
|
||||
<FeedQuestion contract={contract} />
|
||||
) : feedType === 'market' ? (
|
||||
<FeedDescription contract={contract} />
|
||||
) : feedType === 'multi' ? (
|
||||
<FeedAnswer contract={contract} outcome={outcome || '0'} />
|
||||
) : null
|
||||
) : activityItem.type === 'comment' ? (
|
||||
<FeedComment
|
||||
activityItem={activityItem}
|
||||
moreHref={contractPath(contract)}
|
||||
feedType={feedType}
|
||||
/>
|
||||
) : activityItem.type === 'bet' ? (
|
||||
<FeedBet activityItem={activityItem} feedType={feedType} />
|
||||
) : activityItem.type === 'betgroup' ? (
|
||||
<FeedBetGroup activityItem={activityItem} feedType={feedType} />
|
||||
) : activityItem.type === 'close' ? (
|
||||
<FeedClose contract={contract} />
|
||||
) : activityItem.type === 'resolve' ? (
|
||||
<FeedResolve contract={contract} />
|
||||
) : activityItem.type === 'expand' ? (
|
||||
<FeedExpand setExpanded={setExpanded} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContractFeed(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
feedType: FeedType
|
||||
outcome?: string // Which multi-category outcome to filter
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, feedType, outcome, betRowClassName } = props
|
||||
const { id, outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const user = useUser()
|
||||
|
||||
const comments = useComments(id) ?? props.comments
|
||||
|
||||
let bets = useBets(contract.id) ?? props.bets
|
||||
bets = isBinary
|
||||
? bets.filter((bet) => !bet.isAnte)
|
||||
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
||||
|
||||
if (feedType === 'multi') {
|
||||
bets = bets.filter((bet) => bet.outcome === outcome)
|
||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||
// Keep bets on comments or your bets where you can comment.
|
||||
const commentBetIds = new Set(comments.map((comment) => comment.betId))
|
||||
bets = bets.filter(
|
||||
(bet) =>
|
||||
commentBetIds.has(bet.id) ||
|
||||
canAddComment(bet.createdTime, user?.id === bet.userId)
|
||||
)
|
||||
}
|
||||
|
||||
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
||||
|
||||
const allItems: ActivityItem[] = [
|
||||
{ type: 'start', id: '0' },
|
||||
...groupBets(bets, comments, groupWindow, contract, user?.id),
|
||||
]
|
||||
if (contract.closeTime && contract.closeTime <= Date.now()) {
|
||||
allItems.push({ type: 'close', id: `${contract.closeTime}` })
|
||||
}
|
||||
if (contract.resolution) {
|
||||
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||
}
|
||||
if (feedType === 'multi') {
|
||||
// Hack to add some more padding above the 'multi' feedType, by adding a null item
|
||||
allItems.unshift({ type: undefined, id: '-1' })
|
||||
}
|
||||
|
||||
// If there are more than 5 items, only show the first, an expand item, and last 3
|
||||
let items = allItems
|
||||
if (!expanded && allItems.length > 5 && feedType == 'activity') {
|
||||
items = [
|
||||
allItems[0],
|
||||
{ type: 'expand', id: 'expand' },
|
||||
...allItems.slice(-3),
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<FeedItems
|
||||
contract={contract}
|
||||
items={items}
|
||||
feedType={feedType}
|
||||
setExpanded={setExpanded}
|
||||
betRowClassName={betRowClassName}
|
||||
outcome={outcome}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContractActivityFeed(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, betRowClassName } = props
|
||||
|
||||
const user = useUser()
|
||||
|
||||
let bets = props.bets.sort((b1, b2) => b1.createdTime - b2.createdTime)
|
||||
let comments = props.comments.sort(
|
||||
(c1, c2) => c1.createdTime - c2.createdTime
|
||||
)
|
||||
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||
// Keep last two comments.
|
||||
comments = comments.slice(-2)
|
||||
const lastBet = bets[bets.length - 1]
|
||||
|
||||
// Include up to 2 outcomes from comments and last bet.
|
||||
const outcomes = filterDefined(
|
||||
_.uniq([
|
||||
...comments.map(
|
||||
(comment) => bets.find((bet) => bet.id === comment.betId)?.outcome
|
||||
),
|
||||
lastBet?.outcome,
|
||||
])
|
||||
).slice(0, 2)
|
||||
|
||||
// Keep bets on selected outcomes.
|
||||
bets = bets.filter((bet) => outcomes.includes(bet.outcome))
|
||||
|
||||
const answerGroups = outcomes.map((outcome) => {
|
||||
const answerBets = bets.filter((bet) => bet.outcome === outcome)
|
||||
const answerComments = comments.filter((comment) =>
|
||||
answerBets.some((bet) => bet.id === comment.betId)
|
||||
)
|
||||
const answer = contract.answers?.find(
|
||||
(answer) => answer.id === outcome
|
||||
) as Answer
|
||||
|
||||
return {
|
||||
contract,
|
||||
answer,
|
||||
bets: answerBets,
|
||||
comments: answerComments,
|
||||
user,
|
||||
}
|
||||
})
|
||||
|
||||
console.log('comments', comments, 'outcomes', outcomes, 'bets', bets)
|
||||
}
|
||||
|
||||
const allItems: ActivityItem[] = [
|
||||
{ type: 'start', id: '0' },
|
||||
...groupBets(bets, comments, DAY_IN_MS, contract, user?.id),
|
||||
]
|
||||
if (contract.closeTime && contract.closeTime <= Date.now()) {
|
||||
allItems.push({ type: 'close', id: `${contract.closeTime}` })
|
||||
}
|
||||
if (contract.resolution) {
|
||||
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||
}
|
||||
|
||||
// Remove all but last bet group.
|
||||
const betGroups = allItems.filter((item) => item.type === 'betgroup')
|
||||
const lastBetGroup = betGroups[betGroups.length - 1]
|
||||
const filtered = allItems.filter(
|
||||
(item) => item.type !== 'betgroup' || item.id === lastBetGroup?.id
|
||||
)
|
||||
|
||||
// Only show the first item plus the last three items.
|
||||
const items =
|
||||
filtered.length > 3 ? [filtered[0], ...filtered.slice(-3)] : filtered
|
||||
|
||||
return (
|
||||
<FeedItems
|
||||
contract={contract}
|
||||
items={items}
|
||||
feedType="activity"
|
||||
setExpanded={() => {}}
|
||||
betRowClassName={betRowClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContractSummaryFeed(props: {
|
||||
contract: Contract
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, betRowClassName } = props
|
||||
const { outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
return (
|
||||
<div className="flow-root pr-2 md:pr-0">
|
||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
||||
<div className="relative pb-8">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<FeedQuestion contract={contract} showDescription />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
68
web/components/feed/find-active-contracts.ts
Normal file
68
web/components/feed/find-active-contracts.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import _ from 'lodash'
|
||||
import { Contract } from '../../lib/firebase/contracts'
|
||||
import { Comment } from '../../lib/firebase/comments'
|
||||
import { Bet } from '../../../common/bet'
|
||||
|
||||
const MAX_ACTIVE_CONTRACTS = 75
|
||||
|
||||
// This does NOT include comment times, since those aren't part of the contract atm.
|
||||
// TODO: Maybe store last activity time directly in the contract?
|
||||
// Pros: simplifies this code; cons: harder to tweak "activity" definition later
|
||||
function lastActivityTime(contract: Contract) {
|
||||
return Math.max(
|
||||
contract.resolutionTime || 0,
|
||||
contract.lastUpdatedTime,
|
||||
contract.createdTime
|
||||
)
|
||||
}
|
||||
|
||||
// Types of activity to surface:
|
||||
// - Comment on a market
|
||||
// - New market created
|
||||
// - Market resolved
|
||||
// - Bet on market
|
||||
export function findActiveContracts(
|
||||
allContracts: Contract[],
|
||||
recentComments: Comment[],
|
||||
recentBets: Bet[]
|
||||
) {
|
||||
const idToActivityTime = new Map<string, number>()
|
||||
function record(contractId: string, time: number) {
|
||||
// Only record if the time is newer
|
||||
const oldTime = idToActivityTime.get(contractId)
|
||||
idToActivityTime.set(contractId, Math.max(oldTime ?? 0, time))
|
||||
}
|
||||
|
||||
const contractsById = new Map(allContracts.map((c) => [c.id, c]))
|
||||
|
||||
// Record contract activity.
|
||||
for (const contract of allContracts) {
|
||||
record(contract.id, lastActivityTime(contract))
|
||||
}
|
||||
|
||||
// Add every contract that had a recent comment, too
|
||||
for (const comment of recentComments) {
|
||||
const contract = contractsById.get(comment.contractId)
|
||||
if (contract) record(contract.id, comment.createdTime)
|
||||
}
|
||||
|
||||
// Add contracts by last bet time.
|
||||
const contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||
const contractMostRecentBet = _.mapValues(
|
||||
contractBets,
|
||||
(bets) => _.maxBy(bets, (bet) => bet.createdTime) as Bet
|
||||
)
|
||||
for (const bet of Object.values(contractMostRecentBet)) {
|
||||
const contract = contractsById.get(bet.contractId)
|
||||
if (contract) record(contract.id, bet.createdTime)
|
||||
}
|
||||
|
||||
let activeContracts = allContracts.filter(
|
||||
(contract) => contract.visibility === 'public' && !contract.isResolved
|
||||
)
|
||||
activeContracts = _.sortBy(
|
||||
activeContracts,
|
||||
(c) => -(idToActivityTime.get(c.id) ?? 0)
|
||||
)
|
||||
return activeContracts.slice(0, MAX_ACTIVE_CONTRACTS)
|
||||
}
|
|
@ -4,11 +4,11 @@ import { useMemo, useRef } from 'react'
|
|||
import { Fold } from '../../common/fold'
|
||||
import { User } from '../../common/user'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
import { findActiveContracts } from '../components/feed/find-active-contracts'
|
||||
import { Bet } from '../lib/firebase/bets'
|
||||
import { Comment, getRecentComments } from '../lib/firebase/comments'
|
||||
import { Contract, getActiveContracts } from '../lib/firebase/contracts'
|
||||
import { listAllFolds } from '../lib/firebase/folds'
|
||||
import { findActiveContracts } from '../components/activity-feed'
|
||||
import { useInactiveContracts } from './use-contracts'
|
||||
import { useFollowedFolds } from './use-fold'
|
||||
import { useUserBetContracts } from './use-user-bets'
|
||||
|
|
|
@ -40,6 +40,10 @@ export async function createComment(
|
|||
return await setDoc(ref, comment)
|
||||
}
|
||||
|
||||
export const canAddComment = (createdTime: number, isSelf: boolean) => {
|
||||
return isSelf && Date.now() - createdTime < 60 * 60 * 1000
|
||||
}
|
||||
|
||||
function getCommentsCollection(contractId: string) {
|
||||
return collection(db, 'contracts', contractId, 'comments')
|
||||
}
|
||||
|
|
|
@ -12,10 +12,7 @@ import {
|
|||
getFoldBySlug,
|
||||
getFoldContracts,
|
||||
} from '../../../lib/firebase/folds'
|
||||
import {
|
||||
ActivityFeed,
|
||||
findActiveContracts,
|
||||
} from '../../../components/activity-feed'
|
||||
import { ActivityFeed } from '../../../components/feed/activity-feed'
|
||||
import { TagsList } from '../../../components/tags-list'
|
||||
import { Row } from '../../../components/layout/row'
|
||||
import { UserLink } from '../../../components/user-page'
|
||||
|
@ -43,6 +40,7 @@ import { filterDefined } from '../../../../common/util/array'
|
|||
import { useRecentBets } from '../../../hooks/use-bets'
|
||||
import { useRecentComments } from '../../../hooks/use-comments'
|
||||
import { LoadingIndicator } from '../../../components/loading-indicator'
|
||||
import { findActiveContracts } from '../../../components/feed/find-active-contracts'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||
|
|
|
@ -6,7 +6,10 @@ import _ from 'lodash'
|
|||
|
||||
import { Contract } from '../lib/firebase/contracts'
|
||||
import { Page } from '../components/page'
|
||||
import { ActivityFeed, SummaryActivityFeed } from '../components/activity-feed'
|
||||
import {
|
||||
ActivityFeed,
|
||||
SummaryActivityFeed,
|
||||
} from '../components/feed/activity-feed'
|
||||
import { Comment } from '../lib/firebase/comments'
|
||||
import FeedCreate from '../components/feed-create'
|
||||
import { Spacer } from '../components/layout/spacer'
|
||||
|
|
Loading…
Reference in New Issue
Block a user