Files
Arkie-Library-Frontend/src/components/messageStream/MessageBubble.tsx
TerryM 2ef26390be feat(stream): bubble footer with timestamp and inline favorite/download
Match the Figma 4206-6509 card layout for /browse: every bubble now
renders a bottom row with the publish timestamp on the left and the
action buttons on the right. Image, album, video, text and link cards
show only the FavoriteButton; file-document cards show the
FavoriteButton plus a new BubbleAttachmentDownloadButton sized to
match. Removes the absolute-positioned favorite from the default
variant, drops the right-aligned timestamp block, and strips the inline
per-row download button from FileDocBubble's default variant since the
download now lives in the footer. The 'latest' masonry variant is
untouched so the home page continues to use LatestFileCard's existing
internal footer.
2026-06-03 21:20:53 +08:00

123 lines
4.0 KiB
TypeScript

import type { ComponentType } from "react";
import type { Post } from "../../types/post";
import { TextBubble } from "./bubbles/TextBubble";
import { FileDocBubble } from "./bubbles/FileDocBubble";
import { ImageBubble } from "./bubbles/ImageBubble";
import { ImageWithTextBubble } from "./bubbles/ImageWithTextBubble";
import { AlbumBubble } from "./bubbles/AlbumBubble";
import { VideoBubble } from "./bubbles/VideoBubble";
import { LinkPreviewCard } from "./LinkPreviewCard";
import { formatDateTime } from "./utils/formatTime";
import { FavoriteButton } from "../../favorites/FavoriteButton";
import { BubbleAttachmentDownloadButton } from "./BubbleAttachmentDownloadButton";
export type MessageBubbleVariant = "default" | "latest";
type BubbleComponent = ComponentType<{
post: Post;
variant?: MessageBubbleVariant;
}>;
export function pickBubble(post: Post): BubbleComponent {
const a = post.attachments;
if (a.length === 0) return TextBubble;
if (a.length >= 2 && a.every((x) => x.kind === "image")) return AlbumBubble;
const only = a[0];
if (only.kind === "video") return VideoBubble;
if (only.kind === "image") {
return post.text ? ImageWithTextBubble : ImageBubble;
}
return FileDocBubble;
}
export function MessageBubble({
post,
fluid = false,
variant = "default",
}: {
post: Post;
/** When true, fill the parent container instead of applying the standalone
* feed max-widths. Used by the desktop 3-column masonry on the home page. */
fluid?: boolean;
/** Desktop latest-updates cards follow the dedicated Figma masonry design. */
variant?: MessageBubbleVariant;
}) {
const Bubble = pickBubble(post);
const isVisual =
Bubble === AlbumBubble ||
Bubble === VideoBubble ||
Bubble === ImageBubble ||
Bubble === ImageWithTextBubble;
const isFileBubble = Bubble === FileDocBubble;
const isLatestVariant = variant === "latest";
const isLatestFileCard = isLatestVariant && isFileBubble;
return (
<div
id={`post-${post.id}`}
className={
fluid
? "w-full scroll-mt-[82px] md:scroll-mt-[98px]"
: "mx-auto w-full max-w-[358px] scroll-mt-[82px] md:max-w-[680px] md:scroll-mt-[98px] lg:max-w-[900px] xl:max-w-[1120px]"
}
>
<article
className={`relative w-full overflow-hidden rounded-2xl bg-[#272632] text-left shadow-sm ${
isVisual || isLatestFileCard ? "p-0" : "px-4 py-3"
}`}
>
{isLatestVariant && !isFileBubble ? (
<FavoriteButton
resourceId={post.id}
size="sm"
className="absolute z-20 bottom-4 right-4 shadow-lg shadow-black/30"
/>
) : null}
<Bubble post={post} variant={variant} />
{post.linkPreview ? (
<div className={isVisual ? "px-4 pt-3" : "mt-3"}>
<LinkPreviewCard preview={post.linkPreview} />
</div>
) : null}
{!isLatestVariant ? (
<div
className={`flex items-center justify-between gap-3 ${
isVisual ? "px-4 pb-3 pt-3" : "mt-3"
}`}
>
<time
dateTime={post.publishedAt}
className="min-w-0 truncate text-[12px] leading-[19px] text-[#A8A9AE]"
>
{formatDateTime(post.publishedAt)}
</time>
<div className="flex shrink-0 items-center gap-2">
<FavoriteButton resourceId={post.id} size="sm" />
{isFileBubble && post.attachments[0] ? (
<BubbleAttachmentDownloadButton
postId={post.id}
attachment={post.attachments[0]}
/>
) : null}
</div>
</div>
) : null}
{isLatestVariant && !isFileBubble ? (
<time
dateTime={post.publishedAt}
className={`block text-right text-[12px] leading-[19px] text-[#A8A9AE] ${
isVisual ? "px-4 pb-3 pt-0.5" : "mt-3"
}`}
>
{formatDateTime(post.publishedAt)}
</time>
) : null}
</article>
</div>
);
}