Compare commits

...

1 Commits

Author SHA1 Message Date
James Grugett
e2657c75a3 Send only the collapsed activity feed items to client instead of all bets and comments 2022-02-06 16:26:06 -06:00
5 changed files with 185 additions and 95 deletions

View File

@ -6,7 +6,6 @@ import {
CheckIcon, CheckIcon,
DotsVerticalIcon, DotsVerticalIcon,
LockClosedIcon, LockClosedIcon,
StarIcon,
UserIcon, UserIcon,
UsersIcon, UsersIcon,
XIcon, XIcon,
@ -43,6 +42,7 @@ import { fromNow } from '../lib/util/time'
import BetRow from './bet-row' import BetRow from './bet-row'
import { parseTags } from '../../common/util/parse' import { parseTags } from '../../common/util/parse'
import { Avatar } from './avatar' import { Avatar } from './avatar'
import { User } from '../../common/user'
function FeedComment(props: { function FeedComment(props: {
activityItem: any activityItem: any
@ -470,7 +470,12 @@ function groupBets(
if (group.length == 1) { if (group.length == 1) {
items.push(toActivityItem(group[0])) items.push(toActivityItem(group[0]))
} else if (group.length > 1) { } 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 = [] group = []
} }
@ -504,6 +509,52 @@ function groupBets(
return items as ActivityItem[] 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' }) { function BetGroupSpan(props: { bets: Bet[]; outcome: 'YES' | 'NO' }) {
const { bets, outcome } = props const { bets, outcome } = props
@ -583,9 +634,7 @@ function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
) )
} }
// Missing feed items: export type ActivityItem = {
// - Bet sold?
type ActivityItem = {
id: string id: string
type: type:
| 'bet' | 'bet'
@ -595,48 +644,34 @@ type ActivityItem = {
| 'close' | 'close'
| 'resolve' | 'resolve'
| 'expand' | 'expand'
createdTime?: number
} }
export function ContractFeed(props: { export function ContractFeed(props: {
contract: Contract contract: Contract
bets: Bet[] activityItems: ActivityItem[]
comments: Comment[]
// Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market // Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market
feedType: 'activity' | 'market' feedType: 'activity' | 'market'
betRowClassName?: string betRowClassName?: string
}) { }) {
const { contract, feedType, betRowClassName } = props const { contract, activityItems, feedType, betRowClassName } = props
const { id } = contract const { id } = contract
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const user = useUser() const user = useUser()
let bets = useBets(id) ?? props.bets let bets = useBets(id)
bets = withoutAnteBets(contract, bets) 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 items =
bets && comments
const allItems = [ ? getContractFeedItems(contract, bets, comments, user, {
{ type: 'start', id: 0 }, feedType,
...groupBets(bets, comments, groupWindow, user?.id), expanded,
] })
if (contract.closeTime && contract.closeTime <= Date.now()) { : activityItems
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),
]
}
return ( return (
<div className="flow-root pr-2 md:pr-0"> <div className="flow-root pr-2 md:pr-0">

View File

@ -14,7 +14,7 @@ import { Row } from './layout/row'
import { Linkify } from './linkify' import { Linkify } from './linkify'
import clsx from 'clsx' import clsx from 'clsx'
import { ContractDetails, ResolutionOrChance } from './contract-card' import { ContractDetails, ResolutionOrChance } from './contract-card'
import { ContractFeed } from './contract-feed' import { ContractFeed, getContractFeedItems } from './contract-feed'
import { TweetButton } from './tweet-button' import { TweetButton } from './tweet-button'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { Comment } from '../../common/comment' import { Comment } from '../../common/comment'
@ -46,6 +46,11 @@ export const ContractOverview = (props: {
const url = `https://manifold.markets${contractPath(contract)}` const url = `https://manifold.markets${contractPath(contract)}`
const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}` const tweetText = `${tweetQuestion}\n\n${tweetDescription}\n\n${url}`
const activityItems = getContractFeedItems(contract, bets, comments, user, {
feedType: 'market',
expanded: true,
})
return ( return (
<Col className={clsx('mb-6', className)}> <Col className={clsx('mb-6', className)}>
<Row className="justify-between gap-4 px-2"> <Row className="justify-between gap-4 px-2">
@ -131,8 +136,7 @@ export const ContractOverview = (props: {
<ContractFeed <ContractFeed
contract={contract} contract={contract}
bets={bets} activityItems={activityItems}
comments={comments}
feedType="market" feedType="market"
betRowClassName="md:hidden !mt-0" betRowClassName="md:hidden !mt-0"
/> />

View File

@ -1,5 +1,9 @@
import _ from 'lodash' import _ from 'lodash'
import { ContractFeed } from '../components/contract-feed' import {
ActivityItem,
ContractFeed,
getContractFeedItems,
} from '../components/contract-feed'
import { Page } from '../components/page' import { Page } from '../components/page'
import { Contract } from '../lib/firebase/contracts' import { Contract } from '../lib/firebase/contracts'
import { Comment } from '../lib/firebase/comments' import { Comment } from '../lib/firebase/comments'
@ -85,27 +89,86 @@ export function findActiveContracts(
return contracts.slice(0, MAX_ACTIVE_CONTRACTS) 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: { export function ActivityFeed(props: {
contracts: Contract[] contracts: Contract[]
contractBets: Bet[][] contractActivityItems: ActivityItem[][]
contractComments: Comment[][]
}) { }) {
const { contracts, contractBets, contractComments } = props const { contracts, contractActivityItems } = props
return contracts.length > 0 ? ( return contracts.length > 0 ? (
<Col className="items-center"> <Col className="items-center">
<Col className="w-full max-w-3xl"> <Col className="w-full max-w-3xl">
<Col className="w-full bg-white self-center divide-gray-300 divide-y"> <Col className="w-full bg-white self-center divide-gray-300 divide-y">
{contracts.map((contract, i) => ( {contracts.map((contract, i) => {
return (
<div key={contract.id} className="py-6 px-2 sm:px-4"> <div key={contract.id} className="py-6 px-2 sm:px-4">
<ContractFeed <ContractFeed
contract={contract} contract={contract}
bets={contractBets[i]} activityItems={contractActivityItems[i]}
comments={contractComments[i]}
feedType="activity" feedType="activity"
/> />
</div> </div>
))} )
})}
</Col> </Col>
</Col> </Col>
</Col> </Col>
@ -117,7 +180,7 @@ export function ActivityFeed(props: {
export default function ActivityPage() { export default function ActivityPage() {
return ( return (
<Page> <Page>
<ActivityFeed contracts={[]} contractBets={[]} contractComments={[]} /> <ActivityFeed contracts={[]} contractActivityItems={[]} />
</Page> </Page>
) )
} }

View File

@ -39,6 +39,7 @@ import { FollowFoldButton } from '../../../components/follow-fold-button'
import FeedCreate from '../../../components/feed-create' import FeedCreate from '../../../components/feed-create'
import { SEO } from '../../../components/SEO' import { SEO } from '../../../components/SEO'
import { useTaggedContracts } from '../../../hooks/use-contracts' import { useTaggedContracts } from '../../../hooks/use-contracts'
import { getContractFeedItems } from '../../../components/contract-feed'
export async function getStaticProps(props: { params: { slugs: string[] } }) { export async function getStaticProps(props: { params: { slugs: string[] } }) {
const { slugs } = props.params const { slugs } = props.params
@ -175,6 +176,16 @@ export default function FoldPage(props: {
(contract) => contractsMap[contract.id] (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]) { if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
return <Custom404 /> return <Custom404 />
} }
@ -260,8 +271,7 @@ export default function FoldPage(props: {
<> <>
<ActivityFeed <ActivityFeed
contracts={activeContracts} contracts={activeContracts}
contractBets={activeContractBets} contractActivityItems={contractActivityItems}
contractComments={activeContractComments}
/> />
{activeContracts.length === 0 && ( {activeContracts.length === 0 && (
<div className="text-gray-500 mt-4 mx-2 lg:mx-0"> <div className="text-gray-500 mt-4 mx-2 lg:mx-0">

View File

@ -4,9 +4,9 @@ import _ from 'lodash'
import { Contract, listAllContracts } from '../lib/firebase/contracts' import { Contract, listAllContracts } from '../lib/firebase/contracts'
import { Page } from '../components/page' import { Page } from '../components/page'
import { ActivityFeed, findActiveContracts } from './activity' import { ActivityFeed, getActivity } from './activity'
import { Comment, listAllComments } from '../lib/firebase/comments' import { listAllComments } from '../lib/firebase/comments'
import { Bet, listAllBets } from '../lib/firebase/bets' import { listAllBets } from '../lib/firebase/bets'
import FeedCreate from '../components/feed-create' import FeedCreate from '../components/feed-create'
import { Spacer } from '../components/layout/spacer' import { Spacer } from '../components/layout/spacer'
import { Col } from '../components/layout/col' import { Col } from '../components/layout/col'
@ -22,6 +22,7 @@ import { SearchIcon } from '@heroicons/react/outline'
import { Row } from '../components/layout/row' import { Row } from '../components/layout/row'
import { SparklesIcon } from '@heroicons/react/solid' import { SparklesIcon } from '@heroicons/react/solid'
import { useFollowedFolds } from '../hooks/use-fold' import { useFollowedFolds } from '../hooks/use-fold'
import { ActivityItem } from '../components/contract-feed'
export async function getStaticProps() { export async function getStaticProps() {
const [contracts, folds] = await Promise.all([ const [contracts, folds] = await Promise.all([
@ -36,9 +37,7 @@ export async function getStaticProps() {
return { return {
props: { props: {
contracts, ...getActivity(contracts, contractBets, contractComments),
contractBets,
contractComments,
folds, folds,
}, },
@ -48,18 +47,14 @@ export async function getStaticProps() {
const Home = (props: { const Home = (props: {
contracts: Contract[] contracts: Contract[]
contractBets: Bet[][] contractActivityItems: ActivityItem[][]
contractComments: Comment[][]
folds: Fold[] folds: Fold[]
}) => { }) => {
const { contractBets, contractComments, folds } = props const { contractActivityItems, folds } = props
const user = useUser() const user = useUser()
const contracts = useUpdatedContracts(props.contracts) const contracts = useUpdatedContracts(props.contracts)
const contractIdToIndex = _.fromPairs(
contracts.map((contract, index) => [contract.id, index])
)
const followedFoldIds = useFollowedFolds(user) const followedFoldIds = useFollowedFolds(user)
const followedFolds = filterDefined( const followedFolds = filterDefined(
@ -76,36 +71,20 @@ const Home = (props: {
const feedContracts = const feedContracts =
followedFoldIds && yourBetContracts followedFoldIds && yourBetContracts
? contracts.filter( ? contracts
.filter(
(contract) => (contract) =>
contract.lowercaseTags.some((tag) => tagSet.has(tag)) || contract.lowercaseTags.some((tag) => tagSet.has(tag)) ||
yourBetContracts.has(contract.id) yourBetContracts.has(contract.id)
) )
.slice(0, 75)
: undefined : undefined
const oneDayMS = 24 * 60 * 60 * 1000 const feedContractSet = new Set(feedContracts?.map((contract) => contract.id))
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 activeContracts = const feedActivityItems = contractActivityItems.filter((_, index) =>
feedContracts && feedContractSet.has(contracts[index].id)
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]]
)
: []
if (user === null) { if (user === null) {
Router.replace('/') Router.replace('/')
@ -143,11 +122,10 @@ const Home = (props: {
<SparklesIcon className="inline w-5 h-5" aria-hidden="true" /> <SparklesIcon className="inline w-5 h-5" aria-hidden="true" />
Recent activity Recent activity
</Row> </Row>
{activeContracts ? ( {feedContracts ? (
<ActivityFeed <ActivityFeed
contracts={activeContracts} contracts={feedContracts}
contractBets={activeBets} contractActivityItems={feedActivityItems}
contractComments={activeComments}
/> />
) : ( ) : (
<LoadingIndicator className="mt-4" /> <LoadingIndicator className="mt-4" />