feat: share post to other apps
This commit is contained in:
@@ -3,13 +3,19 @@
|
||||
/**
|
||||
* PostActionsFooter — the icon-button row at the bottom of a post card.
|
||||
*
|
||||
* Left cluster: Like · Comment · Reshare
|
||||
* Right cluster: Save to Collection (folder-plus)
|
||||
* Left cluster (non-owner): Like · Comment · Reshare
|
||||
* Left cluster (owner): Like · Comment · Send
|
||||
* Right cluster: Save to Collection (folder-plus)
|
||||
*
|
||||
* Like / Comment / Reshare are placeholders for now — they fire a
|
||||
* "coming soon" toast on click. Save to Collection mounts the existing
|
||||
* CollectionsDrawer in `existingPostId` mode so the post is attached
|
||||
* directly without creating a duplicate.
|
||||
* Reshare and Send are *mutually exclusive* — once the post is on your
|
||||
* account there's nothing left to reshare onto it, so the Reshare slot
|
||||
* is replaced by Send (external share via SharePostSheet). Ownership is
|
||||
* decided by `user.id === post.user_id`, which the BFF already includes
|
||||
* on every post fetch — no extra round-trip needed.
|
||||
*
|
||||
* Like / Comment / Reshare are placeholders today (toast on click).
|
||||
* Save to Collection mounts the existing CollectionsDrawer in
|
||||
* `existingPostId` mode so the post attaches without a duplicate.
|
||||
*
|
||||
* Auth gating is handled per-action via `useAuthGate` — anonymous users
|
||||
* see the in-place auth modal before the underlying action runs.
|
||||
@@ -21,29 +27,59 @@ import {
|
||||
HiOutlineChatBubbleOvalLeft,
|
||||
HiOutlineFolderPlus,
|
||||
HiOutlineHeart,
|
||||
HiOutlinePaperAirplane,
|
||||
} from "react-icons/hi2";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { posts as postsApi, SocialError, type Post } from "@/lib/social";
|
||||
import { useAuth } from "@/components/auth/auth-provider";
|
||||
import { useAuthGate } from "@/components/auth/auth-gate";
|
||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||
import { CollectionsDrawer } from "@/components/selection-toolbar/collections-drawer";
|
||||
import { SharePostSheet } from "./share-post-sheet";
|
||||
|
||||
// All four icons come from the Heroicons v2 outline family (react-icons
|
||||
// All icons come from the Heroicons v2 outline family (react-icons
|
||||
// `hi2` namespace), so they share one design language — uniform stroke
|
||||
// weight, matching corner radius, matching optical sizing. No more
|
||||
// Lucide cross-icon visual drift.
|
||||
// weight, matching corner radius, matching optical sizing.
|
||||
const ICON_CLASS = "size-[1.375rem]";
|
||||
|
||||
export function PostActionsFooter({ postID }: { postID: string }) {
|
||||
export function PostActionsFooter({ post }: { post: Post }) {
|
||||
const { user } = useAuth();
|
||||
const { requireAuth } = useAuthGate();
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
const [collectionsOpen, setCollectionsOpen] = useState(false);
|
||||
const [shareOpen, setShareOpen] = useState(false);
|
||||
// Optimistic local mirror of post.viewer_has_reshared so the swap
|
||||
// happens the instant the user taps Reshare. Reconciles to the
|
||||
// server's value via rollback if the API call fails.
|
||||
const [reshared, setReshared] = useState<boolean>(
|
||||
!!post.viewer_has_reshared,
|
||||
);
|
||||
|
||||
const isOwner = !!user && user.id === post.user_id;
|
||||
// Owners and resharers both see Send — the post is "on their account"
|
||||
// either way, so the action they need is external share, not reshare.
|
||||
const showSend = isOwner || reshared;
|
||||
|
||||
async function withAuth(fn: () => void | Promise<void>) {
|
||||
if (!(await requireAuth())) return;
|
||||
await fn();
|
||||
}
|
||||
|
||||
async function onReshare() {
|
||||
// Optimistic flip first so the UI feels instant; revert on error.
|
||||
setReshared(true);
|
||||
try {
|
||||
await postsApi.reshare(post.id);
|
||||
toast.success("Reshared to your account", { duration: 2000 });
|
||||
} catch (err) {
|
||||
setReshared(false);
|
||||
const msg =
|
||||
err instanceof SocialError ? err.message : "Could not reshare";
|
||||
toast.error(msg, { duration: 3000 });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-2">
|
||||
@@ -66,15 +102,23 @@ export function PostActionsFooter({ postID }: { postID: string }) {
|
||||
}
|
||||
icon={<HiOutlineChatBubbleOvalLeft className={ICON_CLASS} />}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Reshare"
|
||||
onClick={() =>
|
||||
withAuth(() => {
|
||||
toast("Resharing is coming soon", { duration: 2000 });
|
||||
})
|
||||
}
|
||||
icon={<HiOutlineArrowPath className={ICON_CLASS} />}
|
||||
/>
|
||||
{/* Reshare for posts not yet on your account, Send for posts
|
||||
that are — authored OR previously reshared. Mutually
|
||||
exclusive: the row stays three icons regardless of viewer
|
||||
state. */}
|
||||
{showSend ? (
|
||||
<ActionButton
|
||||
label="Send"
|
||||
onClick={() => setShareOpen(true)}
|
||||
icon={<HiOutlinePaperAirplane className={ICON_CLASS} />}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
label="Reshare"
|
||||
onClick={() => withAuth(onReshare)}
|
||||
icon={<HiOutlineArrowPath className={ICON_CLASS} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ActionButton
|
||||
label="Save to collection"
|
||||
@@ -87,8 +131,17 @@ export function PostActionsFooter({ postID }: { postID: string }) {
|
||||
open={collectionsOpen}
|
||||
onClose={() => setCollectionsOpen(false)}
|
||||
isMobile={isMobile}
|
||||
existingPostId={postID}
|
||||
existingPostId={post.id}
|
||||
/>
|
||||
|
||||
{showSend ? (
|
||||
<SharePostSheet
|
||||
open={shareOpen}
|
||||
onClose={() => setShareOpen(false)}
|
||||
post={post}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user