From 88ff11c130e3ecef8aa2e95fcbc17c7059fc12d6 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 13:38:55 +0300 Subject: [PATCH] feat(web): add favorites for sticker and GIF pickers --- docs/core-checklist-status.md | 4 +- web/src/components/MessageComposer.tsx | 122 +++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 9 deletions(-) diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index 1cc313d..85ba201 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -25,8 +25,8 @@ Legend: 16. Media & Attachments - `DONE` (upload/preview/download/gallery) 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) -20. GIF - `PARTIAL` (web GIF picker with preset catalog + local search) +19. Stickers - `PARTIAL` (web sticker picker with preset pack + favorites) +20. GIF - `PARTIAL` (web GIF picker with preset catalog + local search + 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) diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index 9bdfbac..6910324 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -29,6 +29,29 @@ const GIF_PRESETS: Array<{ name: string; url: string }> = [ { name: "Success", url: "https://media.giphy.com/media/4T7e4DmcrP9du/giphy.gif" }, ]; +const STICKER_FAVORITES_KEY = "bm_sticker_favorites_v1"; +const GIF_FAVORITES_KEY = "bm_gif_favorites_v1"; + +function loadFavorites(key: string): Set { + try { + const raw = window.localStorage.getItem(key); + if (!raw) return new Set(); + const parsed = JSON.parse(raw) as string[]; + if (!Array.isArray(parsed)) return new Set(); + return new Set(parsed.filter((item) => typeof item === "string")); + } catch { + return new Set(); + } +} + +function saveFavorites(key: string, values: Set): void { + try { + window.localStorage.setItem(key, JSON.stringify([...values])); + } catch { + return; + } +} + export function MessageComposer() { const activeChatId = useChatStore((s) => s.activeChatId); const chats = useChatStore((s) => s.chats); @@ -72,7 +95,11 @@ export function MessageComposer() { const [showFormatMenu, setShowFormatMenu] = useState(false); const [showStickerMenu, setShowStickerMenu] = useState(false); const [showGifMenu, setShowGifMenu] = useState(false); + const [stickerTab, setStickerTab] = useState<"all" | "favorites">("all"); + const [gifTab, setGifTab] = useState<"all" | "favorites">("all"); const [gifQuery, setGifQuery] = useState(""); + const [favoriteStickers, setFavoriteStickers] = useState>(() => loadFavorites(STICKER_FAVORITES_KEY)); + const [favoriteGifs, setFavoriteGifs] = useState>(() => loadFavorites(GIF_FAVORITES_KEY)); const [captionDraft, setCaptionDraft] = useState(""); const mediaInputRef = useRef(null); const fileInputRef = useRef(null); @@ -259,6 +286,32 @@ export function MessageComposer() { } } + function toggleStickerFavorite(url: string) { + setFavoriteStickers((prev) => { + const next = new Set(prev); + if (next.has(url)) { + next.delete(url); + } else { + next.add(url); + } + saveFavorites(STICKER_FAVORITES_KEY, next); + return next; + }); + } + + function toggleGifFavorite(url: string) { + setFavoriteGifs((prev) => { + const next = new Set(prev); + if (next.has(url)) { + next.delete(url); + } else { + next.add(url); + } + saveFavorites(GIF_FAVORITES_KEY, next); + return next; + }); + } + function onComposerKeyDown(event: KeyboardEvent) { if (event.key !== "Enter") { return; @@ -795,20 +848,46 @@ export function MessageComposer() {

Stickers

- +
+ + + +
- {STICKER_PRESETS.map((sticker) => ( + {STICKER_PRESETS.filter((item) => (stickerTab === "favorites" ? favoriteStickers.has(item.url) : true)).map((sticker) => ( ))}
@@ -829,16 +908,45 @@ export function MessageComposer() { Close
+
+ + +
- {GIF_PRESETS.filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase())).map((gif) => ( + {GIF_PRESETS + .filter((item) => item.name.toLowerCase().includes(gifQuery.trim().toLowerCase())) + .filter((item) => (gifTab === "favorites" ? favoriteGifs.has(item.url) : true)) + .map((gif) => ( ))}