UNPKG

@replyke/ui-core-react-native

Version:

Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.

226 lines 11 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useEffect, useCallback, useRef } from "react"; import { View, Text, TextInput, TouchableOpacity, Animated, TouchableWithoutFeedback, Keyboard, } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; import { MagnifyingGlassIcon } from "../icons"; import { getImageComponent } from "../helpers/getImageComponent"; const MAX_ITEMS = 60; // Define the maximum number of items to load const FETCH_LIMIT = 30; export default function GiphyContainer({ giphyApiKey, onClickBack, onSelectGif, visible, }) { // Dynamically get the correct Image component and whether it is expo-image. const { ImageComponent, isExpo } = getImageComponent(); const [gifs, setGifs] = useState([]); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [currentOffset, setCurrentOffset] = useState(0); const [totalCount, setTotalCount] = useState(0); const [flatListWidth, setFlatListWidth] = useState(0); // Track the width of the FlatList // Search states & debouncing const [query, setQuery] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); // Refs to track fetching state and gifs length const isFetchingRef = useRef(false); const gifsLengthRef = useRef(0); // Debounce effect (like in the web version) useEffect(() => { const handler = setTimeout(() => { setDebouncedQuery(query); }, 500); // 500ms debounce return () => clearTimeout(handler); }, [query]); // Update gifsLengthRef when gifs change useEffect(() => { gifsLengthRef.current = gifs.length; }, [gifs]); // Fetch function that decides between trending or search const fetchGifs = useCallback(async (offset = 0) => { // Prevent multiple fetches if (isFetchingRef.current) { return; } // Check if we've reached the maximum limit if (Math.max(gifsLengthRef.current, offset) >= MAX_ITEMS) { return; } isFetchingRef.current = true; if (offset === 0) { setLoading(true); setLoadingMore(false); } else { setLoadingMore(true); } let url = ""; const trimmed = debouncedQuery.trim(); if (trimmed.length === 0) { // Fetch trending if no query url = `https://api.giphy.com/v1/gifs/trending?api_key=${giphyApiKey}&limit=${FETCH_LIMIT}&offset=${offset}`; } else { // Fetch search results url = `https://api.giphy.com/v1/gifs/search?api_key=${giphyApiKey}&q=${encodeURIComponent(trimmed)}&limit=${FETCH_LIMIT}&offset=${offset}`; } try { const res = await fetch(url); const json = (await res.json()); if (json.meta.status !== 200) { console.error("API Error:", json.meta.msg); return; } setTotalCount(json.pagination.total_count); setCurrentOffset(json.pagination.offset + json.pagination.count); // Calculate how many items we can still fetch const remainingItems = MAX_ITEMS - gifsLengthRef.current; const fetchedItems = json.data.slice(0, remainingItems); if (offset === 0) { setGifs(fetchedItems); } else { setGifs((prevGifs) => [...prevGifs, ...fetchedItems]); } } catch (err) { console.error("Fetch Error:", err); } finally { if (offset === 0) { setLoading(false); } else { setLoadingMore(false); } isFetchingRef.current = false; } }, [debouncedQuery, giphyApiKey]); // Fetch more GIFs when reaching the end const fetchMoreGifs = useCallback(() => { if (loading || loadingMore || gifsLengthRef.current >= totalCount || gifsLengthRef.current >= MAX_ITEMS || isFetchingRef.current) { return; } fetchGifs(currentOffset); }, [loading, loadingMore, totalCount, currentOffset, fetchGifs]); // Reset search and pagination when visibility or query changes useEffect(() => { if (!visible) { // Reset all states when not visible setQuery(""); setDebouncedQuery(""); setGifs([]); setCurrentOffset(0); setTotalCount(0); setLoading(false); setLoadingMore(false); isFetchingRef.current = false; } else { // When visibility changes to true, reset offset and fetch initial gifs setCurrentOffset(0); setTotalCount(0); setGifs([]); fetchGifs(0); } }, [visible, debouncedQuery, fetchGifs]); // Handle ScrollView layout to dynamically calculate column width const handleLayout = (event) => { const { width } = event.nativeEvent.layout; setFlatListWidth(width); }; const renderMasonryColumns = () => { if (!flatListWidth) return null; const padding = 16; // Total horizontal padding const columnSpacing = 8; // Spacing between columns const columnWidth = (flatListWidth - padding - columnSpacing) / 2; // Two columns // Split data into two columns const columns = [[], []]; gifs.forEach((gif, index) => { columns[index % 2].push(gif); // Alternately add to each column }); return (_jsx(View, { style: { flexDirection: "row", justifyContent: "space-between", paddingHorizontal: 4, }, children: columns.map((column, colIndex) => (_jsx(View, { style: { flex: 1, marginHorizontal: 4 }, children: column.map((item) => { const aspectRatio = parseInt(item.images.fixed_width.height) / parseInt(item.images.fixed_width.width); const imageStyle = { width: columnWidth, height: columnWidth * aspectRatio, borderRadius: 4, }; // Build the props based on which Image component is being used. // For expo-image, we assume it accepts a string for its "source" // and additional props like "contentFit" and "transition". // For React Native's Image, we wrap the URL in an object with "uri". const imageProps = isExpo ? { source: item.images.fixed_width.webp, // expo-image accepts a string style: imageStyle, contentFit: "cover", transition: 500, } : { source: { uri: item.images.fixed_width.webp }, // React Native expects an object with a "uri" property style: imageStyle, }; return (_jsx(TouchableOpacity, { style: { marginBottom: 8 }, onPress: () => { Keyboard.dismiss(); // Dismiss the keyboard onSelectGif({ id: item.id, url: item.url, aspectRatio, gifUrl: item.images.fixed_width.webp, gifPreviewUrl: item.images.preview_gif.webp, altText: item.title, }); }, children: _jsx(ImageComponent, { ...imageProps }) }, item.id)); }) }, colIndex))) })); }; return (_jsx(TouchableWithoutFeedback, { onPress: Keyboard.dismiss, accessible: false, children: _jsxs(Animated.View, { style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "white", zIndex: 999, opacity: visible ? 1 : 0, pointerEvents: visible ? "auto" : "none", }, children: [_jsxs(View, { style: { flexDirection: "row", padding: 8, alignItems: "stretch", gap: 8, }, children: [_jsx(TouchableOpacity, { style: { backgroundColor: "#e5e7eb", aspectRatio: 1, // Ensures width equals height alignItems: "center", justifyContent: "center", borderRadius: 8, // Keeps it rounded }, onPress: onClickBack, children: _jsx(Text, { style: { color: "#888", fontSize: 22, lineHeight: 22 }, children: "\u2190" }) }), _jsxs(View, { style: { flex: 1, flexDirection: "row", backgroundColor: "#e5e7eb", borderRadius: 8, paddingHorizontal: 16, alignItems: "center", gap: 12, }, children: [_jsx(MagnifyingGlassIcon, { width: 16, color: "#888" }), _jsx(TextInput, { style: { flex: 1, paddingVertical: 12, fontSize: 15, }, placeholder: "Search GIPHY", onChangeText: (value) => setQuery(value), value: query })] })] }), loading && gifs.length === 0 ? (_jsx(Text, { style: { textAlign: "center", marginTop: 16 }, children: "Loading..." })) : (_jsxs(ScrollView, { onLayout: handleLayout, onScroll: ({ nativeEvent }) => { const { layoutMeasurement, contentOffset, contentSize } = nativeEvent; const currentScroll = contentOffset.y + layoutMeasurement.height; const threshold = contentSize.height * 0.8; // 80% scroll if (currentScroll >= threshold) { fetchMoreGifs(); } }, keyboardShouldPersistTaps: "handled" // Important to allow tapping through the keyboard , scrollEventThrottle: 16, children: [renderMasonryColumns(), loadingMore && (_jsx(Text, { style: { textAlign: "center", marginVertical: 16 }, children: "Loading more..." }))] }))] }) })); } //# sourceMappingURL=GiphyContainer.js.map