fix: feed card follow ui
This commit is contained in:
@@ -18,22 +18,30 @@ import { useAuthGate } from "@/components/auth/auth-gate";
|
||||
export function FeedFollowButton({
|
||||
handle,
|
||||
followsYou,
|
||||
requested = false,
|
||||
}: {
|
||||
handle: string;
|
||||
/** The author already follows the viewer → label becomes "Follow back". */
|
||||
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 [requested, setRequested] = useState(false);
|
||||
const [justRequested, setJustRequested] = 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() {
|
||||
if (busy || requested) return;
|
||||
if (busy || showRequested) return;
|
||||
if (!(await requireAuth())) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await relationships.follow(handle);
|
||||
setRequested(true);
|
||||
setJustRequested(true);
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
err instanceof SocialError ? err.message : "Could not follow",
|
||||
@@ -44,7 +52,7 @@ export function FeedFollowButton({
|
||||
}
|
||||
}
|
||||
|
||||
if (requested) {
|
||||
if (showRequested) {
|
||||
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">
|
||||
<Clock className="size-3.5" />
|
||||
|
||||
@@ -71,11 +71,25 @@ export function PostCard({
|
||||
show (delete for owners, block/report for everyone else), so
|
||||
a view with nothing to expose simply hides the dot. */}
|
||||
<header className="flex items-center gap-3 mb-4">
|
||||
<UserAvatar
|
||||
src={post.author_avatar_url}
|
||||
name={authorName}
|
||||
className="size-9"
|
||||
/>
|
||||
{authorHandle ? (
|
||||
<Link
|
||||
href={`/u/${authorHandle}`}
|
||||
aria-label={`View ${authorName}'s profile`}
|
||||
className="shrink-0"
|
||||
>
|
||||
<UserAvatar
|
||||
src={post.author_avatar_url}
|
||||
name={authorName}
|
||||
className="size-9"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<UserAvatar
|
||||
src={post.author_avatar_url}
|
||||
name={authorName}
|
||||
className="size-9"
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
{authorHandle ? (
|
||||
<Link
|
||||
|
||||
@@ -33,6 +33,7 @@ import { AtSign, ImageIcon, Repeat2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Post, PostImage } from "@/lib/social";
|
||||
import { displaySourceLabel } from "@/lib/source-reference";
|
||||
import { timeAgo } from "@/lib/time";
|
||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||
import { UserAvatar } from "@/components/ui/user-avatar";
|
||||
import { useAuth } from "@/components/auth/auth-provider";
|
||||
@@ -125,11 +126,25 @@ export function PostScrollCard({
|
||||
on the right (Delete for author, Block/Report for everyone
|
||||
else; PostCardMenu hides itself when there are no items). */}
|
||||
<header className="flex items-center gap-3 px-4 py-3">
|
||||
<UserAvatar
|
||||
src={post.author_avatar_url}
|
||||
name={post.author_name ?? "Unknown"}
|
||||
className="size-9"
|
||||
/>
|
||||
{post.author_handle ? (
|
||||
<Link
|
||||
href={`/u/${post.author_handle}`}
|
||||
aria-label={`View ${post.author_name || post.author_handle}'s profile`}
|
||||
className="shrink-0"
|
||||
>
|
||||
<UserAvatar
|
||||
src={post.author_avatar_url}
|
||||
name={post.author_name ?? "Unknown"}
|
||||
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">
|
||||
<Link
|
||||
href={
|
||||
@@ -139,16 +154,16 @@ export function PostScrollCard({
|
||||
>
|
||||
{post.author_name || "Unknown"}
|
||||
</Link>
|
||||
{post.author_handle ? (
|
||||
<span className="block text-xs text-muted-foreground truncate">
|
||||
@{post.author_handle}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="block text-xs text-muted-foreground truncate">
|
||||
{post.author_handle ? <>@{post.author_handle} · </> : null}
|
||||
{timeAgo(post.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
{enableFollow && post.author_handle && !post.viewer_follows_author ? (
|
||||
<FeedFollowButton
|
||||
handle={post.author_handle}
|
||||
followsYou={!!post.author_follows_viewer}
|
||||
requested={!!post.viewer_follow_requested}
|
||||
/>
|
||||
) : null}
|
||||
<PostCardMenu
|
||||
|
||||
@@ -94,6 +94,9 @@ export type Post = {
|
||||
viewer_follows_author?: boolean;
|
||||
/** Home-feed only: does the author follow the viewer (drives "Follow back")? */
|
||||
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
|
||||
* shown to the owner on their own profile and in private collections. */
|
||||
is_hidden?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user