feat: chat stickers
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Stickers on a message: a 2-column grid, rendered without a chat bubble.
|
||||
*
|
||||
* Unlike the media grid, this never collapses into "+N more" — every sticker
|
||||
* sent is drawn. They are small and deliberately playful, and hiding some
|
||||
* behind a counter would make a wall of them read as an attachment list
|
||||
* rather than what it is.
|
||||
*
|
||||
* Long-pressing or right-clicking one offers to keep it, which is the only
|
||||
* way a sticker spreads beyond the person who earned it.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { toast } from "sonner";
|
||||
import { chat, type Attachment } from "@/lib/chat";
|
||||
|
||||
export function StickerGrid({
|
||||
conversationId,
|
||||
messageId,
|
||||
stickers,
|
||||
/** Own stickers are already in your collection — no point offering to save. */
|
||||
mine,
|
||||
}: {
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
stickers: Attachment[];
|
||||
mine: boolean;
|
||||
}) {
|
||||
const [saving, setSaving] = useState<string | null>(null);
|
||||
|
||||
if (stickers.length === 0) return null;
|
||||
|
||||
const save = async (a: Attachment) => {
|
||||
const key = a.entity_id;
|
||||
if (!key || mine || saving) return;
|
||||
setSaving(key);
|
||||
try {
|
||||
await chat.saveSticker(conversationId, messageId, key);
|
||||
toast.success(`Saved ${a.title ?? "sticker"} to your collection`);
|
||||
} catch {
|
||||
toast.error("Could not save that sticker");
|
||||
} finally {
|
||||
setSaving(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid w-fit grid-cols-2 gap-1">
|
||||
{stickers.map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
type="button"
|
||||
disabled={mine}
|
||||
onContextMenu={(e) => {
|
||||
if (mine) return;
|
||||
e.preventDefault();
|
||||
void save(a);
|
||||
}}
|
||||
title={mine ? (a.title ?? "") : `${a.title ?? "Sticker"} — hold to save`}
|
||||
aria-label={a.title ?? "Sticker"}
|
||||
className="relative size-20 disabled:cursor-default sm:size-24"
|
||||
>
|
||||
{a.image_url && (
|
||||
<Image
|
||||
src={a.image_url}
|
||||
alt={a.title ?? ""}
|
||||
fill
|
||||
sizes="96px"
|
||||
className="object-contain"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user