Show custom feed of contracts from folds your follow or have bet on.
This commit is contained in:
		
							parent
							
								
									fdbaa5270b
								
							
						
					
					
						commit
						3cf4cb7a77
					
				
							
								
								
									
										3
									
								
								common/util/array.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								common/util/array.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export function filterDefined<T>(array: (T | null | undefined)[]) { | ||||||
|  |   return array.filter((item) => item) as T[] | ||||||
|  | } | ||||||
|  | @ -52,5 +52,9 @@ service cloud.firestore { | ||||||
|       allow read; |       allow read; | ||||||
|       allow write: if request.auth.uid == userId; |       allow write: if request.auth.uid == userId; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     match /{somePath=**}/followers/{userId} { | ||||||
|  |       allow read; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | @ -32,12 +32,12 @@ import { OutcomeLabel, YesLabel, NoLabel, MarketLabel } from './outcome-label' | ||||||
| 
 | 
 | ||||||
| export function BetsList(props: { user: User }) { | export function BetsList(props: { user: User }) { | ||||||
|   const { user } = props |   const { user } = props | ||||||
|   const bets = useUserBets(user?.id ?? '') |   const bets = useUserBets(user.id) | ||||||
| 
 | 
 | ||||||
|   const [contracts, setContracts] = useState<Contract[]>([]) |   const [contracts, setContracts] = useState<Contract[]>([]) | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const loadedBets = bets === 'loading' ? [] : bets |     const loadedBets = bets ? bets : [] | ||||||
|     const contractIds = _.uniq(loadedBets.map((bet) => bet.contractId)) |     const contractIds = _.uniq(loadedBets.map((bet) => bet.contractId)) | ||||||
| 
 | 
 | ||||||
|     let disposed = false |     let disposed = false | ||||||
|  | @ -52,7 +52,7 @@ export function BetsList(props: { user: User }) { | ||||||
|     } |     } | ||||||
|   }, [bets]) |   }, [bets]) | ||||||
| 
 | 
 | ||||||
|   if (bets === 'loading') { |   if (!bets) { | ||||||
|     return <></> |     return <></> | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||||
| import { Bet, listenForUserBets } from '../lib/firebase/bets' | import { Bet, listenForUserBets } from '../lib/firebase/bets' | ||||||
| 
 | 
 | ||||||
| export const useUserBets = (userId: string) => { | export const useUserBets = (userId: string | undefined) => { | ||||||
|   const [bets, setBets] = useState<Bet[] | 'loading'>('loading') |   const [bets, setBets] = useState<Bet[] | undefined>(undefined) | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     return listenForUserBets(userId, setBets) |     if (userId) return listenForUserBets(userId, setBets) | ||||||
|   }, [userId]) |   }, [userId]) | ||||||
| 
 | 
 | ||||||
