fix: feed card follow ui

This commit is contained in:
gosangam
2026-06-18 00:21:06 +05:30
parent 071eb4d58a
commit 84cb273941
4 changed files with 59 additions and 19 deletions
+12 -4
View File
@@ -18,22 +18,30 @@ import { useAuthGate } from "@/components/auth/auth-gate";
export function FeedFollowButton({ export function FeedFollowButton({
handle, handle,
followsYou, followsYou,
requested = false,
}: { }: {
handle: string; handle: string;
/** The author already follows the viewer → label becomes "Follow back". */ /** The author already follows the viewer → label becomes "Follow back". */
followsYou: boolean; followsYou: boolean;
/** Server-truth: the viewer already has a pending follow request to this
* author (e.g. sent in a prior session or on the profile page). */
requested?: boolean;
}) { }) {
const { requireAuth } = useAuthGate(); const { requireAuth } = useAuthGate();
const [requested, setRequested] = useState(false); const [justRequested, setJustRequested] = useState(false);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
// Show "Requested" when the server already knows about a pending request or
// the viewer just sent one in this session.
const showRequested = requested || justRequested;
async function follow() { async function follow() {
if (busy || requested) return; if (busy || showRequested) return;
if (!(await requireAuth())) return; if (!(await requireAuth())) return;
setBusy(true); setBusy(true);
try { try {
await relationships.follow(handle); await relationships.follow(handle);
setRequested(true); setJustRequested(true);
} catch (err) { } catch (err) {
toast.error( toast.error(
err instanceof SocialError ? err.message : "Could not follow", err instanceof SocialError ? err.message : "Could not follow",
@@ -44,7 +52,7 @@ export function FeedFollowButton({
} }
} }
if (requested) { if (showRequested) {
return ( return (
<span className="shrink-0 inline-flex items-center gap-1 rounded-full border border-border px-3 h-8 text-xs font-medium text-muted-foreground"> <span className="shrink-0 inline-flex items-center gap-1 rounded-full border border-border px-3 h-8 text-xs font-medium text-muted-foreground">
<Clock className="size-3.5" /> <Clock className="size-3.5" />
+14
View File
@@ -71,11 +71,25 @@ export function PostCard({
show (delete for owners, block/report for everyone else), so show (delete for owners, block/report for everyone else), so
a view with nothing to expose simply hides the dot. */} a view with nothing to expose simply hides the dot. */}
<header className="flex items-center gap-3 mb-4"> <header className="flex items-center gap-3 mb-4">
{authorHandle ? (
<Link
href={`/u/${authorHandle}`}
aria-label={`View ${authorName}'s profile`}
className="shrink-0"
>
<UserAvatar <UserAvatar
src={post.author_avatar_url} src={post.author_avatar_url}
name={authorName} name={authorName}
className="size-9" className="size-9"
/> />
</Link>
) : (
<UserAvatar
src={post.author_avatar_url}
name={authorName}
className="size-9"
/>
)}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{authorHandle ? ( {authorHandle ? (
<Link <Link
+18 -3
View File
@@ -33,6 +33,7 @@ import { AtSign, ImageIcon, Repeat2 } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Post, PostImage } from "@/lib/social"; import type { Post, PostImage } from "@/lib/social";
import { displaySourceLabel } from "@/lib/source-reference"; import { displaySourceLabel } from "@/lib/source-reference";
import { timeAgo } from "@/lib/time";
import { useMediaQuery } from "@/components/selection-toolbar/shared"; import { useMediaQuery } from "@/components/selection-toolbar/shared";
import { UserAvatar } from "@/components/ui/user-avatar"; import { UserAvatar } from "@/components/ui/user-avatar";
import { useAuth } from "@/components/auth/auth-provider"; import { useAuth } from "@/components/auth/auth-provider";
@@ -125,11 +126,25 @@ export function PostScrollCard({
on the right (Delete for author, Block/Report for everyone on the right (Delete for author, Block/Report for everyone
else; PostCardMenu hides itself when there are no items). */} else; PostCardMenu hides itself when there are no items). */}
<header className="flex items-center gap-3 px-4 py-3"> <header className="flex items-center gap-3 px-4 py-3">
{post.author_handle ? (
<Link
href={`/u/${post.author_handle}`}
aria-label={`View ${post.author_name || post.author_handle}'s profile`}
className="shrink-0"
>
<UserAvatar <UserAvatar
src={post.author_avatar_url} src={post.author_avatar_url}
name={post.author_name ?? "Unknown"} name={post.author_name ?? "Unknown"}
className="size-9" className="size-9"
/> />
</Link>
) : (
<UserAvatar
src={post.author_avatar_url}
name={post.author_name ?? "Unknown"}
className="size-9"
/>
)}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<Link <Link
href={ href={
@@ -139,16 +154,16 @@ export function PostScrollCard({
> >
{post.author_name || "Unknown"} {post.author_name || "Unknown"}
</Link> </Link>
{post.author_handle ? (
<span className="block text-xs text-muted-foreground truncate"> <span className="block text-xs text-muted-foreground truncate">
@{post.author_handle} {post.author_handle ? <>@{post.author_handle} · </> : null}
{timeAgo(post.created_at)}
</span> </span>
) : null}
</div> </div>
{enableFollow && post.author_handle && !post.viewer_follows_author ? ( {enableFollow && post.author_handle && !post.viewer_follows_author ? (
<FeedFollowButton <FeedFollowButton
handle={post.author_handle} handle={post.author_handle}
followsYou={!!post.author_follows_viewer} followsYou={!!post.author_follows_viewer}
requested={!!post.viewer_follow_requested}
/> />
) : null} ) : null}
<PostCardMenu <PostCardMenu
+3
View File
@@ -94,6 +94,9 @@ export type Post = {
viewer_follows_author?: boolean; viewer_follows_author?: boolean;
/** Home-feed only: does the author follow the viewer (drives "Follow back")? */ /** Home-feed only: does the author follow the viewer (drives "Follow back")? */
author_follows_viewer?: boolean; author_follows_viewer?: boolean;
/** Home-feed only: viewer has a pending (unaccepted) follow request to the
* author — drives the "Requested" state on the feed follow button. */
viewer_follow_requested?: boolean;
/** Owner-only: post is hidden from timelines + public collections. Still /** Owner-only: post is hidden from timelines + public collections. Still
* shown to the owner on their own profile and in private collections. */ * shown to the owner on their own profile and in private collections. */
is_hidden?: boolean; is_hidden?: boolean;