From 5f472aa610c484afcd9ef2387ae0b03ec905ffbc Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Mon, 10 Jan 2022 04:23:41 -0500 Subject: [PATCH] Tracks all market activity on a single page --- web/hooks/use-comments.ts | 12 ++++- web/lib/firebase/comments.ts | 34 ++++++++++++- web/pages/activity.tsx | 94 ++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 web/pages/activity.tsx diff --git a/web/hooks/use-comments.ts b/web/hooks/use-comments.ts index 0a9dead3..bedc7598 100644 --- a/web/hooks/use-comments.ts +++ b/web/hooks/use-comments.ts @@ -1,5 +1,9 @@ import { useEffect, useState } from 'react' -import { Comment, listenForComments } from '../lib/firebase/comments' +import { + Comment, + listenForComments, + listenForRecentComments, +} from '../lib/firebase/comments' export const useComments = (contractId: string) => { const [comments, setComments] = useState('loading') @@ -10,3 +14,9 @@ export const useComments = (contractId: string) => { return comments } + +export const useRecentComments = () => { + const [recentComments, setRecentComments] = useState() + useEffect(() => listenForRecentComments(setRecentComments), []) + return recentComments +} diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index d46c6ee9..2d58e102 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -1,6 +1,17 @@ -import { doc, collection, onSnapshot, setDoc } from 'firebase/firestore' +import { + doc, + collection, + onSnapshot, + setDoc, + query, + collectionGroup, + getDocs, + where, + orderBy, +} from 'firebase/firestore' import { db } from './init' import { User } from './users' +import { listenForValues } from './utils' // Currently, comments are created after the bet, not atomically with the bet. // They're uniquely identified by the pair contractId/betId. @@ -58,3 +69,24 @@ export function mapCommentsByBetId(comments: Comment[]) { } return map } + +const DAY_IN_MS = 24 * 60 * 60 * 1000 + +// Define "recent" as "<3 days ago" for now +const recentCommentsQuery = query( + collectionGroup(db, 'comments'), + where('createdTime', '>', Date.now() - 3 * DAY_IN_MS), + orderBy('createdTime', 'desc') +) + +export async function getRecentComments() { + const snapshot = await getDocs(recentCommentsQuery) + const comments = snapshot.docs.map((doc) => doc.data() as Comment) + return comments +} + +export function listenForRecentComments( + setComments: (comments: Comment[]) => void +) { + return listenForValues(recentCommentsQuery, setComments) +} diff --git a/web/pages/activity.tsx b/web/pages/activity.tsx new file mode 100644 index 00000000..7d13fcd7 --- /dev/null +++ b/web/pages/activity.tsx @@ -0,0 +1,94 @@ +import _ from 'lodash' +import { ContractFeed } from '../components/contract-feed' +import { Row } from '../components/layout/row' +import { Page } from '../components/page' +import { Title } from '../components/title' +import { useRecentComments } from '../hooks/use-comments' +import { useContracts } from '../hooks/use-contracts' +import { Contract } from '../lib/firebase/contracts' +import { Comment } from '../lib/firebase/comments' + +function FeedCard(props: { contract: Contract }) { + const { contract } = props + return ( +
+ +
+ ) +} + +// 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 +function findActiveContracts( + allContracts: Contract[], + recentComments: Comment[] +) { + const idToActivityTime = new Map() + 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)) + } + + let contracts: Contract[] = [] + + // Find contracts with activity in the last 3 days + const DAY_IN_MS = 24 * 60 * 60 * 1000 + for (const contract of allContracts || []) { + if (lastActivityTime(contract) > Date.now() - 3 * DAY_IN_MS) { + contracts.push(contract) + record(contract.id, lastActivityTime(contract)) + } + } + + // Add every contract that had a recent comment, too + const contractsById = new Map(allContracts.map((c) => [c.id, c])) + for (const comment of recentComments) { + const contract = contractsById.get(comment.contractId) + if (contract) { + contracts.push(contract) + record(contract.id, comment.createdTime) + } + } + + contracts = _.uniqBy(contracts, (c) => c.id) + contracts = _.sortBy(contracts, (c) => -(idToActivityTime.get(c.id) ?? 0)) + return contracts +} + +export default function ActivityPage() { + const contracts = useContracts() || [] + const recentComments = useRecentComments() || [] + // TODO: Handle static props correctly? + const activeContracts = findActiveContracts(contracts, recentComments) + + return ( + + + {contracts ? ( + <Row className="gap-4"> + <div> + {activeContracts.map((contract) => ( + <FeedCard contract={contract} /> + ))} + </div> + </Row> + ) : ( + <></> + )} + </Page> + ) +}