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)) + ) +}