import { DropdownMenu, DropdownMenuItem, DropdownMenuTrigger, Input, ResponsiveDropdownMenuContent, Text, useTheme, zero, } from "@streamplace/components"; import { ACTIVITY_LABELS } from "@streamplace/components/src/lib/metadata-constants"; import { useStreamplaceStore } from "@streamplace/components/src/streamplace-store/streamplace-store"; import { usePDSAgent } from "@streamplace/components/src/streamplace-store/xrpc"; import { Image } from "expo-image"; import { X } from "lucide-react-native"; import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Platform, Pressable, View } from "react-native"; import { games as gamesLex, place } from "streamplace"; import { getDidFromAtUri, getGameCoverUrl } from "../utils/game"; const { p, px, r, layout, borders, gap, flex } = zero; interface GameResult { uri: string; name: string; coverUrl?: string; } interface ActivityPickerProps { value: place.stream.livestream.Main["activity"] | undefined; onChange: ( activity: place.stream.livestream.Main["activity"] | undefined, ) => void; } interface DropdownPos { top: number; left: number; width: number; } export default function ActivityPicker({ value, onChange, }: ActivityPickerProps) { const agent = usePDSAgent(); const { theme, zero: z } = useTheme(); const c = theme.colors; const gamesEnabled = useStreamplaceStore((s) => s.gamesEnabled); const [mode, setMode] = useState<"game" | "label">(() => { if (value?.$type === "place.stream.defs#activityLabel") return "label"; if (value?.$type === "place.stream.defs#activityGame" && gamesEnabled) return "game"; return gamesEnabled ? "game" : "label"; }); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [selectedCoverUrl, setSelectedCoverUrl] = useState< string | undefined >(); const [selectedGenres, setSelectedGenres] = useState([]); const [dropdownPos, setDropdownPos] = useState(null); const debounceRef = useRef | null>(null); const inputContainerRef = useRef(null); const selectedGame = value?.$type === "place.stream.defs#activityGame" ? (value as place.stream.defs.ActivityGame) : null; const selectedLabel = value?.$type === "place.stream.defs#activityLabel" ? (value as place.stream.defs.ActivityLabel) : null; const showResults = searching || results.length > 0; useEffect(() => { if (!selectedGame || !agent || selectedCoverUrl !== undefined) return; agent.client .call(place.stream.game.getGame, { uri: selectedGame.uri }) .then((res) => { setSelectedCoverUrl(res.coverUrl); setSelectedGenres(res.genres ?? []); }) .catch(() => {}); }, [selectedGame?.uri]); useLayoutEffect(() => { if (Platform.OS !== "web" || !inputContainerRef.current || !showResults) { setDropdownPos(null); return; } const el = inputContainerRef.current as unknown as HTMLElement; const rect = el.getBoundingClientRect(); setDropdownPos({ top: rect.bottom + 4, left: rect.left, width: rect.width, }); }, [showResults]); useEffect(() => { if (!query.trim() || !agent) { setResults([]); return; } if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(async () => { setSearching(true); try { const res = await agent.client.call(place.stream.game.search, { q: query, limit: 8, }); const games: GameResult[] = []; for (const result of res.results) { if ( !gamesLex.gamesgamesgamesgames.defs.gameSummaryView.isTypeOf(result) ) continue; const did = getDidFromAtUri(result.uri); const cover = getGameCoverUrl(result.media, did); games.push({ uri: result.uri, name: result.name, coverUrl: cover, }); } setResults(games); } catch (e) { console.error("game search error:", e); setResults([]); } finally { setSearching(false); } }, 300); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [query, agent]); const selectGame = (game: GameResult) => { onChange({ $type: "place.stream.defs#activityGame", uri: game.uri as any, name: game.name, }); setSelectedCoverUrl(game.coverUrl); setQuery(""); setResults([]); }; const clearActivity = () => { onChange(undefined); setSelectedCoverUrl(undefined); setSelectedGenres([]); setQuery(""); setResults([]); }; const resultsList = ( {searching && ( Searching... )} {results.map((game) => ( selectGame(game)} style={[layout.flex.row, layout.flex.alignCenter, p[2], { gap: 10 }]} > {game.name} ))} ); return ( {(["game", "label"] as const) .filter((m) => m !== "game" || gamesEnabled) .map((m) => ( { setMode(m); if (m === "game" && selectedLabel) onChange(undefined); if (m === "label" && selectedGame) onChange(undefined); }} style={[ px[3], borders.width.thin, { paddingVertical: 6, borderRadius: 6 }, { borderColor: mode === m ? c.border : c.border }, { backgroundColor: mode === m ? c.card : "transparent", }, ]} > {m === "game" ? "Game" : "Other Activity"} ))} {mode === "game" && ( {selectedGame ? ( {selectedCoverUrl && ( )} {selectedGame.name} ) : Platform.OS === "web" ? ( {showResults && dropdownPos && // kind of hacky, but using a portal to avoid z-index and overflow issues vs // rendering the dropdown absolutely within the container require("react-dom").createPortal( {resultsList} , document.body, )} ) : ( Search for a game... {searching && ( Searching... )} {results.map((game) => ( selectGame(game)} > {game.name} ))} )} )} {mode === "label" && ( {ACTIVITY_LABELS.map(({ value: labelValue, display }) => { const selected = selectedLabel?.label === labelValue; return ( onChange( selected ? undefined : { $type: "place.stream.defs#activityLabel", label: labelValue, }, ) } style={[ px[3], borders.width.thin, { paddingVertical: 6, borderRadius: 16 }, { borderColor: selected ? c.primary : c.border }, { backgroundColor: selected ? c.primary : "transparent" }, ]} > {display} ); })} )} ); }