diff --git a/apps/api/lexicons/graph/getFollowers.json b/apps/api/lexicons/graph/getFollowers.json index d0d7e29b..3cbabd3b 100644 --- a/apps/api/lexicons/graph/getFollowers.json +++ b/apps/api/lexicons/graph/getFollowers.json @@ -57,6 +57,10 @@ "cursor": { "type": "string", "description": "A cursor value to pass to subsequent calls to get the next page of results." + }, + "count": { + "type": "integer", + "description": "The total number of followers." } } } diff --git a/apps/api/lexicons/graph/getFollows.json b/apps/api/lexicons/graph/getFollows.json index 7fe4cdef..83a04782 100644 --- a/apps/api/lexicons/graph/getFollows.json +++ b/apps/api/lexicons/graph/getFollows.json @@ -57,6 +57,10 @@ "cursor": { "type": "string", "description": "A cursor value to pass to subsequent calls to get the next page of results." + }, + "count": { + "type": "integer", + "description": "The total number of follows." } } } diff --git a/apps/api/pkl/defs/graph/getFollowers.pkl b/apps/api/pkl/defs/graph/getFollowers.pkl index de43f1c8..9e9b835f 100644 --- a/apps/api/pkl/defs/graph/getFollowers.pkl +++ b/apps/api/pkl/defs/graph/getFollowers.pkl @@ -56,6 +56,10 @@ defs = new Mapping { description = "A cursor value to pass to subsequent calls to get the next page of results." } + ["count"] = new IntegerType { + type = "integer" + description = "The total number of followers." + } } } } diff --git a/apps/api/pkl/defs/graph/getFollows.pkl b/apps/api/pkl/defs/graph/getFollows.pkl index b3a501e2..6bc919d8 100644 --- a/apps/api/pkl/defs/graph/getFollows.pkl +++ b/apps/api/pkl/defs/graph/getFollows.pkl @@ -56,6 +56,10 @@ defs = new Mapping { description = "A cursor value to pass to subsequent calls to get the next page of results." } + ["count"] = new IntegerType { + type = "integer" + description = "The total number of follows." + } } } } diff --git a/apps/api/src/lexicon/lexicons.ts b/apps/api/src/lexicon/lexicons.ts index aa3a54bb..3139df5b 100644 --- a/apps/api/src/lexicon/lexicons.ts +++ b/apps/api/src/lexicon/lexicons.ts @@ -2915,6 +2915,10 @@ export const schemaDict = { description: "A cursor value to pass to subsequent calls to get the next page of results.", }, + count: { + type: "integer", + description: "The total number of followers.", + }, }, }, }, @@ -2979,6 +2983,10 @@ export const schemaDict = { description: "A cursor value to pass to subsequent calls to get the next page of results.", }, + count: { + type: "integer", + description: "The total number of follows.", + }, }, }, }, diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts b/apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts index a18151da..173decdf 100644 --- a/apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts +++ b/apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts @@ -24,6 +24,8 @@ export interface OutputSchema { followers: AppRockskyActorDefs.ProfileViewBasic[]; /** A cursor value to pass to subsequent calls to get the next page of results. */ cursor?: string; + /** The total number of followers. */ + count?: number; [k: string]: unknown; } diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts b/apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts index 2dd251a2..42bcdc86 100644 --- a/apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts +++ b/apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts @@ -24,6 +24,8 @@ export interface OutputSchema { follows: AppRockskyActorDefs.ProfileViewBasic[]; /** A cursor value to pass to subsequent calls to get the next page of results. */ cursor?: string; + /** The total number of follows. */ + count?: number; [k: string]: unknown; } diff --git a/apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts b/apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts index f946f686..1f22caf7 100644 --- a/apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts +++ b/apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts @@ -1,5 +1,5 @@ import type { Context } from "context"; -import { eq, desc, and, lt, inArray } from "drizzle-orm"; +import { eq, desc, and, lt, inArray, count } from "drizzle-orm"; import { Effect, pipe } from "effect"; import type { Server } from "lexicon"; import type { QueryParams } from "lexicon/types/app/rocksky/graph/getFollowers"; @@ -20,6 +20,7 @@ export default function (server: Server, ctx: Context) { return Effect.succeed({ subject: {} satisfies ProfileViewBasic, followers: [] as ProfileViewBasic[], + count: 0, }); }), ); @@ -41,7 +42,7 @@ const retrieve = ({ params: QueryParams; ctx: Context; }): Effect.Effect< - [SelectUser | undefined, SelectUser[], string | undefined], + [SelectUser | undefined, SelectUser[], string | undefined, number], Error > => { return Effect.tryPromise({ @@ -121,17 +122,36 @@ const retrieve = ({ ? rows[rows.length - 1]?.createdAt.getTime().toString(10) : undefined, ), + ctx.db + .select({ count: count() }) + .from(tables.follows) + .where( + params.dids && params.dids?.length > 0 + ? and( + eq(tables.follows.subject_did, params.actor), + inArray(tables.follows.follower_did, params.dids), + ) + : eq(tables.follows.subject_did, params.actor), + ) + .execute() + .then((rows) => rows[0]?.count ?? 0), ]), catch: (error) => new Error(`Failed to retrieve user followers: ${error}`), }); }; -const presentation = ([user, followers, cursor]: [ +const presentation = ([user, followers, cursor, totalCount]: [ SelectUser | undefined, SelectUser[], string | undefined, + number, ]): Effect.Effect< - { subject: ProfileViewBasic; followers: ProfileViewBasic[] }, + { + subject: ProfileViewBasic; + followers: ProfileViewBasic[]; + cursor?: string; + count: number; + }, never > => { return Effect.sync(() => ({ @@ -154,5 +174,6 @@ const presentation = ([user, followers, cursor]: [ updatedAt: follower.updatedAt.toISOString(), })), cursor, + count: totalCount, })); }; diff --git a/apps/api/src/xrpc/app/rocksky/graph/getFollows.ts b/apps/api/src/xrpc/app/rocksky/graph/getFollows.ts index 43683b24..de1070cf 100644 --- a/apps/api/src/xrpc/app/rocksky/graph/getFollows.ts +++ b/apps/api/src/xrpc/app/rocksky/graph/getFollows.ts @@ -1,5 +1,5 @@ import type { Context } from "context"; -import { eq, desc, and, lt, inArray } from "drizzle-orm"; +import { eq, desc, and, lt, inArray, count } from "drizzle-orm"; import { Effect, pipe } from "effect"; import type { Server } from "lexicon"; import type { QueryParams } from "lexicon/types/app/rocksky/graph/getFollowers"; @@ -12,8 +12,8 @@ export default function (server: Server, ctx: Context) { pipe( { params, ctx }, retrieve, - Effect.flatMap(([user, follows, cursor]) => - presentation(user, follows, cursor), + Effect.flatMap(([user, follows, cursor, totalCount]) => + presentation(user, follows, cursor, totalCount), ), Effect.retry({ times: 3 }), Effect.timeout("120 seconds"), @@ -22,6 +22,7 @@ export default function (server: Server, ctx: Context) { return Effect.succeed({ subject: undefined, follows: [], + count: 0, }); }), ); @@ -43,7 +44,7 @@ const retrieve = ({ params: QueryParams; ctx: Context; }): Effect.Effect< - [SelectUser | undefined, SelectUser[], string | undefined], + [SelectUser | undefined, SelectUser[], string | undefined, number], Error > => { // Build where conditions dynamically @@ -65,6 +66,19 @@ const retrieve = ({ const whereConditions = buildWhereConditions(); + // Build where conditions for count (without cursor) + const buildCountWhereConditions = () => { + const conditions = [eq(tables.follows.follower_did, params.actor)]; + + if (params.dids && params.dids.length > 0) { + conditions.push(inArray(tables.follows.subject_did, params.dids)); + } + + return conditions.length > 1 ? and(...conditions) : conditions[0]; + }; + + const countWhereConditions = buildCountWhereConditions(); + return Effect.tryPromise({ try: () => Promise.all([ @@ -98,6 +112,12 @@ const retrieve = ({ ? rows[rows.length - 1]?.createdAt.getTime().toString(10) : undefined, ), + ctx.db + .select({ count: count() }) + .from(tables.follows) + .where(countWhereConditions) + .execute() + .then((rows) => rows[0]?.count ?? 0), ]), catch: (error) => new Error(`Failed to retrieve user follows: ${error}`), }); @@ -107,8 +127,14 @@ const presentation = ( user: SelectUser | undefined, follows: SelectUser[], cursor: string | undefined, + count: number, ): Effect.Effect< - { subject: ProfileViewBasic; follows: ProfileViewBasic[] }, + { + subject: ProfileViewBasic; + follows: ProfileViewBasic[]; + cursor?: string; + count: number; + }, never > => { return Effect.sync(() => ({ @@ -131,5 +157,6 @@ const presentation = ( updatedAt: follow.updatedAt.toISOString(), })), cursor, + count, })); }; diff --git a/apps/web/src/pages/profile/followers/Followers.tsx b/apps/web/src/pages/profile/followers/Followers.tsx index 91891c70..fc5a6739 100644 --- a/apps/web/src/pages/profile/followers/Followers.tsx +++ b/apps/web/src/pages/profile/followers/Followers.tsx @@ -15,6 +15,7 @@ import { Button } from "baseui/button"; import { IconCheck, IconPlus } from "@tabler/icons-react"; import SignInModal from "../../../components/SignInModal"; import { useState, useEffect, useRef } from "react"; +import numeral from "numeral"; function Followers() { const [, setActiveKey] = useAtom(activeTabAtom); @@ -90,10 +91,12 @@ function Followers() { unfollowAccount(followerDid); }; + const count = data?.pages?.flatMap((page) => page.count)[0]; + return ( <> - Followers + Followers {count > 0 ? `(${numeral(count).format("0,0")})` : ""} {allFollowers.length === 0 && data && ( diff --git a/apps/web/src/pages/profile/follows/Follows.tsx b/apps/web/src/pages/profile/follows/Follows.tsx index c4702397..ac8f3a74 100644 --- a/apps/web/src/pages/profile/follows/Follows.tsx +++ b/apps/web/src/pages/profile/follows/Follows.tsx @@ -15,6 +15,7 @@ import { Button } from "baseui/button"; import { IconCheck, IconPlus } from "@tabler/icons-react"; import SignInModal from "../../../components/SignInModal"; import { useState, useEffect, useRef } from "react"; +import numeral from "numeral"; function Follows() { const [, setActiveKey] = useAtom(activeTabAtom); @@ -91,10 +92,12 @@ function Follows() { unfollowAccount(followDid); }; + const count = data?.pages?.flatMap((page) => page.count)[0]; + return ( <> - Following + Following {count > 0 ? `(${numeral(count).format("0,0")})` : ""} {allFollows.length === 0 && data && (