UNPKG

@cometchat/chat-uikit-react-native

Version:

Ready-to-use Chat UI Components for React Native

131 lines (129 loc) 7.96 kB
import React, { useEffect, useMemo, useRef } from "react"; import { Animated, Dimensions, Easing, ScrollView, StyleSheet, View, } from "react-native"; import Svg, { Circle, Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { useTheme } from "../theme"; /** * Utility to resolve a style value with **theme‑fallback**. * * @param key – The style property being resolved. * @param overrides – Optional user overrides (`props.style`). * @param theme – Current theme object. * @returns The resolved style value. */ function resolveStyleValue(key, overrides, theme) { return overrides?.[key] ?? theme.groupStyles.skeletonStyle[key]; } // ────────────────────────────────────────────────────────────────────────────── // Layout constants – tweak here if the design changes // ────────────────────────────────────────────────────────────────────────────── const { width: SCREEN_WIDTH } = Dimensions.get("window"); const PADDING = 20; const AVATAR_RADIUS = 25; const LIST_ITEM_HEIGHT = 25; const LIST_ITEM_SUBTITLE_HEIGHT = 20; const LIST_ITEM_SUBTITLE_SPACING = 10; const LIST_ITEM_SPACING = 30; const LIST_ITEM_COUNT = 14; /** Total SVG height required to render all placeholder rows. */ const TOTAL_HEIGHT = PADDING + LIST_ITEM_COUNT * (LIST_ITEM_HEIGHT + LIST_ITEM_SUBTITLE_SPACING + LIST_ITEM_SUBTITLE_HEIGHT + LIST_ITEM_SPACING); // ────────────────────────────────────────────────────────────────────────────── // SVG building blocks // ────────────────────────────────────────────────────────────────────────────── /** * Generates the repetitive row shapes used by both bottom and top layers. * * @param fill – The fill used for rectangles & circles in this layer. */ const useRowShapes = (fill) => useMemo(() => Array.from({ length: LIST_ITEM_COUNT }).map((_, index) => { const baseY = PADDING + index * (LIST_ITEM_HEIGHT + LIST_ITEM_SUBTITLE_SPACING + LIST_ITEM_SUBTITLE_HEIGHT + LIST_ITEM_SPACING) - 10; return (<React.Fragment key={index}> <Circle cx={PADDING + AVATAR_RADIUS} cy={baseY + AVATAR_RADIUS} r={AVATAR_RADIUS} fill={fill}/> <Rect x={PADDING + 2 * AVATAR_RADIUS + 12} y={baseY} width={SCREEN_WIDTH - (PADDING + 2 * AVATAR_RADIUS + 12 + PADDING)} height={LIST_ITEM_HEIGHT} rx={LIST_ITEM_HEIGHT / 2} fill={fill}/> <Rect x={PADDING + 2 * AVATAR_RADIUS + 12} y={baseY + LIST_ITEM_HEIGHT + LIST_ITEM_SUBTITLE_SPACING} width={(SCREEN_WIDTH - (PADDING + 2 * AVATAR_RADIUS + 12 + PADDING)) * 0.6 /* 60% width */} height={LIST_ITEM_SUBTITLE_HEIGHT} rx={LIST_ITEM_SUBTITLE_HEIGHT / 2} fill={fill}/> </React.Fragment>); }), [fill]); // ────────────────────────────────────────────────────────────────────────────── // Component implementation // ────────────────────────────────────────────────────────────────────────────── export const Skeleton = ({ style }) => { const theme = useTheme(); /** Resolved style helpers (with theme‑fallback). */ const get = (key) => resolveStyleValue(key, style, theme); // Animated shimmer setup ----------------------------------------------------- const shimmerTranslate = useRef(new Animated.Value(0)).current; useEffect(() => { const duration = 1000 / get("speed"); const loop = Animated.loop(Animated.timing(shimmerTranslate, { toValue: 1, duration, easing: Easing.linear, useNativeDriver: false, // SVG cannot use native driver })); loop.start(); return () => loop.stop(); // <‑‑ Prevent memory leaks on unmount }, [get("speed"), shimmerTranslate]); // Interpolated translation across the screen width const translateX = shimmerTranslate.interpolate({ inputRange: [0, 1], outputRange: [-SCREEN_WIDTH * 2, SCREEN_WIDTH], }); // SVG layers --------------------------------------------------------------- const rowShapesBottom = useRowShapes("url(#gradient)" /* gradient fill */); const rowShapesTop = useRowShapes(get("backgroundColor")); // ────────────────────────────────────────────────────────────────────────── // Render // ────────────────────────────────────────────────────────────────────────── // Note: Providing a backgroundColor override will be used for the top mask layer, // which may hide the gradient effect defined by linearGradientColors. return (<ScrollView scrollEnabled={false} showsVerticalScrollIndicator={false} style={{ backgroundColor: get("containerBackgroundColor") }}> {/* Bottom gradient layer */} <Svg height={TOTAL_HEIGHT} width={SCREEN_WIDTH} fill="none" preserveAspectRatio="xMidYMid meet"> {/* Reusable linear gradient */} <Defs> <LinearGradient id="gradient" x1="0" y1="0" x2={SCREEN_WIDTH} y2="0" gradientUnits="userSpaceOnUse"> <Stop stopColor={get("linearGradientColors")[0]}/> <Stop offset="1" stopColor={get("linearGradientColors")[1]}/> </LinearGradient> </Defs> {rowShapesBottom} </Svg> {/* Shimmer highlight (runs twice for smoother effect) */} {[0, SCREEN_WIDTH / 2].map((offset) => (<Animated.View // eslintdisablenextline react/no-array-index-key key={offset} style={[ styles.shimmer, { transform: [ { translateX: Animated.add(translateX, offset) }, { translateY: -20 }, { rotate: "15deg" }, ], backgroundColor: get("shimmerBackgroundColor"), opacity: get("shimmerOpacity"), }, ]}/>))} {/* Top solid layer (masks shimmer to placeholder shapes) */} <View style={StyleSheet.absoluteFill} pointerEvents="none"> <Svg height={TOTAL_HEIGHT} width={SCREEN_WIDTH} fill="none" preserveAspectRatio="xMidYMid meet"> {rowShapesTop} </Svg> </View> </ScrollView>); }; // ────────────────────────────────────────────────────────────────────────────── // Styles // ────────────────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ shimmer: { position: "absolute", width: "25%", top: 0, bottom: 0, }, }); //# sourceMappingURL=Skeleton.js.map