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
This commit is contained in:
parent
749f7aad40
commit
c1bda8a775
|
@ -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())
|
||||
|
|
|
@ -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<string>('all')
|
||||
const showFollows = category === 'following'
|
||||
const followsKey =
|
||||
showFollows && follows?.length ? `${follows.join(',')}` : ''
|
||||
|
||||
if (!sort) return <></>
|
||||
|
||||
const indexName = `${indexPrefix}contracts-${sort}`
|
||||
|
||||
return (
|
||||
<InstantSearch
|
||||
searchClient={searchClient}
|
||||
indexName={`${indexPrefix}contracts-${sort}`}
|
||||
indexName={indexName}
|
||||
key={`search-${
|
||||
additionalFilter?.tag ?? additionalFilter?.creatorId ?? ''
|
||||
}`}
|
||||
}${followsKey}`}
|
||||
initialUiState={
|
||||
showFollows
|
||||
? {
|
||||
[indexName]: {
|
||||
refinementList: {
|
||||
creatorId: ['', ...(follows ?? [])],
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Row className="gap-1 sm:gap-2">
|
||||
<SearchBox
|
||||
|
@ -120,11 +139,20 @@ export function ContractSearch(props: {
|
|||
/>
|
||||
)}
|
||||
|
||||
<ContractSearchInner
|
||||
querySortOptions={querySortOptions}
|
||||
filter={filter}
|
||||
additionalFilter={{ category, ...additionalFilter }}
|
||||
/>
|
||||
<Spacer h={4} />
|
||||
|
||||
{showFollows && (follows ?? []).length === 0 ? (
|
||||
<>You're not following anyone yet.</>
|
||||
) : (
|
||||
<ContractSearchInner
|
||||
querySortOptions={querySortOptions}
|
||||
filter={filter}
|
||||
additionalFilter={{
|
||||
category: category === 'following' ? 'all' : category,
|
||||
...additionalFilter,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</InstantSearch>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,15 @@ export function CategorySelector(props: {
|
|||
}}
|
||||
/>
|
||||
|
||||
<CategoryButton
|
||||
key="following"
|
||||
category="Following"
|
||||
isFollowed={category === 'following'}
|
||||
toggle={() => {
|
||||
setCategory('following')
|
||||
}}
|
||||
/>
|
||||
|
||||
{CATEGORY_LIST.map((cat) => (
|
||||
<CategoryButton
|
||||
key={cat}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx'
|
||||
import { Fold } from 'common/fold'
|
||||
import { useFollowedFoldIds } from 'web/hooks/use-fold'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { followFold, unfollowFold } from 'web/lib/firebase/folds'
|
||||
import { FollowButton } from '../follow-button'
|
||||
|
||||
export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
||||
const { fold, className } = props
|
||||
|
@ -10,7 +10,7 @@ export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
|||
const user = useUser()
|
||||
|
||||
const followedFoldIds = useFollowedFoldIds(user)
|
||||
const following = followedFoldIds
|
||||
const isFollowing = followedFoldIds
|
||||
? followedFoldIds.includes(fold.id)
|
||||
: undefined
|
||||
|
||||
|
@ -22,27 +22,12 @@ export function FollowFoldButton(props: { fold: Fold; className?: string }) {
|
|||
if (user) unfollowFold(fold, user)
|
||||
}
|
||||
|
||||
if (!user || following === undefined)
|
||||
return (
|
||||
<button className={clsx('btn btn-sm invisible', className)}>
|
||||
Follow
|
||||
</button>
|
||||
)
|
||||
|
||||
if (following) {
|
||||
return (
|
||||
<button
|
||||
className={clsx('btn btn-outline btn-sm', className)}
|
||||
onClick={onUnfollow}
|
||||
>
|
||||
Following
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={clsx('btn btn-sm', className)} onClick={onFollow}>
|
||||
Follow
|
||||
</button>
|
||||
<FollowButton
|
||||
isFollowing={isFollowing}
|
||||
onFollow={onFollow}
|
||||
onUnfollow={onUnfollow}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
37
web/components/follow-button.tsx
Normal file
37
web/components/follow-button.tsx
Normal file
|
@ -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 (
|
||||
<button className={clsx('btn btn-sm invisible', className)}>
|
||||
Follow
|
||||
</button>
|
||||
)
|
||||
|
||||
if (isFollowing) {
|
||||
return (
|
||||
<button
|
||||
className={clsx('btn btn-outline btn-sm', className)}
|
||||
onClick={onUnfollow}
|
||||
>
|
||||
Following
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={clsx('btn btn-sm', className)} onClick={onFollow}>
|
||||
Follow
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
<Page>
|
||||
<SEO
|
||||
|
@ -116,6 +128,13 @@ export function UserPage(props: {
|
|||
|
||||
{/* Top right buttons (e.g. edit, follow) */}
|
||||
<div className="absolute right-0 top-0 mt-4 mr-4">
|
||||
{!isCurrentUser && (
|
||||
<FollowButton
|
||||
isFollowing={isFollowing}
|
||||
onFollow={onFollow}
|
||||
onUnfollow={onUnfollow}
|
||||
/>
|
||||
)}
|
||||
{isCurrentUser && (
|
||||
<SiteLink className="btn" href="/profile">
|
||||
<PencilIcon className="h-5 w-5" />{' '}
|
||||
|
@ -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
|
||||
|
|
12
web/hooks/use-follows.ts
Normal file
12
web/hooks/use-follows.ts
Normal file
|
@ -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<string[] | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) return listenForFollows(userId, setFollowIds)
|
||||
}, [userId])
|
||||
|
||||
return followIds
|
||||
}
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user