Skip to content

Show Users Their Own Follower Counts #1912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions components/EditProfilePage/EditProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ import { TestimoniesTab } from "./TestimoniesTab"
import { useFlags } from "components/featureFlags"
import LoginPage from "components/Login/Login"
import { PendingUpgradeBanner } from "components/PendingUpgradeBanner"
import { FollowersTab } from "./FollowersTab"

const tabTitle = ["about-you", "testimonies", "following"] as const
const tabTitle = ["about-you", "testimonies", "following", "followers"] as const
export type TabTitles = (typeof tabTitle)[number]

export default function EditProfile({
Expand Down Expand Up @@ -115,6 +116,7 @@ export function EditProfileForm({
isOrg = isOrg || isPendingUpgrade

const { t } = useTranslation("editProfile")
const [followerCount, setFollowerCount] = useState<number | null>(null)

const tabs = [
{
Expand Down Expand Up @@ -147,6 +149,18 @@ export function EditProfileForm({
title: t("tabs.following"),
eventKey: "following",
content: <FollowingTab className="mt-3 mb-4" />
},
{
title: followerCount
? t("tabs.followersWithCount", { count: followerCount })
: t("tabs.followers"),
eventKey: "followers",
content: (
<FollowersTab
className="mt-3 mb-4"
setFollowerCount={setFollowerCount}
/>
)
}
]

Expand Down Expand Up @@ -174,9 +188,7 @@ export function EditProfileForm({
>
<TabNavWrapper>
{tabs.map((t, i) => (
<>
<TabNavItem tab={t} i={i} />
</>
<TabNavItem key={i} tab={t} i={i} />
))}
</TabNavWrapper>
<StyledTabContent>
Expand Down
109 changes: 109 additions & 0 deletions components/EditProfilePage/FollowersTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { functions } from "components/firebase"
import { httpsCallable } from "firebase/functions"
import { useTranslation } from "next-i18next"
import { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react"
import { useAuth } from "../auth"
import { Profile, usePublicProfile } from "components/db"
import { Internal } from "components/links"
import { FollowUserButton } from "components/shared/FollowButton"
import React from "react"
import { Col, Row, Spinner, Stack, Alert } from "../bootstrap"
import { TitledSectionCard } from "../shared"
import { OrgIconSmall } from "./StyledEditProfileComponents"

type ProfileWithId = Profile & { profileId: string }

export const FollowersTab = ({
className,
setFollowerCount
}: {
className?: string
setFollowerCount: Dispatch<SetStateAction<number | null>>
}) => {
const uid = useAuth().user?.uid
const [followers, setFollowers] = useState<ProfileWithId[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { t } = useTranslation("editProfile")

useEffect(() => {
const fetchFollowers = async () => {
try {
const { data: followerIds } = await httpsCallable<
void,
ProfileWithId[]
>(functions, "getFollowers")()
setFollowers(followerIds)
setFollowerCount(followerIds.length)
setLoading(false)
} catch (err) {
console.error("Error fetching followerIds", err)
setError("Error fetching followers.")
setLoading(false)
return
}
}
if (uid) fetchFollowers()
}, [uid])
return (
<TitledSectionCard className={className}>
<div className="mx-4 mt-3 d-flex flex-column gap-3">
<Stack>
<h2>{t("follow.your_followers")}</h2>
<p className="mt-0 text-muted">
{t("follow.follower_info_disclaimer")}
</p>
<div className="mt-3">
{error ? (
<Alert variant="danger">{error}</Alert>
) : loading ? (
<Spinner animation="border" className="mx-auto" />
) : (
followers.map((profile, i) => (
<FollowerCard key={i} {...profile} />
))
)}
</div>
</Stack>
</div>
</TitledSectionCard>
)
}

const FollowerCard = ({
profileId,
fullName,
profileImage,
public: isPublic
}: ProfileWithId) => {
const { t } = useTranslation("profile")
const displayName = isPublic && fullName ? fullName : t("anonymousUser")
return (
<div className={`fs-3 lh-lg`}>
<Row className="align-items-center justify-content-between g-0 w-100">
<Col className="d-flex align-items-center flex-grow-1 p-0 text-start">
<OrgIconSmall
className="mr-4 mt-0 mb-0 ms-0"
profileImage={profileImage}
/>
{isPublic ? (
<Internal href={`/profile?id=${profileId}`}>{displayName}</Internal>
) : (
<span>{displayName}</span>
)}
</Col>
{isPublic ? (
<Col
xs="auto"
className="d-flex justify-content-end ms-auto text-end p-0"
>
<FollowUserButton profileId={profileId} />
</Col>
) : (
<></>
)}
</Row>
<hr className={`mt-3`} />
</div>
)
}
11 changes: 5 additions & 6 deletions components/EditProfilePage/StyledEditProfileComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const StyledTabNav = styled(Nav).attrs(props => ({
export const TabNavWrapper = ({ children, className, ...props }: NavProps) => {
return (
<Nav
className={`d-flex mb-3 text-center h3 color-dark ${className}`}
className={`d-flex w-100 flex-column flex-lg-row flex-lg-nowrap mb-3 text-center h3 color-dark ${className}`}
{...props}
>
{children}
Expand All @@ -68,12 +68,11 @@ export const TabNavItem = ({
className?: string
}) => {
return (
<Nav.Item
className={`flex-grow-1 col-12 col-md-auto ${className}`}
key={tab.eventKey}
>
<Nav.Item className={`flex-lg-fill ${className}`} key={tab.eventKey}>
<TabNavLink eventKey={tab.eventKey} className={`rounded-top m-0 p-0`}>
<p className={`my-0 ${i === 0 ? "" : "mx-4"}`}>{tab.title}</p>
<p className={`my-0 text-nowrap ${i === 0 ? "" : "mx-4"}`}>
{tab.title}
</p>
<hr className={`my-0`} />
</TabNavLink>
</Nav.Item>
Expand Down
3 changes: 2 additions & 1 deletion functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export {
followBill,
unfollowBill,
followUser,
unfollowUser
unfollowUser,
getFollowers
} from "./subscriptions"

export { transcription } from "./webhooks"
Expand Down
46 changes: 46 additions & 0 deletions functions/src/subscriptions/getFollowers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FieldPath, getFirestore } from "firebase-admin/firestore"
import * as functions from "firebase-functions"
import { checkAuth } from "../common"

export const getFollowers = functions.https.onCall(async (_, context) => {
const uid = checkAuth(context, false)

functions.logger.log(`[getFollowers] Finding followers for user UID: ${uid}`)

const firestore = getFirestore()

return await firestore
.collectionGroup("activeTopicSubscriptions")
.where("userLookup.profileId", "==", uid)
.get()
.then(snapshot => {
const followerProfileIds = Array.from(
// use a set to deduplicate
new Set(
snapshot.docs
.map(doc => doc.ref.parent.parent!.id)
.filter(id => id != uid)
)
)

functions.logger.log(
`[getFollowers] Found ${followerProfileIds.length} followers for user UID: ${uid}`
)

return firestore
.collection("profiles")
.where(FieldPath.documentId(), "in", followerProfileIds)
.get()
.then(snapshot =>
snapshot.docs.map(doc => ({ profileId: doc.id, ...doc.data() }))
)
})
.catch(error => {
functions.logger.error("[getFollowers] Caught error:", error)
throw new functions.https.HttpsError(
"internal",
"Failed to retrieve followers.",
error
)
})
})
4 changes: 2 additions & 2 deletions functions/src/subscriptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { followBill } from "./followBill"
import { unfollowBill } from "./unfollowBill"
import { followUser } from "./followUser"
import { unfollowUser } from "./unfollowUser"

import { getFollowers } from "./getFollowers"
// export the functions
export { followBill, unfollowBill, followUser, unfollowUser }
export { followBill, unfollowBill, followUser, unfollowUser, getFollowers }
2 changes: 2 additions & 0 deletions pages/edit-profile/[[...docName]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const getStaticPaths: GetStaticPaths = async ctx => {
{ params: { docName: ["about-you"] } },
{ params: { docName: ["testimonies"] } },
{ params: { docName: ["following"] } },
{ params: { docName: ["followers"] } },
{ params: { docName: [] } }
],
fallback: false
Expand All @@ -36,6 +37,7 @@ export const getStaticProps = createGetStaticTranslationProps([
"auth",
"common",
"editProfile",
"profile",
"footer",
"testimony"
])
13 changes: 9 additions & 4 deletions public/locales/en/editProfile.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"tabs": {
"personalInfo": "Personal Information",
"testimonies": "Testimonies",
"following": "Following"
"following": "Following",
"followers": "Followers",
"followersWithCount": "Followers ({{count}})"
},
"content": {
"reqPending": "Your request to be an organization account is pending approval",
Expand All @@ -25,7 +27,10 @@
"follow": {
"bills": "Bills I Follow",
"orgs": "Users I Follow",
"unfollow": "Unfollow"
"your_followers": "Your Followers",
"unfollow": "Unfollow",
"follow": "Follow",
"follower_info_disclaimer": "Names and follower counts are not publicly available; only visible to you."
},
"confirmation": {
"unfollowMessage": "Are you sure you want to unfollow",
Expand All @@ -38,7 +43,7 @@
"weekly": "Weekly",
"monthly": "Monthly"
},
"emailIconAlt":"open envelope with letter, toggles update frequency options",
"emailIconAlt": "open envelope with letter, toggles update frequency options",
"legislator": {
"representative": "Representative",
"searchRepresentative": "Search your representative",
Expand Down Expand Up @@ -100,4 +105,4 @@
"admin": "Your profile is currently public. Others can view your profile page. Admin accounts cannot set their profile to private.",
"default": "The publicity of your profile is unknown. Please contact the admin for more information."
}
}
}