diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index 32db9013..926f8d88 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -6,7 +6,6 @@ import { CheckIcon, DotsVerticalIcon, LockClosedIcon, - StarIcon, UserIcon, UsersIcon, XIcon, @@ -43,6 +42,7 @@ import { fromNow } from '../lib/util/time' import BetRow from './bet-row' import { parseTags } from '../../common/util/parse' import { Avatar } from './avatar' +import { User } from '../../common/user' function FeedComment(props: { activityItem: any @@ -470,7 +470,12 @@ function groupBets( if (group.length == 1) { items.push(toActivityItem(group[0])) } else if (group.length > 1) { - items.push({ type: 'betgroup', bets: [...group], id: group[0].id }) + items.push({ + type: 'betgroup', + bets: [...group], + id: group[0].id, + createdTime: group[group.length - 1].createdTime, + }) } group = [] } @@ -504,6 +509,52 @@ function groupBets( return items as ActivityItem[] } +export function getContractFeedItems( + contract: Contract, + bets: Bet[], + comments: Comment[], + user: User | null | undefined, + options: { feedType: 'activity' | 'market'; expanded: boolean } +) { + const { feedType, expanded } = options + const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS + + const allItems: ActivityItem[] = [ + { type: 'start', id: '0', createdTime: contract.createdTime }, + ...groupBets( + bets.filter((bet) => !bet.isAnte), + comments, + groupWindow, + user?.id + ), + ] + if (contract.closeTime && contract.closeTime <= Date.now()) { + allItems.push({ + type: 'close', + id: `${contract.closeTime}`, + createdTime: contract.closeTime, + }) + } + if (contract.resolution) { + allItems.push({ + type: 'resolve', + id: `${contract.resolutionTime}`, + createdTime: contract.resolutionTime, + }) + } + + // 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 items +} + function BetGroupSpan(props: { bets: Bet[]; outcome: 'YES' | 'NO' }) { const { bets, outcome } = props @@ -583,9 +634,7 @@ function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) { ) } -// Missing feed items: -// - Bet sold? -type ActivityItem = { +export type ActivityItem = { id: string type: | 'bet' @@ -595,48 +644,34 @@ type ActivityItem = { | 'close' | 'resolve' | 'expand' + + createdTime?: number } export function ContractFeed(props: { contract: Contract - bets: Bet[] - comments: Comment[] + activityItems: ActivityItem[] // Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market feedType: 'activity' | 'market' betRowClassName?: string }) { - const { contract, feedType, betRowClassName } = props + const { contract, activityItems, feedType, betRowClassName } = props const { id } = contract const [expanded, setExpanded] = useState(false) const user = useUser() - let bets = useBets(id) ?? props.bets + let bets = useBets(id) bets = withoutAnteBets(contract, bets) - const comments = useComments(id) ?? props.comments + const comments = useComments(id) - const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS - - const allItems = [ - { type: 'start', id: 0 }, - ...groupBets(bets, comments, groupWindow, 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 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), - ] - } + const items = + bets && comments + ? getContractFeedItems(contract, bets, comments, user, { + feedType, + expanded, + }) + : activityItems return (
diff --git a/web/components/contract-overview.tsx b/web/components/contract-overview.tsx index 3528a727..1de1d157 100644 --- a/web/components/contract-overview.tsx +++ b/web/components/contract-overview.tsx @@ -14,7 +14,7 @@ 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 { ContractFeed, getContractFeedItems } from './contract-feed' import { TweetButton } from './tweet-button' import { Bet } from '../../common/bet' import { Comment } from '../../common/comment' @@ -46,6 +46,11 @@ export const ContractOverview = (props: { const url = `https://manifold.markets${contractPath(contract)}` const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}` + const activityItems = getContractFeedItems(contract, bets, comments, user, { + feedType: 'market', + expanded: true, + }) + return ( @@ -131,8 +136,7 @@ export const ContractOverview = (props: { diff --git a/web/pages/activity.tsx b/web/pages/activity.tsx index be5a9809..15df7311 100644 --- a/web/pages/activity.tsx +++ b/web/pages/activity.tsx @@ -1,5 +1,9 @@ import _ from 'lodash' -import { ContractFeed } from '../components/contract-feed' +import { + ActivityItem, + ContractFeed, + getContractFeedItems, +} from '../components/contract-feed' import { Page } from '../components/page' import { Contract } from '../lib/firebase/contracts' import { Comment } from '../lib/firebase/comments' @@ -85,27 +89,86 @@ export function findActiveContracts( return contracts.slice(0, MAX_ACTIVE_CONTRACTS) } +export function getActivity( + contracts: Contract[], + contractBets: Bet[][], + contractComments: Comment[][] +) { + const contractActivityItems = contracts.map((contract, i) => { + const bets = contractBets[i] + const comments = contractComments[i] + return getContractFeedItems(contract, bets, comments, undefined, { + expanded: false, + feedType: 'activity', + }) + }) + + const hotMarketTimes: { [contractId: string]: number } = {} + + // Add recent top-trading contracts, ordered by last bet. + const DAY_IN_MS = 24 * 60 * 60 * 1000 + const contractTotalBets = _.map(contractBets, (bets) => { + const recentBets = bets.filter( + (bet) => bet.createdTime > Date.now() - DAY_IN_MS + ) + return _.sumBy(recentBets, (bet) => bet.amount) + }) + const topTradedContracts = _.sortBy( + contractTotalBets.map((total, index) => [total, index] as [number, number]), + ([total, _]) => -1 * total + ).slice(0, MAX_HOT_MARKETS) + + for (const [_total, index] of topTradedContracts) { + const lastBet = _.last(contractBets[index]) + if (lastBet) { + hotMarketTimes[contracts[index].id] = lastBet.createdTime + } + } + + const orderedContracts = _.sortBy( + contracts.map((c, index) => [c, index] as const), + ([contract, index]) => { + const activeTypes = ['start', 'comment', 'close', 'resolve'] + const { createdTime } = _.last( + contractActivityItems[index].filter((item) => + activeTypes.includes(item.type) + ) + ) as ActivityItem + const activeTime = createdTime ?? 0 + const hotTime = hotMarketTimes[contract.id] ?? 0 + return -1 * Math.max(activeTime, hotTime) + } + ) + + return { + contracts: orderedContracts.map(([_, index]) => contracts[index]), + contractActivityItems: orderedContracts.map( + ([_, index]) => contractActivityItems[index] + ), + } +} + export function ActivityFeed(props: { contracts: Contract[] - contractBets: Bet[][] - contractComments: Comment[][] + contractActivityItems: ActivityItem[][] }) { - const { contracts, contractBets, contractComments } = props + const { contracts, contractActivityItems } = props return contracts.length > 0 ? ( - {contracts.map((contract, i) => ( -
- -
- ))} + {contracts.map((contract, i) => { + return ( +
+ +
+ ) + })} @@ -117,7 +180,7 @@ export function ActivityFeed(props: { export default function ActivityPage() { return ( - + ) } diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index f43f847a..e0e9103b 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -39,6 +39,7 @@ import { FollowFoldButton } from '../../../components/follow-fold-button' import FeedCreate from '../../../components/feed-create' import { SEO } from '../../../components/SEO' import { useTaggedContracts } from '../../../hooks/use-contracts' +import { getContractFeedItems } from '../../../components/contract-feed' export async function getStaticProps(props: { params: { slugs: string[] } }) { const { slugs } = props.params @@ -175,6 +176,16 @@ export default function FoldPage(props: { (contract) => contractsMap[contract.id] ) + const contractActivityItems = activeContracts.map((contract, index) => + getContractFeedItems( + contract, + activeContractBets[index], + activeContractComments[index], + user, + { feedType: 'activity', expanded: false } + ) + ) + if (fold === null || !foldSubpages.includes(page) || slugs[2]) { return } @@ -260,8 +271,7 @@ export default function FoldPage(props: { <> {activeContracts.length === 0 && (
diff --git a/web/pages/home.tsx b/web/pages/home.tsx index f80c32b1..eef04ff8 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -4,9 +4,9 @@ import _ from 'lodash' import { Contract, listAllContracts } from '../lib/firebase/contracts' import { Page } from '../components/page' -import { ActivityFeed, findActiveContracts } from './activity' -import { Comment, listAllComments } from '../lib/firebase/comments' -import { Bet, listAllBets } from '../lib/firebase/bets' +import { ActivityFeed, getActivity } from './activity' +import { listAllComments } from '../lib/firebase/comments' +import { listAllBets } from '../lib/firebase/bets' import FeedCreate from '../components/feed-create' import { Spacer } from '../components/layout/spacer' import { Col } from '../components/layout/col' @@ -22,6 +22,7 @@ import { SearchIcon } from '@heroicons/react/outline' import { Row } from '../components/layout/row' import { SparklesIcon } from '@heroicons/react/solid' import { useFollowedFolds } from '../hooks/use-fold' +import { ActivityItem } from '../components/contract-feed' export async function getStaticProps() { const [contracts, folds] = await Promise.all([ @@ -36,9 +37,7 @@ export async function getStaticProps() { return { props: { - contracts, - contractBets, - contractComments, + ...getActivity(contracts, contractBets, contractComments), folds, }, @@ -48,18 +47,14 @@ export async function getStaticProps() { const Home = (props: { contracts: Contract[] - contractBets: Bet[][] - contractComments: Comment[][] + contractActivityItems: ActivityItem[][] folds: Fold[] }) => { - const { contractBets, contractComments, folds } = props + const { contractActivityItems, folds } = props const user = useUser() const contracts = useUpdatedContracts(props.contracts) - const contractIdToIndex = _.fromPairs( - contracts.map((contract, index) => [contract.id, index]) - ) const followedFoldIds = useFollowedFolds(user) const followedFolds = filterDefined( @@ -76,36 +71,20 @@ const Home = (props: { const feedContracts = followedFoldIds && yourBetContracts - ? contracts.filter( - (contract) => - contract.lowercaseTags.some((tag) => tagSet.has(tag)) || - yourBetContracts.has(contract.id) - ) + ? contracts + .filter( + (contract) => + contract.lowercaseTags.some((tag) => tagSet.has(tag)) || + yourBetContracts.has(contract.id) + ) + .slice(0, 75) : undefined - const oneDayMS = 24 * 60 * 60 * 1000 - const recentBets = (feedContracts ?? []) - .map((c) => contractBets[contractIdToIndex[c.id]]) - .flat() - .filter((bet) => bet.createdTime > Date.now() - oneDayMS) - const feedComments = (feedContracts ?? []) - .map((c) => contractComments[contractIdToIndex[c.id]]) - .flat() + const feedContractSet = new Set(feedContracts?.map((contract) => contract.id)) - const activeContracts = - feedContracts && - findActiveContracts(feedContracts, feedComments, recentBets, 365) - - const activeBets = activeContracts - ? activeContracts.map( - (contract) => contractBets[contractIdToIndex[contract.id]] - ) - : [] - const activeComments = activeContracts - ? activeContracts.map( - (contract) => contractComments[contractIdToIndex[contract.id]] - ) - : [] + const feedActivityItems = contractActivityItems.filter((_, index) => + feedContractSet.has(contracts[index].id) + ) if (user === null) { Router.replace('/') @@ -143,11 +122,10 @@ const Home = (props: {