2026-05-25 05:25:57 +08:00
|
|
|
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";
|
2026-05-30 01:40:00 +08:00
|
|
|
import { LinkPreviewCard } from "./LinkPreviewCard";
|
2026-05-25 05:25:57 +08:00
|
|
|
import { formatDateTime } from "./utils/formatTime";
|
2026-06-02 00:36:11 +08:00
|
|
|
import { FavoriteButton } from "../../favorites/FavoriteButton";
|
2026-05-25 05:25:57 +08:00
|
|
|
|
2026-06-03 08:18:05 +08:00
|
|
|
export type MessageBubbleVariant = "default" | "latest";
|
|
|
|
|
|
|
|
|
|
type BubbleComponent = ComponentType<{
|
|
|
|
|
post: Post;
|
|
|
|
|
variant?: MessageBubbleVariant;
|
|
|
|
|
}>;
|
2026-05-25 05:25:57 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 18:35:57 +08:00
|
|
|
export function MessageBubble({
|
|
|
|
|
post,
|
|
|
|
|
fluid = false,
|
2026-06-03 08:18:05 +08:00
|
|
|
variant = "default",
|
2026-06-05 18:16:33 +08:00
|
|
|
onFavoriteChange,
|
2026-05-31 18:35:57 +08:00
|
|
|
}: {
|
|
|
|
|
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;
|
2026-06-03 08:18:05 +08:00
|
|
|
/** Desktop latest-updates cards follow the dedicated Figma masonry design. */
|
|
|
|
|
variant?: MessageBubbleVariant;
|
2026-06-05 18:16:33 +08:00
|
|
|
onFavoriteChange?: (postId: string, favorited: boolean) => void;
|
2026-05-31 18:35:57 +08:00
|
|
|
}) {
|
2026-05-25 05:25:57 +08:00
|
|
|
const Bubble = pickBubble(post);
|
2026-05-28 10:36:38 +08:00
|
|
|
const isVisual =
|
|
|
|
|
Bubble === AlbumBubble ||
|
|
|
|
|
Bubble === VideoBubble ||
|
|
|
|
|
Bubble === ImageBubble ||
|
|
|
|
|
Bubble === ImageWithTextBubble;
|
2026-06-03 21:20:53 +08:00
|
|
|
const isFileBubble = Bubble === FileDocBubble;
|
|
|
|
|
const isLatestVariant = variant === "latest";
|
|
|
|
|
const isLatestFileCard = isLatestVariant && isFileBubble;
|
2026-05-25 05:25:57 +08:00
|
|
|
|
|
|
|
|
return (
|
2026-05-27 13:16:40 +08:00
|
|
|
<div
|
2026-05-25 05:25:57 +08:00
|
|
|
id={`post-${post.id}`}
|
2026-05-31 18:35:57 +08:00
|
|
|
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]"
|
|
|
|
|
}
|
2026-05-25 05:25:57 +08:00
|
|
|
>
|
2026-05-27 13:16:40 +08:00
|
|
|
<article
|
2026-05-28 10:36:38 +08:00
|
|
|
className={`relative w-full overflow-hidden rounded-2xl bg-[#272632] text-left shadow-sm ${
|
2026-06-03 08:18:05 +08:00
|
|
|
isVisual || isLatestFileCard ? "p-0" : "px-4 py-3"
|
2026-05-28 10:36:38 +08:00
|
|
|
}`}
|
2026-05-25 05:25:57 +08:00
|
|
|
>
|
2026-06-03 21:20:53 +08:00
|
|
|
{isLatestVariant && !isFileBubble ? (
|
2026-06-03 08:18:05 +08:00
|
|
|
<FavoriteButton
|
|
|
|
|
resourceId={post.id}
|
|
|
|
|
size="sm"
|
2026-06-03 21:20:53 +08:00
|
|
|
className="absolute z-20 bottom-4 right-4 shadow-lg shadow-black/30"
|
2026-06-05 18:16:33 +08:00
|
|
|
onFavoriteChange={(favorited) =>
|
|
|
|
|
onFavoriteChange?.(post.id, favorited)
|
|
|
|
|
}
|
2026-06-03 08:18:05 +08:00
|
|
|
/>
|
|
|
|
|
) : null}
|
2026-06-03 21:20:53 +08:00
|
|
|
|
2026-06-03 08:18:05 +08:00
|
|
|
<Bubble post={post} variant={variant} />
|
2026-06-03 21:20:53 +08:00
|
|
|
|
2026-05-30 01:40:00 +08:00
|
|
|
{post.linkPreview ? (
|
|
|
|
|
<div className={isVisual ? "px-4 pt-3" : "mt-3"}>
|
|
|
|
|
<LinkPreviewCard preview={post.linkPreview} />
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-06-03 21:20:53 +08:00
|
|
|
|
|
|
|
|
{!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">
|
2026-06-05 18:16:33 +08:00
|
|
|
<FavoriteButton
|
|
|
|
|
resourceId={post.id}
|
|
|
|
|
size="sm"
|
|
|
|
|
onFavoriteChange={(favorited) =>
|
|
|
|
|
onFavoriteChange?.(post.id, favorited)
|
|
|
|
|
}
|
|
|
|
|
/>
|
2026-06-03 21:20:53 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{isLatestVariant && !isFileBubble ? (
|
2026-06-03 08:18:05 +08:00
|
|
|
<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}
|
2026-05-27 13:16:40 +08:00
|
|
|
</article>
|
|
|
|
|
</div>
|
2026-05-25 05:25:57 +08:00
|
|
|
);
|
|
|
|
|
}
|