feat: sticker preview

This commit is contained in:
gosangam
2026-08-30 10:54:56 +05:30
parent 7a9d362b15
commit cc9ea45c61
+39 -6
View File
@@ -1,7 +1,13 @@
"use client"; "use client";
/** /**
* Stickers on a message: a 2-column grid, rendered without a chat bubble. * Stickers on a message, rendered without a chat bubble.
*
* Laid out as rows of at most two, NOT a fixed 2-column grid: a grid reserves
* the second cell whether or not anything fills it, so a single sticker sat
* beside a gap the width of another sticker. Rows size themselves — a lone
* sticker takes the row and is drawn larger, a pair shares it and both shrink.
* An odd count therefore ends on one big sticker rather than a hole.
* *
* Unlike the media grid, this never collapses into "+N more" — every sticker * Unlike the media grid, this never collapses into "+N more" — every sticker
* sent is drawn. They are small and deliberately playful, and hiding some * sent is drawn. They are small and deliberately playful, and hiding some
@@ -16,6 +22,10 @@ import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { toast } from "sonner"; import { toast } from "sonner";
import { chat, type Attachment } from "@/lib/chat"; import { chat, type Attachment } from "@/lib/chat";
import { cn } from "@/lib/utils";
/** At most two per row, as WhatsApp and Telegram do. */
const STICKERS_PER_ROW = 2;
export function StickerGrid({ export function StickerGrid({
conversationId, conversationId,
@@ -47,9 +57,22 @@ export function StickerGrid({
} }
}; };
// Chunk into rows of at most two. Doing this in JS rather than CSS is what
// lets a row know how many it holds, which is what sets the size.
const rows: Attachment[][] = [];
for (let i = 0; i < stickers.length; i += STICKERS_PER_ROW) {
rows.push(stickers.slice(i, i + STICKERS_PER_ROW));
}
return (
// w-fit so the block hugs its rows: any fixed width would reintroduce the
// empty space this layout exists to remove.
<div className="flex w-fit flex-col gap-1">
{rows.map((row, i) => (
<div key={i} className="flex gap-1">
{row.map((a) => {
const alone = row.length === 1;
return ( return (
<div className="grid w-fit grid-cols-2 gap-1">
{stickers.map((a) => (
<button <button
key={a.id} key={a.id}
type="button" type="button"
@@ -59,20 +82,30 @@ export function StickerGrid({
e.preventDefault(); e.preventDefault();
void save(a); void save(a);
}} }}
title={mine ? (a.title ?? "") : `${a.title ?? "Sticker"} — hold to save`} title={
mine
? (a.title ?? "")
: `${a.title ?? "Sticker"} — hold to save`
}
aria-label={a.title ?? "Sticker"} aria-label={a.title ?? "Sticker"}
className="relative size-20 disabled:cursor-default sm:size-24" className={cn(
"relative disabled:cursor-default",
alone ? "size-32 sm:size-36" : "size-24 sm:size-28",
)}
> >
{a.image_url && ( {a.image_url && (
<Image <Image
src={a.image_url} src={a.image_url}
alt={a.title ?? ""} alt={a.title ?? ""}
fill fill
sizes="96px" sizes={alone ? "144px" : "112px"}
className="object-contain" className="object-contain"
/> />
)} )}
</button> </button>
);
})}
</div>
))} ))}
</div> </div>
); );