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)
|
17. Voice Messages - `PARTIAL` (record/send/play/seek/speed; UX still being polished)
|
||||||
18. Circle Video Messages - `PARTIAL` (send/play present, recording UX basic)
|
18. Circle Video Messages - `PARTIAL` (send/play present, recording UX basic)
|
||||||
19. Stickers - `PARTIAL` (web sticker picker with preset pack + favorites)
|
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)
|
21. Message History/Search - `DONE` (history/pagination/chat+global search)
|
||||||
22. Text Formatting - `PARTIAL` (bold/italic/underline/spoiler/mono/links supported; toolbar still evolving)
|
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)
|
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 STICKER_FAVORITES_KEY = "bm_sticker_favorites_v1";
|
||||||
const GIF_FAVORITES_KEY = "bm_gif_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> {
|
function loadFavorites(key: string): Set<string> {
|
||||||
try {
|
try {
|
||||||
@@ -98,6 +100,8 @@ export function MessageComposer() {
|
|||||||
const [stickerTab, setStickerTab] = useState<"all" | "favorites">("all");
|
const [stickerTab, setStickerTab] = useState<"all" | "favorites">("all");
|
||||||
const [gifTab, setGifTab] = useState<"all" | "favorites">("all");
|
const [gifTab, setGifTab] = useState<"all" | "favorites">("all");
|
||||||
const [gifQuery, setGifQuery] = useState("");
|
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 [favoriteStickers, setFavoriteStickers] = useState<Set<string>>(() => loadFavorites(STICKER_FAVORITES_KEY));
|
||||||
const [favoriteGifs, setFavoriteGifs] = useState<Set<string>>(() => loadFavorites(GIF_FAVORITES_KEY));
|
const [favoriteGifs, setFavoriteGifs] = useState<Set<string>>(() => loadFavorites(GIF_FAVORITES_KEY));
|
||||||
const [captionDraft, setCaptionDraft] = useState("");
|
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>) {
|
function onComposerKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
|
||||||
if (event.key !== "Enter") {
|
if (event.key !== "Enter") {
|
||||||
return;
|
return;
|
||||||
@@ -925,8 +982,8 @@ export function MessageComposer() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
|
||||||
{GIF_PRESETS
|
{(gifResults.length > 0 ? gifResults : GIF_PRESETS)
|
||||||
.filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase()))
|
.filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase()) || gifResults.length > 0)
|
||||||
.filter((item) => (gifTab === "favorites" ? favoriteGifs.has(item.url) : true))
|
.filter((item) => (gifTab === "favorites" ? favoriteGifs.has(item.url) : true))
|
||||||
.map((gif) => (
|
.map((gif) => (
|
||||||
<button
|
<button
|
||||||
@@ -950,6 +1007,7 @@ export function MessageComposer() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{gifLoading ? <p className="mt-2 text-xs text-slate-400">Searching GIF...</p> : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user