@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
JavaScript
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