UNPKG

@replyke/ui-core-react-native

Version:

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

229 lines 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = GiphyContainer; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const react_native_1 = require("react-native"); const react_native_gesture_handler_1 = require("react-native-gesture-handler"); const icons_1 = require("../icons"); const getImageComponent_1 = require("../helpers/getImageComponent"); const MAX_ITEMS = 60; // Define the maximum number of items to load const FETCH_LIMIT = 30; function GiphyContainer({ giphyApiKey, onClickBack, onSelectGif, visible, }) { // Dynamically get the correct Image component and whether it is expo-image. const { ImageComponent, isExpo } = (0, getImageComponent_1.getImageComponent)(); const [gifs, setGifs] = (0, react_1.useState)([]); const [loading, setLoading] = (0, react_1.useState)(false); const [loadingMore, setLoadingMore] = (0, react_1.useState)(false); const [currentOffset, setCurrentOffset] = (0, react_1.useState)(0); const [totalCount, setTotalCount] = (0, react_1.useState)(0); const [flatListWidth, setFlatListWidth] = (0, react_1.useState)(0); // Track the width of the FlatList // Search states & debouncing const [query, setQuery] = (0, react_1.useState)(""); const [debouncedQuery, setDebouncedQuery] = (0, react_1.useState)(""); // Refs to track fetching state and gifs length const isFetchingRef = (0, react_1.useRef)(false); const gifsLengthRef = (0, react_1.useRef)(0); // Debounce effect (like in the web version) (0, react_1.useEffect)(() => { const handler = setTimeout(() => { setDebouncedQuery(query); }, 500); // 500ms debounce return () => clearTimeout(handler); }, [query]); // Update gifsLengthRef when gifs change (0, react_1.useEffect)(() => { gifsLengthRef.current = gifs.length; }, [gifs]); // Fetch function that decides between trending or search const fetchGifs = (0, react_1.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 = (0, react_1.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 (0, react_1.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 ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: "row", justifyContent: "space-between", paddingHorizontal: 4, }, children: columns.map((column, colIndex) => ((0, jsx_runtime_1.jsx)(react_native_1.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 ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: { marginBottom: 8 }, onPress: () => { react_native_1.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: (0, jsx_runtime_1.jsx)(ImageComponent, { ...imageProps }) }, item.id)); }) }, colIndex))) })); }; return ((0, jsx_runtime_1.jsx)(react_native_1.TouchableWithoutFeedback, { onPress: react_native_1.Keyboard.dismiss, accessible: false, children: (0, jsx_runtime_1.jsxs)(react_native_1.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: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexDirection: "row", padding: 8, alignItems: "stretch", gap: 8, }, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: { backgroundColor: "#e5e7eb", aspectRatio: 1, // Ensures width equals height alignItems: "center", justifyContent: "center", borderRadius: 8, // Keeps it rounded }, onPress: onClickBack, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#888", fontSize: 22, lineHeight: 22 }, children: "\u2190" }) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flex: 1, flexDirection: "row", backgroundColor: "#e5e7eb", borderRadius: 8, paddingHorizontal: 16, alignItems: "center", gap: 12, }, children: [(0, jsx_runtime_1.jsx)(icons_1.MagnifyingGlassIcon, { width: 16, color: "#888" }), (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { style: { flex: 1, paddingVertical: 12, fontSize: 15, }, placeholder: "Search GIPHY", onChangeText: (value) => setQuery(value), value: query })] })] }), loading && gifs.length === 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { textAlign: "center", marginTop: 16 }, children: "Loading..." })) : ((0, jsx_runtime_1.jsxs)(react_native_gesture_handler_1.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 && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { textAlign: "center", marginVertical: 16 }, children: "Loading more..." }))] }))] }) })); } //# sourceMappingURL=GiphyContainer.js.map