feat(web): add Tenor-backed GIF search in composer
All checks were successful
CI / test (push) Successful in 22s
All checks were successful
CI / test (push) Successful in 22s
This commit is contained in:
@@ -26,7 +26,7 @@ Legend:
|
||||
17. Voice Messages - `PARTIAL` (record/send/play/seek/speed; UX still being polished)
|
||||
18. Circle Video Messages - `PARTIAL` (send/play present, recording UX basic)
|
||||
19. Stickers - `PARTIAL` (web sticker picker with preset pack + favorites)
|
||||
20. GIF - `PARTIAL` (web GIF picker with preset catalog + local search + favorites)
|
||||
20. GIF - `PARTIAL` (web GIF picker with Tenor search + preset fallback + favorites)
|
||||
21. Message History/Search - `DONE` (history/pagination/chat+global search)
|
||||
22. Text Formatting - `PARTIAL` (bold/italic/underline/spoiler/mono/links supported; toolbar still evolving)
|
||||
23. Groups - `PARTIAL` (create/add/remove/invite link; advanced moderation partial)
|
||||
|
||||
@@ -31,6 +31,8 @@ const GIF_PRESETS: Array<{ name: string; url: string }> = [
|
||||
|
||||
const STICKER_FAVORITES_KEY = "bm_sticker_favorites_v1";
|
||||
const GIF_FAVORITES_KEY = "bm_gif_favorites_v1";
|
||||
const TENOR_API_KEY = "LIVDSRZULELA";
|
||||
const TENOR_CLIENT_KEY = "benya_messenger_web";
|
||||
|
||||
function loadFavorites(key: string): Set<string> {
|
||||
try {
|
||||
@@ -98,6 +100,8 @@ export function MessageComposer() {
|
||||
const [stickerTab, setStickerTab] = useState<"all" | "favorites">("all");
|
||||
const [gifTab, setGifTab] = useState<"all" | "favorites">("all");
|
||||
const [gifQuery, setGifQuery] = useState("");
|
||||
const [gifResults, setGifResults] = useState<Array<{ name: string; url: string }>>([]);
|
||||
const [gifLoading, setGifLoading] = useState(false);
|
||||
const [favoriteStickers, setFavoriteStickers] = useState<Set<string>>(() => loadFavorites(STICKER_FAVORITES_KEY));
|
||||
const [favoriteGifs, setFavoriteGifs] = useState<Set<string>>(() => loadFavorites(GIF_FAVORITES_KEY));
|
||||
const [captionDraft, setCaptionDraft] = useState("");
|
||||
@@ -312,6 +316,59 @@ export function MessageComposer() {
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const term = gifQuery.trim();
|
||||
if (!showGifMenu || term.length < 2) {
|
||||
setGifResults([]);
|
||||
setGifLoading(false);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const timer = window.setTimeout(() => {
|
||||
setGifLoading(true);
|
||||
void (async () => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
q: term,
|
||||
key: TENOR_API_KEY,
|
||||
client_key: TENOR_CLIENT_KEY,
|
||||
limit: "24",
|
||||
media_filter: "gif",
|
||||
contentfilter: "medium",
|
||||
});
|
||||
const response = await fetch(`https://tenor.googleapis.com/v2/search?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("gif search failed");
|
||||
}
|
||||
const data = (await response.json()) as {
|
||||
results?: Array<{ content_description?: string; media_formats?: { gif?: { url?: string } } }>;
|
||||
};
|
||||
if (cancelled) return;
|
||||
const rows =
|
||||
data.results
|
||||
?.map((item) => ({
|
||||
name: item.content_description || "GIF",
|
||||
url: item.media_formats?.gif?.url || "",
|
||||
}))
|
||||
.filter((item) => item.url.length > 0) ?? [];
|
||||
setGifResults(rows);
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setGifResults([]);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setGifLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, 280);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [gifQuery, showGifMenu]);
|
||||
|
||||
function onComposerKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
|
||||
if (event.key !== "Enter") {
|
||||
return;
|
||||
@@ -925,8 +982,8 @@ export function MessageComposer() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
|
||||
{GIF_PRESETS
|
||||
.filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase()))
|
||||
{(gifResults.length > 0 ? gifResults : GIF_PRESETS)
|
||||
.filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase()) || gifResults.length > 0)
|
||||
.filter((item) => (gifTab === "favorites" ? favoriteGifs.has(item.url) : true))
|
||||
.map((gif) => (
|
||||
<button
|
||||
@@ -950,6 +1007,7 @@ export function MessageComposer() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{gifLoading ? <p className="mt-2 text-xs text-slate-400">Searching GIF...</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user