feat: sticker preview
This commit is contained in:
@@ -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,32 +57,55 @@ 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 (
|
return (
|
||||||
<div className="grid w-fit grid-cols-2 gap-1">
|
// w-fit so the block hugs its rows: any fixed width would reintroduce the
|
||||||
{stickers.map((a) => (
|
// empty space this layout exists to remove.
|
||||||
<button
|
<div className="flex w-fit flex-col gap-1">
|
||||||
key={a.id}
|
{rows.map((row, i) => (
|
||||||
type="button"
|
<div key={i} className="flex gap-1">
|
||||||
disabled={mine}
|
{row.map((a) => {
|
||||||
onContextMenu={(e) => {
|
const alone = row.length === 1;
|
||||||
if (mine) return;
|
return (
|
||||||
e.preventDefault();
|
<button
|
||||||
void save(a);
|
key={a.id}
|
||||||
}}
|
type="button"
|
||||||
title={mine ? (a.title ?? "") : `${a.title ?? "Sticker"} — hold to save`}
|
disabled={mine}
|
||||||
aria-label={a.title ?? "Sticker"}
|
onContextMenu={(e) => {
|
||||||
className="relative size-20 disabled:cursor-default sm:size-24"
|
if (mine) return;
|
||||||
>
|
e.preventDefault();
|
||||||
{a.image_url && (
|
void save(a);
|
||||||
<Image
|
}}
|
||||||
src={a.image_url}
|
title={
|
||||||
alt={a.title ?? ""}
|
mine
|
||||||
fill
|
? (a.title ?? "")
|
||||||
sizes="96px"
|
: `${a.title ?? "Sticker"} — hold to save`
|
||||||
className="object-contain"
|
}
|
||||||
/>
|
aria-label={a.title ?? "Sticker"}
|
||||||
)}
|
className={cn(
|
||||||
</button>
|
"relative disabled:cursor-default",
|
||||||
|
alone ? "size-32 sm:size-36" : "size-24 sm:size-28",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{a.image_url && (
|
||||||
|
<Image
|
||||||
|
src={a.image_url}
|
||||||
|
alt={a.title ?? ""}
|
||||||
|
fill
|
||||||
|
sizes={alone ? "144px" : "112px"}
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user