Files
Arkie-Library-Frontend/src/components/messageStream/bubbles/AlbumBubble.tsx

103 lines
3.8 KiB
TypeScript
Raw Normal View History

2026-05-26 12:07:13 +08:00
import { useI18n } from "../../../i18n";
import type { Post } from "../../../types/post";
import { ALBUM_GAP, ALBUM_MAX_HEIGHT } from "../../../constants/media";
import { AttachmentDownloadPill } from "../AttachmentDownloadPill";
import { BubbleImage } from "../BubbleImage";
import { useImageRatios } from "../hooks/useImageRatios";
import { useLightbox } from "../overlays/ImageLightbox";
import { autolink } from "../utils/autolink";
import { computeAlbumLayout } from "../utils/albumLayout";
2026-05-26 12:07:13 +08:00
import { postDisplayText } from "../utils/postText";
const MAX_VISIBLE = 4;
export function AlbumBubble({ post }: { post: Post }) {
const { openLightbox } = useLightbox();
2026-05-26 12:07:13 +08:00
const { lang } = useI18n();
const images = post.attachments;
2026-05-26 12:07:13 +08:00
const text = postDisplayText(post, lang);
const visible = images.slice(0, MAX_VISIBLE);
const extra = images.length - MAX_VISIBLE;
const sources = visible.map(
(att) => att.thumbnailUrl ?? att.thumbUrl ?? att.url,
);
const ratios = useImageRatios(visible, sources);
const layout = computeAlbumLayout(ratios);
return (
<div className="flex flex-col">
{/* aspect-ratio lives on the wrapper so the grid gets a *definite* height
(inset-0) and its fr rows stretch instead of collapsing to content. */}
<div
className="relative w-full overflow-hidden bg-black"
style={{
aspectRatio: layout?.aspectRatio,
maxHeight: ALBUM_MAX_HEIGHT,
}}
>
<div
className="absolute inset-0 grid"
style={{
gap: ALBUM_GAP,
gridTemplateColumns: layout?.gridTemplateColumns,
gridTemplateRows: layout?.gridTemplateRows,
}}
>
{visible.map((att, i) => {
const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0;
const tile = layout?.tiles[i];
return (
<div
key={att.id}
className="relative overflow-hidden"
style={
tile
? {
gridColumn: `${tile.colStart} / ${tile.colEnd}`,
gridRow: `${tile.rowStart} / ${tile.rowEnd}`,
}
: undefined
}
>
<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"
}
>
<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>
) : null}
</button>
{!isLastSlot ? (
<AttachmentDownloadPill postId={post.id} attachment={att} />
) : null}
</div>
);
})}
</div>
</div>
2026-05-26 12:07:13 +08:00
{text ? (
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
2026-05-26 12:07:13 +08:00
{autolink(text)}
</div>
) : null}
</div>
);
}