From 397d90c6b770a55c595724b6309482b73f0a2d8a Mon Sep 17 00:00:00 2001 From: Forrest Wolf Date: Thu, 2 Jun 2022 19:40:41 -0500 Subject: [PATCH 1/4] Replace some more uses of `any` with more specific types (#389) * Give confirmation button icon a type * Give setBids a type * Make StripeSession type more specific * Give MenuButton buttonContent a type --- functions/src/stripe.ts | 8 +++++++- web/components/confirmation-button.tsx | 2 +- web/components/nav/menu.tsx | 2 +- web/pages/simulator.tsx | 6 ++++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts index d32010a6..ac3e2a19 100644 --- a/functions/src/stripe.ts +++ b/functions/src/stripe.ts @@ -5,7 +5,13 @@ import Stripe from 'stripe' import { getPrivateUser, getUser, isProd, payUser } from './utils' import { sendThankYouEmail } from './emails' -export type StripeSession = Stripe.Event.Data.Object & { id: any, metadata: any} +export type StripeSession = Stripe.Event.Data.Object & { + id: string + metadata: { + userId: string + manticDollarQuantity: string + } +} export type StripeTransaction = { userId: string diff --git a/web/components/confirmation-button.tsx b/web/components/confirmation-button.tsx index e07b6dab..e895467a 100644 --- a/web/components/confirmation-button.tsx +++ b/web/components/confirmation-button.tsx @@ -8,7 +8,7 @@ export function ConfirmationButton(props: { id: string openModalBtn: { label: string - icon?: any + icon?: JSX.Element className?: string } cancelBtn?: { diff --git a/web/components/nav/menu.tsx b/web/components/nav/menu.tsx index 9d348fab..24fb35a4 100644 --- a/web/components/nav/menu.tsx +++ b/web/components/nav/menu.tsx @@ -3,7 +3,7 @@ import { Menu, Transition } from '@headlessui/react' import clsx from 'clsx' export function MenuButton(props: { - buttonContent: any + buttonContent: JSX.Element menuItems: { name: string; href: string; onClick?: () => void }[] className?: string }) { diff --git a/web/pages/simulator.tsx b/web/pages/simulator.tsx index dcf44478..dc6ca873 100644 --- a/web/pages/simulator.tsx +++ b/web/pages/simulator.tsx @@ -110,11 +110,13 @@ function TableRowEnd(props: { entry: Entry | null; isNew?: boolean }) { } } +type Bid = { yesBid: number; noBid: number } + function NewBidTable(props: { steps: number - bids: Array<{ yesBid: number; noBid: number }> + bids: Array setSteps: (steps: number) => void - setBids: (bids: any[]) => void + setBids: (bids: Array) => void }) { const { steps, bids, setSteps, setBids } = props // Prepare for new bids From 749f7aad404a40553012dba8c267c06a9c530de9 Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Thu, 2 Jun 2022 18:07:47 -0700 Subject: [PATCH 2/4] copyedit: username in bet list empty state --- web/components/bets-list.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index f4e5d419..506da013 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -101,7 +101,7 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) { return } - if (bets.length === 0) return + if (bets.length === 0) return // Decending creation time. bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime) const contractBets = groupBy(bets, 'contractId') @@ -219,7 +219,7 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) { {displayedContracts.length === 0 ? ( - + ) : ( displayedContracts.map((contract) => ( { +const NoBets = ({ user }: { user: User }) => { + const me = useUser() return (
- You have not made any bets yet.{' '} - - Find a prediction market! - + {user.id === me?.id ? ( + <> + You have not made any bets yet.{' '} + + Find a prediction market! + + + ) : ( + <>{user.name} has not made any public bets yet. + )}
) } From c1bda8a775a03bc41769391f16602397777690da Mon Sep 17 00:00:00 2001 From: James Grugett Date: Thu, 2 Jun 2022 23:52:14 -0500 Subject: [PATCH 3/4] Follow other users. Filter markets by followed (#387) * Add follow button to user page * Update follows in the database using follow button. * Add toggle for followed market creators to home * Hide follow toggle from user's markets page * Check that sold bet is by auth'd user * Change follow toggle to category pill * Remove unused imports * Remove console.logs --- firestore.rules | 5 +++ web/components/contract-search.tsx | 44 +++++++++++++++++---- web/components/feed/category-selector.tsx | 9 +++++ web/components/folds/follow-fold-button.tsx | 31 ++++----------- web/components/follow-button.tsx | 37 +++++++++++++++++ web/components/user-page.tsx | 23 ++++++++++- web/hooks/use-follows.ts | 12 ++++++ web/lib/firebase/users.ts | 24 +++++++++++ 8 files changed, 153 insertions(+), 32 deletions(-) create mode 100644 web/components/follow-button.tsx create mode 100644 web/hooks/use-follows.ts diff --git a/firestore.rules b/firestore.rules index 1dc4fd37..32649752 100644 --- a/firestore.rules +++ b/firestore.rules @@ -24,6 +24,11 @@ service cloud.firestore { allow write: if request.auth.uid == userId; } + match /users/{userId}/follows/{followUserId} { + allow read; + allow write: if request.auth.uid == userId; + } + match /private-users/{userId} { allow read: if resource.data.id == request.auth.uid || isAdmin(); allow update: if (resource.data.id == request.auth.uid || isAdmin()) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 65a70d8e..6371064e 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -20,10 +20,10 @@ import { ContractsGrid } from './contract/contracts-list' import { Row } from './layout/row' import { useEffect, useRef, useState } from 'react' import { Spacer } from './layout/spacer' -import { useRouter } from 'next/router' import { ENV } from 'common/envs/constants' import { CategorySelector } from './feed/category-selector' import { useUser } from 'web/hooks/use-user' +import { useFollows } from 'web/hooks/use-follows' const searchClient = algoliasearch( 'GJQPAYENIF', @@ -60,6 +60,8 @@ export function ContractSearch(props: { const { querySortOptions, additionalFilter, showCategorySelector } = props const user = useUser() + const follows = useFollows(user?.id) + const { initialSort } = useInitialQueryAndSort(querySortOptions) const sort = sortIndexes @@ -73,15 +75,32 @@ export function ContractSearch(props: { ) const [category, setCategory] = useState('all') + const showFollows = category === 'following' + const followsKey = + showFollows && follows?.length ? `${follows.join(',')}` : '' if (!sort) return <> + + const indexName = `${indexPrefix}contracts-${sort}` + return ( )} - + + + {showFollows && (follows ?? []).length === 0 ? ( + <>You're not following anyone yet. + ) : ( + + )} ) } diff --git a/web/components/feed/category-selector.tsx b/web/components/feed/category-selector.tsx index 7c014a0e..9ae5fd93 100644 --- a/web/components/feed/category-selector.tsx +++ b/web/components/feed/category-selector.tsx @@ -27,6 +27,15 @@ export function CategorySelector(props: { }} /> + { + setCategory('following') + }} + /> + {CATEGORY_LIST.map((cat) => ( - Follow - - ) - - if (following) { - return ( - - ) - } - return ( - + ) } diff --git a/web/components/follow-button.tsx b/web/components/follow-button.tsx new file mode 100644 index 00000000..f7acf7a9 --- /dev/null +++ b/web/components/follow-button.tsx @@ -0,0 +1,37 @@ +import clsx from 'clsx' +import { useUser } from 'web/hooks/use-user' + +export function FollowButton(props: { + isFollowing: boolean | undefined + onFollow: () => void + onUnfollow: () => void + className?: string +}) { + const { isFollowing, onFollow, onUnfollow, className } = props + + const user = useUser() + + if (!user || isFollowing === undefined) + return ( + + ) + + if (isFollowing) { + return ( + + ) + } + + return ( + + ) +} diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index c1099ce8..0c704056 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { User } from 'web/lib/firebase/users' +import { follow, unfollow, User } from 'web/lib/firebase/users' import { CreatorContractsList } from './contract/contracts-list' import { SEO } from './SEO' import { Page } from './page' @@ -89,6 +89,18 @@ export function UserPage(props: { }) }, [usersComments]) + const yourFollows = useFollows(currentUser?.id) + const isFollowing = yourFollows?.includes(user.id) + + const onFollow = () => { + if (!currentUser) return + follow(currentUser.id, user.id) + } + const onUnfollow = () => { + if (!currentUser) return + unfollow(currentUser.id, user.id) + } + return ( + {!isCurrentUser && ( + + )} {isCurrentUser && ( {' '} @@ -281,6 +300,8 @@ export function defaultBannerUrl(userId: string) { } import { ExclamationIcon } from '@heroicons/react/solid' +import { FollowButton } from './follow-button' +import { useFollows } from 'web/hooks/use-follows' function AlertBox(props: { title: string; text: string }) { const { title, text } = props diff --git a/web/hooks/use-follows.ts b/web/hooks/use-follows.ts new file mode 100644 index 00000000..a8a775d8 --- /dev/null +++ b/web/hooks/use-follows.ts @@ -0,0 +1,12 @@ +import { useEffect, useState } from 'react' +import { listenForFollows } from 'web/lib/firebase/users' + +export const useFollows = (userId: string | undefined) => { + const [followIds, setFollowIds] = useState() + + useEffect(() => { + if (userId) return listenForFollows(userId, setFollowIds) + }, [userId]) + + return followIds +} diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 1e316744..61b4fbb3 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -10,6 +10,7 @@ import { getDocs, orderBy, updateDoc, + deleteDoc, } from 'firebase/firestore' import { getAuth } from 'firebase/auth' import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage' @@ -239,3 +240,26 @@ export async function getCategoryFeeds(userId: string) { const feeds = feedData.map((data) => data?.feed ?? []) return Object.fromEntries(zip(CATEGORY_LIST, feeds) as [string, feed][]) } + +export async function follow(userId: string, followedUserId: string) { + const followDoc = doc(db, 'users', userId, 'follows', followedUserId) + await setDoc(followDoc, { + userId: followedUserId, + timestamp: Date.now(), + }) +} + +export async function unfollow(userId: string, unfollowedUserId: string) { + const followDoc = doc(db, 'users', userId, 'follows', unfollowedUserId) + await deleteDoc(followDoc) +} + +export function listenForFollows( + userId: string, + setFollowIds: (followIds: string[]) => void +) { + const follows = collection(db, 'users', userId, 'follows') + return listenForValues<{ userId: string }>(follows, (docs) => + setFollowIds(docs.map(({ userId }) => userId)) + ) +} From 49859e6f0029f0a16c1e53186a047728a6a53bb6 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Fri, 3 Jun 2022 00:50:24 -0700 Subject: [PATCH 4/4] Call cloud functions directly from web client instead of proxy (#405) --- web/lib/firebase/api-call.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index 3fa93004..08f26ff4 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -1,4 +1,5 @@ import { auth } from './users' +import { FIREBASE_CONFIG } from 'common/envs/constants' export class APIError extends Error { code: number @@ -32,10 +33,20 @@ export async function call(url: string, method: string, params: any) { }) } +// Our users access the API through the Vercel proxy routes at /api/v0/blah, +// but right now at least until we get performance under control let's have the +// app just hit the cloud functions directly -- there's no difference and it's +// one less hop + +function getFunctionUrl(name: string) { + const { projectId, region } = FIREBASE_CONFIG + return `https://${region}-${projectId}.cloudfunctions.net/${name}` +} + export function createContract(params: any) { - return call('/api/v0/market', 'POST', params) + return call(getFunctionUrl('createContract'), 'POST', params) } export function placeBet(params: any) { - return call('/api/v0/bets', 'POST', params) + return call(getFunctionUrl('placeBet'), 'POST', params) }