fix: feed card follow ui
This commit is contained in:
@@ -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" />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user