2026-05-26 12:07:13 +08:00
|
|
|
import { useI18n } from "../../../i18n";
|
2026-05-29 17:50:53 +08:00
|
|
|
import type { Post } from "../../../types/post";
|
2026-05-29 22:16:55 +08:00
|
|
|
import { ALBUM_GAP, ALBUM_MAX_HEIGHT } from "../../../constants/media";
|
2026-05-27 15:30:40 +08:00
|
|
|
import { AttachmentDownloadPill } from "../AttachmentDownloadPill";
|
2026-05-29 13:09:09 +08:00
|
|
|
import { BubbleImage } from "../BubbleImage";
|
2026-05-29 22:16:55 +08:00
|
|
|
import { useImageRatios } from "../hooks/useImageRatios";
|
2026-05-25 05:25:57 +08:00
|
|
|
import { useLightbox } from "../overlays/ImageLightbox";
|
|
|
|
|
import { autolink } from "../utils/autolink";
|
2026-05-29 22:16:55 +08:00
|
|
|
import { computeAlbumLayout } from "../utils/albumLayout";
|
2026-05-26 12:07:13 +08:00
|
|
|
import { postDisplayText } from "../utils/postText";
|
2026-05-29 23:49:59 +08:00
|
|
|
import { CollapsibleText } from "../CollapsibleText";
|
2026-05-25 05:25:57 +08:00
|
|
|
|
|
|
|
|
const MAX_VISIBLE = 4;
|
|
|
|
|
|
|
|
|
|
export function AlbumBubble({ post }: { post: Post }) {
|
|
|
|
|
const { openLightbox } = useLightbox();
|
2026-05-26 12:07:13 +08:00
|
|
|
const { lang } = useI18n();
|
2026-05-25 05:25:57 +08:00
|
|
|
const images = post.attachments;
|
2026-05-26 12:07:13 +08:00
|
|
|
const text = postDisplayText(post, lang);
|
2026-05-25 05:25:57 +08:00
|
|
|
const visible = images.slice(0, MAX_VISIBLE);
|
|
|
|
|
const extra = images.length - MAX_VISIBLE;
|
2026-05-29 22:16:55 +08:00
|
|
|
|
|
|
|
|
const sources = visible.map(
|
|
|
|
|
(att) => att.thumbnailUrl ?? att.thumbUrl ?? att.url,
|
|
|
|
|
);
|
|
|
|
|
const ratios = useImageRatios(visible, sources);
|
|
|
|
|
const layout = computeAlbumLayout(ratios);
|
2026-05-25 05:25:57 +08:00
|
|
|
|
|
|
|
|
return (
|
2026-05-28 10:36:38 +08:00
|
|
|
<div className="flex flex-col">
|
2026-05-29 22:56:20 +08:00
|
|
|
{/* aspect-ratio sets a definite box height; tiles are absolutely
|
|
|
|
|
positioned by percentage so the mosaic fills it exactly (no CSS-grid
|
|
|
|
|
`fr` quirks, no leftover black band). */}
|
2026-05-29 22:16:55 +08:00
|
|
|
<div
|
2026-05-29 22:37:16 +08:00
|
|
|
className="relative w-full overflow-hidden bg-black"
|
2026-05-29 22:16:55 +08:00
|
|
|
style={{
|
|
|
|
|
aspectRatio: layout?.aspectRatio,
|
|
|
|
|
maxHeight: ALBUM_MAX_HEIGHT,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-29 22:56:20 +08:00
|
|
|
{visible.map((att, i) => {
|
|
|
|
|
const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0;
|
|
|
|
|
const tile = layout?.tiles[i];
|
|
|
|
|
if (!tile) return null;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={att.id}
|
|
|
|
|
className="absolute overflow-hidden"
|
|
|
|
|
style={{
|
|
|
|
|
left: `${tile.left * 100}%`,
|
|
|
|
|
top: `${tile.top * 100}%`,
|
|
|
|
|
width: `calc(${tile.width * 100}% - ${ALBUM_GAP}px)`,
|
|
|
|
|
height: `calc(${tile.height * 100}% - ${ALBUM_GAP}px)`,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => openLightbox(images, i, text, post.id)}
|
|
|
|
|
className="group block h-full w-full"
|
|
|
|
|
aria-label={
|
|
|
|
|
isLastSlot ? `View all ${images.length} images` : "View image"
|
2026-05-29 17:50:53 +08:00
|
|
|
}
|
2026-05-27 15:30:40 +08:00
|
|
|
>
|
2026-05-29 22:56:20 +08:00
|
|
|
<BubbleImage
|
|
|
|
|
src={sources[i]}
|
|
|
|
|
fallbackSrc={[att.thumbUrl, att.url]}
|
|
|
|
|
loading="lazy"
|
|
|
|
|
className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.03]"
|
|
|
|
|
/>
|
|
|
|
|
{isLastSlot ? (
|
|
|
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center gap-0.5 bg-black/60 text-white backdrop-blur-[1px] transition group-hover:bg-black/50">
|
|
|
|
|
<span className="text-3xl font-bold leading-none md:text-4xl">
|
|
|
|
|
+{extra}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-05-27 15:30:40 +08:00
|
|
|
) : null}
|
2026-05-29 22:56:20 +08:00
|
|
|
</button>
|
|
|
|
|
{!isLastSlot ? (
|
|
|
|
|
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-05-25 05:25:57 +08:00
|
|
|
</div>
|
2026-05-26 12:07:13 +08:00
|
|
|
{text ? (
|
2026-05-29 23:49:59 +08:00
|
|
|
<CollapsibleText
|
|
|
|
|
wrapperClassName="px-4 pt-3"
|
|
|
|
|
className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-6 text-neutral-100"
|
|
|
|
|
>
|
2026-05-26 12:07:13 +08:00
|
|
|
{autolink(text)}
|
2026-05-29 23:49:59 +08:00
|
|
|
</CollapsibleText>
|
2026-05-25 05:25:57 +08:00
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|