|   return bets |   return bets | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| import { | import { | ||||||
|   collection, |   collection, | ||||||
|  |   collectionGroup, | ||||||
|   deleteDoc, |   deleteDoc, | ||||||
|   doc, |   doc, | ||||||
|  |   getDocs, | ||||||
|   query, |   query, | ||||||
|   setDoc, |   setDoc, | ||||||
|   updateDoc, |   updateDoc, | ||||||
|  | @ -12,7 +14,7 @@ import { Fold } from '../../../common/fold' | ||||||
| import { Contract, contractCollection } from './contracts' | import { Contract, contractCollection } from './contracts' | ||||||
| import { db } from './init' | import { db } from './init' | ||||||
| import { User } from './users' | import { User } from './users' | ||||||
| import { getValues, listenForValue, listenForValues } from './utils' | import { getValue, getValues, listenForValue, listenForValues } from './utils' | ||||||
| 
 | 
 | ||||||
| const foldCollection = collection(db, 'folds') | const foldCollection = collection(db, 'folds') | ||||||
| 
 | 
 | ||||||
|  | @ -39,6 +41,10 @@ export function listenForFolds(setFolds: (folds: Fold[]) => void) { | ||||||
|   return listenForValues(foldCollection, setFolds) |   return listenForValues(foldCollection, setFolds) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function getFold(foldId: string) { | ||||||
|  |   return getValue<Fold>(doc(foldCollection, foldId)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function getFoldBySlug(slug: string) { | export async function getFoldBySlug(slug: string) { | ||||||
|   const q = query(foldCollection, where('slug', '==', slug)) |   const q = query(foldCollection, where('slug', '==', slug)) | ||||||
|   const folds = await getValues<Fold>(q) |   const folds = await getValues<Fold>(q) | ||||||
|  | @ -147,3 +153,13 @@ export async function getFoldsByTags(tags: string[]) { | ||||||
| 
 | 
 | ||||||
|   return _.sortBy(folds, (fold) => -1 * fold.followCount) |   return _.sortBy(folds, (fold) => -1 * fold.followCount) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export async function getFollowedFolds(userId: string) { | ||||||
|  |   const snapshot = await getDocs( | ||||||
|  |     query(collectionGroup(db, 'followers'), where('userId', '==', userId)) | ||||||
|  |   ) | ||||||
|  |   const foldIds = snapshot.docs.map( | ||||||
|  |     (doc) => doc.ref.parent.parent?.id as string | ||||||
|  |   ) | ||||||
|  |   return foldIds | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ export function ActivityFeed(props: { | ||||||
|             <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] ?? []} |                 bets={contractBets[i]} | ||||||
|                 comments={contractComments[i]} |                 comments={contractComments[i]} | ||||||
|                 feedType="activity" |                 feedType="activity" | ||||||
|               /> |               /> | ||||||
|  |  | ||||||
|  | @ -1,70 +1,123 @@ | ||||||
| import React from 'react' | import React, { useEffect, useState } from 'react' | ||||||
| import Router from 'next/router' | import Router from 'next/router' | ||||||
| import _ from 'lodash' | 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, findActiveContracts } from './activity' | ||||||
| import { | import { Comment, listAllComments } from '../lib/firebase/comments' | ||||||
|   getRecentComments, | import { Bet, listAllBets } from '../lib/firebase/bets' | ||||||
|   Comment, |  | ||||||
|   listAllComments, |  | ||||||
| } from '../lib/firebase/comments' |  | ||||||
| import { Bet, getRecentBets, 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' | ||||||
| import { useUser } from '../hooks/use-user' | import { useUser } from '../hooks/use-user' | ||||||
| import { useContracts } from '../hooks/use-contracts' | import { useContracts } from '../hooks/use-contracts' | ||||||
|  | import { getFollowedFolds, listAllFolds } from '../lib/firebase/folds' | ||||||
|  | import { Fold } from '../../common/fold' | ||||||
|  | import { filterDefined } from '../../common/util/array' | ||||||
|  | import { useUserBets } from '../hooks/use-user-bets' | ||||||
| 
 | 
 | ||||||
| export async function getStaticProps() { | export async function getStaticProps() { | ||||||
|   const [contracts, recentComments, recentBets] = await Promise.all([ |   const [contracts, folds] = await Promise.all([ | ||||||
|     listAllContracts().catch((_) => []), |     listAllContracts().catch((_) => []), | ||||||
|     getRecentComments().catch(() => []), |     listAllFolds().catch(() => []), | ||||||
|     getRecentBets().catch(() => []), |  | ||||||
|   ]) |   ]) | ||||||
| 
 | 
 | ||||||
|   const activeContracts = findActiveContracts( |  | ||||||
|     contracts, |  | ||||||
|     recentComments, |  | ||||||
|     recentBets |  | ||||||
|   ) |  | ||||||
|   const activeContractBets = await Promise.all( |  | ||||||
|     activeContracts.map((contract) => listAllBets(contract.id).catch((_) => [])) |  | ||||||
|   ) |  | ||||||
|   const activeContractComments = await Promise.all( |  | ||||||
|     activeContracts.map((contract) => |  | ||||||
|       listAllComments(contract.id).catch((_) => []) |  | ||||||
|     ) |  | ||||||
|   ) |  | ||||||
| 
 |  | ||||||
|   return { |   return { | ||||||
|     props: { |     props: { | ||||||
|       activeContracts, |       contracts, | ||||||
|       activeContractBets, |       folds, | ||||||
|       activeContractComments, |  | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     revalidate: 60, // regenerate after a minute
 |     revalidate: 60, // regenerate after a minute
 | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const Home = (props: { | const Home = (props: { contracts: Contract[]; folds: Fold[] }) => { | ||||||
|   activeContracts: Contract[] |   const { folds } = props | ||||||
|   activeContractBets: Bet[][] |  | ||||||
|   activeContractComments: Comment[][] |  | ||||||
| }) => { |  | ||||||
|   const { activeContracts, activeContractBets, activeContractComments } = props |  | ||||||
| 
 | 
 | ||||||
|   const user = useUser() |   const user = useUser() | ||||||
| 
 | 
 | ||||||
|   const contracts = useContracts() ?? activeContracts |   const contracts = useContracts() ?? props.contracts | ||||||
|   const contractsMap = _.fromPairs( | 
 | ||||||
|     contracts.map((contract) => [contract.id, contract]) |   const [followedFoldIds, setFollowedFoldIds] = useState<string[] | undefined>( | ||||||
|  |     undefined | ||||||
|   ) |   ) | ||||||
|   const updatedContracts = activeContracts.map( | 
 | ||||||
|     (contract) => contractsMap[contract.id] |   useEffect(() => { | ||||||
|  |     if (user) { | ||||||
|  |       getFollowedFolds(user.id).then((foldIds) => setFollowedFoldIds(foldIds)) | ||||||
|  |     } | ||||||
|  |   }, [user]) | ||||||
|  | 
 | ||||||
|  |   const followedFolds = filterDefined( | ||||||
|  |     (followedFoldIds ?? []).map((id) => folds.find((fold) => fold.id === id)) | ||||||
|   ) |   ) | ||||||
|  |   const tagSet = new Set( | ||||||
|  |     _.flatten(followedFolds.map((fold) => fold.lowercaseTags)) | ||||||
|  |   ) | ||||||
|  | 
 | ||||||
|  |   const yourBets = useUserBets(user?.id) | ||||||
|  |   const yourBetContracts = new Set( | ||||||
|  |     (yourBets ?? []).map((bet) => bet.contractId) | ||||||
|  |   ) | ||||||
|  | 
 | ||||||
|  |   const feedContracts = | ||||||
|  |     followedFoldIds && yourBets | ||||||
|  |       ? contracts.filter( | ||||||
|  |           (contract) => | ||||||
|  |             contract.lowercaseTags.some((tag) => tagSet.has(tag)) || | ||||||
|  |             yourBetContracts.has(contract.id) | ||||||
|  |         ) | ||||||
|  |       : undefined | ||||||
|  | 
 | ||||||
|  |   const feedContractsKey = feedContracts?.map(({ id }) => id).join(',') | ||||||
|  | 
 | ||||||
|  |   const [feedBets, setFeedBets] = useState<Bet[][] | undefined>() | ||||||
|  |   const [feedComments, setFeedComments] = useState<Comment[][] | undefined>() | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (feedContracts) { | ||||||
|  |       Promise.all( | ||||||
|  |         feedContracts.map((contract) => listAllBets(contract.id)) | ||||||
|  |       ).then(setFeedBets) | ||||||
|  |       Promise.all( | ||||||
|  |         feedContracts.map((contract) => listAllComments(contract.id)) | ||||||
|  |       ).then(setFeedComments) | ||||||
|  |     } | ||||||
|  |     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|  |   }, [feedContractsKey]) | ||||||
|  | 
 | ||||||
|  |   const oneDayMS = 24 * 60 * 60 * 1000 | ||||||
|  |   const recentBets = | ||||||
|  |     feedBets && | ||||||
|  |     feedBets.flat().filter((bet) => bet.createdTime > Date.now() - oneDayMS) | ||||||
|  | 
 | ||||||
|  |   const activeContracts = | ||||||
|  |     feedContracts && | ||||||
|  |     feedComments && | ||||||
|  |     recentBets && | ||||||
|  |     findActiveContracts(feedContracts, feedComments.flat(), recentBets, 365) | ||||||
|  | 
 | ||||||
|  |   const contractBets = activeContracts | ||||||
|  |     ? activeContracts.map( | ||||||
|  |         (contract) => feedBets[feedContracts.indexOf(contract)] | ||||||
|  |       ) | ||||||
|  |     : [] | ||||||
|  |   const contractComments = activeContracts | ||||||
|  |     ? activeContracts.map( | ||||||
|  |         (contract) => feedComments[feedContracts.indexOf(contract)] | ||||||
|  |       ) | ||||||
|  |     : [] | ||||||
|  | 
 | ||||||
|  |   console.log({ | ||||||
|  |     followedFoldIds, | ||||||
|  |     followedFolds, | ||||||
|  |     yourBetContracts, | ||||||
|  |     feedContracts, | ||||||
|  |     feedBets, | ||||||
|  |     feedComments, | ||||||
|  |   }) | ||||||
| 
 | 
 | ||||||
|   if (user === null) { |   if (user === null) { | ||||||
|     Router.replace('/') |     Router.replace('/') | ||||||
|  | @ -74,14 +127,16 @@ const Home = (props: { | ||||||
|   return ( |   return ( | ||||||
|     <Page assertUser="signed-in"> |     <Page assertUser="signed-in"> | ||||||
|       <Col className="items-center"> |       <Col className="items-center"> | ||||||
|         <Col className="max-w-3xl"> |         <Col className="max-w-3xl w-full"> | ||||||
|           <FeedCreate user={user ?? undefined} /> |           <FeedCreate user={user ?? undefined} /> | ||||||
|           <Spacer h={4} /> |           <Spacer h={4} /> | ||||||
|  |           {activeContracts ? ( | ||||||
|             <ActivityFeed |             <ActivityFeed | ||||||
|             contracts={updatedContracts} |               contracts={activeContracts} | ||||||
|             contractBets={activeContractBets} |               contractBets={contractBets} | ||||||
|             contractComments={activeContractComments} |               contractComments={contractComments} | ||||||
|             /> |             /> | ||||||
|  |           ) : null} | ||||||
|         </Col> |         </Col> | ||||||
|       </Col> |       </Col> | ||||||
|     </Page> |     </Page> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user