fix: hide blocked user content

This commit is contained in:
gosangam
2026-06-13 00:47:11 +05:30
parent 25a729959f
commit 6a0559e608
4 changed files with 113 additions and 11 deletions
+45 -6
View File
@@ -42,6 +42,10 @@ interface UserResult {
interface UserSearchResponse {
query: string;
results: UserResult[];
total: number;
page: number;
limit: number;
total_pages: number;
}
// ---------------------------------------------------------------------------
@@ -307,7 +311,11 @@ function SearchInner() {
// User-search branch — strip the @ prefix server-side too.
// Clear content data so we don't flash old content results.
setData(null);
const userParams = new URLSearchParams({ q: userQuery, limit: "20" });
const userParams = new URLSearchParams({
q: userQuery,
page: String(currentPage),
limit: "20",
});
fetch(`/api/search/users?${userParams.toString()}`, {
signal: controller.signal,
})
@@ -319,7 +327,14 @@ function SearchInner() {
.catch((err) => {
if (err.name !== "AbortError") {
setIsLoading(false);
setUserData({ query: userQuery, results: [] });
setUserData({
query: userQuery,
results: [],
total: 0,
page: 1,
limit: 20,
total_pages: 0,
});
}
});
return () => controller.abort();
@@ -540,16 +555,40 @@ function SearchInner() {
userData &&
userData.results.length > 0 && (
<>
<p className="mb-4 text-sm text-muted-foreground">
{userData.results.length}{" "}
{userData.results.length === 1 ? "user" : "users"} matching
&ldquo;@{userQuery}&rdquo;
{/* Match count + top pagination */}
<div className="mb-4 flex items-center justify-between flex-wrap gap-3">
<p className="text-sm text-muted-foreground">
{userData.total.toLocaleString()}{" "}
{userData.total === 1 ? "user" : "users"} matching &ldquo;@
{userQuery}&rdquo;
</p>
{userData.total_pages > 1 && (
<PaginationControls
page={userData.page}
totalPages={userData.total_pages}
onPageChange={handlePageChange}
compact
/>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{userData.results.map((u) => (
<UserResultCard key={u.handle} user={u} />
))}
</div>
{/* Bottom pagination */}
{userData.total_pages > 1 && (
<div className="mt-8">
<PaginationControls
page={userData.page}
totalPages={userData.total_pages}
total={userData.total}
onPageChange={handlePageChange}
/>
</div>
)}
</>
)}
+37
View File
@@ -98,6 +98,43 @@ export default function PublicProfileLayout(props: {
);
}
// A block in either direction hides this profile's content. The backend
// already refuses the feed/mentions/followers endpoints with 403, so this
// is the matching client state: keep the header (its action row carries the
// Unblock button when *you* blocked them) but replace the tabs + feed with
// a notice instead of a misleading empty grid.
const rel = data.relationship;
const youBlocked = !!rel?.blocked;
const theyBlocked = !!rel?.blocked_by;
const blocked = youBlocked || theyBlocked;
if (blocked) {
return (
<main className="flex-1 max-w-3xl w-full mx-auto px-4 sm:px-6">
<ProfileHeader
key={data.handle}
avatarURL={data.avatar_url}
name={data.name}
handle={data.handle}
followerCount={data.follower_count}
followingCount={data.following_count}
relationship={data.relationship}
enableActions
interactiveCounts={false}
joinedAt={data.joined_at}
country={data.country}
/>
<div className="py-12 text-center">
<p className="text-sm text-muted-foreground">
{youBlocked
? `You've blocked @${data.handle}. Unblock them to see their posts.`
: `@${data.handle}'s posts aren't available.`}
</p>
</div>
</main>
);
}
const tabs: ProfileTab[] = [
{
href: `/u/${data.handle}`,
+14 -1
View File
@@ -15,14 +15,26 @@ const BFF_BASE = process.env.BFF_URL || "http://localhost:8080";
type EmptyShape = {
query: string;
results: Array<{ handle: string; name: string; avatar_url: string | null }>;
total: number;
page: number;
limit: number;
total_pages: number;
};
const empty = (q: string): EmptyShape => ({ query: q, results: [] });
const empty = (q: string): EmptyShape => ({
query: q,
results: [],
total: 0,
page: 1,
limit: 20,
total_pages: 0,
});
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const q = (searchParams.get("q") || "").trim();
const limit = searchParams.get("limit") || "20";
const page = searchParams.get("page") || "1";
if (!q) {
return NextResponse.json(empty(""));
@@ -32,6 +44,7 @@ export async function GET(request: NextRequest) {
const url = new URL("/api/v1/users/search", BFF_BASE);
url.searchParams.set("q", q);
url.searchParams.set("limit", limit);
url.searchParams.set("page", page);
const res = await fetch(url.toString(), {
headers: { Accept: "application/json" },
+16 -3
View File
@@ -51,6 +51,7 @@ export function ProfileHeader({
relationship,
enableActions = false,
country,
interactiveCounts = true,
}: {
avatarURL: string | null;
name: string;
@@ -75,6 +76,10 @@ export function ProfileHeader({
* this header with key={handle} so the live count + relationship state
* below re-seeds cleanly when navigating between profiles. */
enableActions?: boolean;
/** When false, the follower/following stats render as plain text (no
* click target) and the follow-list drawer isn't mounted — used on a
* blocked profile, where the lists endpoint is gated server-side. */
interactiveCounts?: boolean;
}) {
// Owner affordances ride on `editHref`; the follow-action row rides on
// `enableActions`. Mutually exclusive by construction.
@@ -128,12 +133,20 @@ export function ProfileHeader({
<Stat
n={followers}
label="followers"
onClick={() => setFollowList("followers")}
onClick={
interactiveCounts
? () => setFollowList("followers")
: undefined
}
/>
<Stat
n={following}
label="following"
onClick={() => setFollowList("following")}
onClick={
interactiveCounts
? () => setFollowList("following")
: undefined
}
/>
</>
) : null}
@@ -181,7 +194,7 @@ export function ProfileHeader({
onClose={() => setZoomOpen(false)}
/>
) : null}
{showCounts ? (
{showCounts && interactiveCounts ? (
<FollowListDrawer
open={followList !== null}
mode={followList ?? "followers"}