diff options
Diffstat (limited to 'apps/web/src/stickers/stickers-store.ts')
| -rw-r--r-- | apps/web/src/stickers/stickers-store.ts | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/apps/web/src/stickers/stickers-store.ts b/apps/web/src/stickers/stickers-store.ts new file mode 100644 index 00000000..aacc5a79 --- /dev/null +++ b/apps/web/src/stickers/stickers-store.ts @@ -0,0 +1,238 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { + browseAll, + browseCategory, + searchAll, + searchStickers as searchStickersFromProviders, +} from "@/stickers"; +import type { StickerBrowseResult, StickerSearchResult } from "@/stickers"; +import { STICKER_CATEGORIES } from "@/stickers/categories"; +import type { StickerCategory } from "@/stickers/types"; +import { registerDefaultStickerProviders } from "@/stickers/providers"; +import { stickersRegistry } from "@/stickers/registry"; +import { parseStickerId } from "@/stickers/sticker-id"; + +const MAX_RECENT_STICKERS = 50; +let browseRequestVersion = 0; + +function isValidStickerId(value: unknown): value is string { + if (typeof value !== "string") { + return false; + } + + try { + const parsed = parseStickerId({ stickerId: value }); + return stickersRegistry.has(parsed.providerId); + } catch { + return false; + } +} + +function sanitizeRecentStickers({ + recentStickers, +}: { + recentStickers: unknown; +}): string[] { + registerDefaultStickerProviders({}); + + if (!Array.isArray(recentStickers)) { + return []; + } + + const sanitized: string[] = []; + for (const stickerId of recentStickers) { + if (!isValidStickerId(stickerId)) { + continue; + } + if (sanitized.includes(stickerId)) { + continue; + } + sanitized.push(stickerId); + if (sanitized.length >= MAX_RECENT_STICKERS) { + break; + } + } + + return sanitized; +} + +type ViewMode = "search" | "browse"; + +interface StickersStore { + searchQuery: string; + selectedCategory: StickerCategory; + viewMode: ViewMode; + searchResults: StickerSearchResult | null; + browseContent: StickerBrowseResult | null; + recentStickers: string[]; + isSearching: boolean; + isBrowsing: boolean; + + setSearchQuery: ({ query }: { query: string }) => void; + setSelectedCategory: ({ category }: { category: StickerCategory }) => void; + searchStickers: ({ query }: { query: string }) => Promise<void>; + browseStickers: () => Promise<void>; + addToRecentStickers: ({ stickerId }: { stickerId: string }) => void; + clearRecentStickers: () => void; +} + +export const useStickersStore = create<StickersStore>()( + persist( + (set, get) => ({ + searchQuery: "", + selectedCategory: "all", + viewMode: "browse", + + searchResults: null, + browseContent: null, + recentStickers: [], + + isSearching: false, + isBrowsing: false, + + setSearchQuery: ({ query }) => set({ searchQuery: query }), + + setSelectedCategory: ({ category }) => { + set({ + selectedCategory: category in STICKER_CATEGORIES ? category : "all", + browseContent: null, + }); + + const query = get().searchQuery.trim(); + if (query) { + void get().searchStickers({ query }); + return; + } + + void get().browseStickers(); + }, + + searchStickers: async ({ query }: { query: string }) => { + const trimmedQuery = query.trim(); + if (!trimmedQuery) { + set({ searchResults: null, viewMode: "browse" }); + await get().browseStickers(); + return; + } + + const category = get().selectedCategory; + const selectedCategory = + category in STICKER_CATEGORIES ? category : "all"; + + set({ isSearching: true, viewMode: "search" }); + try { + if (selectedCategory === "all") { + const browseContent = await searchAll({ query: trimmedQuery }); + set({ browseContent, searchResults: null }); + } else { + const results = await searchStickersFromProviders({ + query: trimmedQuery, + category: selectedCategory, + limit: 100, + }); + set({ searchResults: results }); + } + } catch (error) { + console.error("Search failed:", error); + set({ searchResults: null }); + } finally { + set({ isSearching: false }); + } + }, + + browseStickers: async () => { + const version = ++browseRequestVersion; + const category = get().selectedCategory; + const selectedCategory = + category in STICKER_CATEGORIES ? category : "all"; + + set({ isBrowsing: true, viewMode: "browse" }); + try { + const browseContent = + selectedCategory === "all" + ? await browseAll({ + recentStickers: get().recentStickers, + }) + : await browseCategory({ + category: selectedCategory, + }); + + if (version !== browseRequestVersion) return; + set({ browseContent }); + } catch (error) { + if (version !== browseRequestVersion) return; + console.error("Browse failed:", error); + set({ browseContent: null }); + } finally { + if (version === browseRequestVersion) { + set({ isBrowsing: false }); + } + } + }, + + addToRecentStickers: ({ stickerId }: { stickerId: string }) => { + const sanitizedStickerIds = sanitizeRecentStickers({ + recentStickers: [stickerId], + }); + if (sanitizedStickerIds.length === 0) { + return; + } + + set((state) => { + const recent = [ + sanitizedStickerIds[0], + ...state.recentStickers.filter((s) => s !== sanitizedStickerIds[0]), + ]; + return { + recentStickers: recent.slice(0, MAX_RECENT_STICKERS), + }; + }); + + if (get().viewMode === "browse" && get().selectedCategory === "all") { + void get().browseStickers(); + } + }, + + clearRecentStickers: () => { + set({ recentStickers: [] }); + + if (get().viewMode === "browse" && get().selectedCategory === "all") { + void get().browseStickers(); + } + }, + }), + { + name: "stickers-settings", + version: 1, + migrate: (persistedState) => { + if ( + typeof persistedState === "object" && + persistedState !== null && + "selectedCategory" in persistedState + ) { + const typedState = persistedState as { + selectedCategory?: string; + recentStickers?: string[]; + }; + const category = typedState.selectedCategory ?? "all"; + return { + ...typedState, + selectedCategory: + category in STICKER_CATEGORIES + ? (category as StickerCategory) + : "all", + recentStickers: sanitizeRecentStickers({ + recentStickers: typedState.recentStickers ?? [], + }), + }; + } + return persistedState; + }, + partialize: (state) => ({ + selectedCategory: state.selectedCategory, + recentStickers: state.recentStickers, + }), + }, + ), +); |
