From 3e6a1c6f3973dc9e7435e6817b4cd248ecdff341 Mon Sep 17 00:00:00 2001 From: gosangam Date: Sat, 29 Aug 2026 16:26:53 +0530 Subject: [PATCH] fix: chat accepts posts --- components/chat/message-attachments.tsx | 4 + components/chat/share-to-chat-dialog.tsx | 6 +- components/chat/shared-post-card.tsx | 75 +++++++++++++++++++ .../social/post/post-actions-footer.tsx | 32 +++----- lib/chat.ts | 22 ++++++ 5 files changed, 118 insertions(+), 21 deletions(-) create mode 100644 components/chat/shared-post-card.tsx diff --git a/components/chat/message-attachments.tsx b/components/chat/message-attachments.tsx index c61be257..0ae6a2ce 100644 --- a/components/chat/message-attachments.tsx +++ b/components/chat/message-attachments.tsx @@ -18,6 +18,7 @@ import { LinkPreviewCard } from "@/components/chat/link-preview-card"; import { MediaGrid } from "@/components/chat/media-grid"; import { MediaLightbox } from "@/components/chat/media-lightbox"; import { QuoteCard } from "@/components/chat/quote-card"; +import { SharedPostCard } from "@/components/chat/shared-post-card"; import { attachmentUrl, type Attachment } from "@/lib/chat"; function humanSize(bytes?: number | null): string { @@ -58,6 +59,9 @@ export function MessageAttachments({ {rest.map((a) => { switch (a.kind) { + case "entity": + return ; + case "quote": return ; diff --git a/components/chat/share-to-chat-dialog.tsx b/components/chat/share-to-chat-dialog.tsx index b3485113..22c2e2e1 100644 --- a/components/chat/share-to-chat-dialog.tsx +++ b/components/chat/share-to-chat-dialog.tsx @@ -37,12 +37,16 @@ type SendState = "idle" | "sending" | "sent"; export function ShareToChatDialog({ body = "", quote, + entity, onClose, }: { /** Plain message text. Empty when sending a quote on its own. */ body?: string; /** A passage from the library, sent as a tappable quotation. */ quote?: QuotePayload; + /** A post, sent as a card. Only the reference travels — the BFF resolves + * the contents and re-checks visibility for each viewer. */ + entity?: { type: "post"; id: string }; onClose: () => void; }) { const { user } = useAuth(); @@ -111,7 +115,7 @@ export function ShareToChatDialog({ mark(handle, "sending"); try { const id = conversationId ?? (await chat.openDM(handle)).id; - await chat.send(id, body, { quote }); + await chat.send(id, body, { quote, entity }); mark(handle, "sent"); } catch { mark(handle, "idle"); diff --git a/components/chat/shared-post-card.tsx b/components/chat/shared-post-card.tsx new file mode 100644 index 00000000..deee33b2 --- /dev/null +++ b/components/chat/shared-post-card.tsx @@ -0,0 +1,75 @@ +"use client"; + +/** + * A post shared into a chat, rendered as a card with its author. + * + * The contents are resolved by the BFF on every read, per viewer — never from + * a snapshot frozen at send time. So a post that has since been deleted or + * hidden arrives as `entity_unavailable` and renders as a tombstone rather + * than quietly showing content the reader is no longer allowed to see. + */ + +import Link from "next/link"; +import { ImageOff } from "lucide-react"; +import { UserAvatar } from "@/components/ui/user-avatar"; +import type { Attachment } from "@/lib/chat"; + +export function SharedPostCard({ attachment }: { attachment: Attachment }) { + const post = attachment.entity; + + if (!post || attachment.entity_unavailable) { + return ( +
+ + + This post is no longer available + +
+ ); + } + + return ( + + + + + + {post.author_name} + + + @{post.author_handle} + + + + + {post.image_url && ( + // eslint-disable-next-line @next/next/no-img-element + + )} + + {post.caption && ( + + {post.caption} + + )} + + {post.source_title && ( + + {post.source_title} + + )} + + ); +} diff --git a/components/social/post/post-actions-footer.tsx b/components/social/post/post-actions-footer.tsx index 96695d0d..c47ca439 100644 --- a/components/social/post/post-actions-footer.tsx +++ b/components/social/post/post-actions-footer.tsx @@ -39,7 +39,6 @@ import { type Comment, type Post, } from "@/lib/social"; -import { buildPostShareText } from "@/lib/post-share-text"; import { useLongPress } from "@/lib/use-long-press"; import { ShareToChatDialog } from "@/components/chat/share-to-chat-dialog"; import { useAuth } from "@/components/auth/auth-provider"; @@ -65,8 +64,9 @@ export function PostActionsFooter({ post }: { post: Post }) { // on mobile native-share; sheet mount on desktop) so the Send icon // can render a spinner while the user waits. const [sharePending, setSharePending] = useState(false); - // The post as chat-ready text. Non-null opens the picker. - const [chatBody, setChatBody] = useState(null); + // Opens the chat picker for this post. The post travels as a reference, + // not as text — the BFF renders the card and re-checks who may see it. + const [sharingToChat, setSharingToChat] = 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. @@ -148,21 +148,10 @@ export function PostActionsFooter({ post }: { post: Post }) { // Tap → send to a chat. Hold → the platform's own share sheet, for // everything outside the app. The link text is identical either way; only // the destination differs. - async function onSendClick() { - if (sharePending) return; - setSharePending(true); - try { - const share = await buildPostShareText(post, post.author_handle ?? ""); - if (!share) { - // No handle means no post URL — fall back to the sheet, which - // resolves the handle itself and has its own failure path. - await openShareSheet(post); - return; - } - setChatBody(share.text); - } finally { - setSharePending(false); - } + function onSendClick() { + // No URL to resolve: sharing in-app sends the post id, so this is + // instant where the native sheet has to await the shortener. + setSharingToChat(true); } async function onSendHold() { @@ -302,8 +291,11 @@ export function PostActionsFooter({ post }: { post: Post }) { no per-card instance lives here. The provider's openShareSheet swaps the active post under the hood. */} - {chatBody !== null && ( - setChatBody(null)} /> + {sharingToChat && ( + setSharingToChat(false)} + /> )} ); diff --git a/lib/chat.ts b/lib/chat.ts index 4d5f13b5..5905ff67 100644 --- a/lib/chat.ts +++ b/lib/chat.ts @@ -188,6 +188,25 @@ export type Attachment = { description?: string | null; site_name?: string | null; image_url?: string | null; + + /** Shared entity. `entity` is resolved per viewer on every read, so it can + * be absent even though the message still references a post. */ + entity_type?: "post" | null; + entity_id?: string | null; + entity?: SharedPost | null; + /** The post was deleted, hidden, or is no longer visible to this viewer. */ + entity_unavailable?: boolean; +}; + +/** The post card the BFF resolves for a shared-post attachment. */ +export type SharedPost = { + id: string; + author_handle: string; + author_name: string; + author_avatar_url?: string | null; + caption: string; + image_url?: string | null; + source_title?: string; }; /** How two people in a DM stand with each other. */ @@ -261,6 +280,8 @@ export const chat = { previewUrl?: string; attachments?: UploadedAttachment[]; quote?: QuotePayload; + /** Share a post as a card. Only the reference — the BFF resolves it. */ + entity?: { type: "post"; id: string }; }, ) => call(`/chat/conversations/${conversationId}/messages`, { @@ -273,6 +294,7 @@ export const chat = { preview_url: opts?.previewUrl ?? null, attachments: opts?.attachments ?? [], quote: opts?.quote ?? null, + entity: opts?.entity ?? null, }), }),