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"; import { CometChatTheme } from "../theme/type"; /** * React‑Native skeleton list with animated shimmer. * * @remarks * The component respects the brand/theme defaults defined in * `theme.groupStyles.skeletonStyle` but allows **per‑instance overrides** via the * `style` prop. * * @example * ```tsx * * ``` */ export interface SkeletonProps { /** * Partial style overrides. Any omitted property falls back to the theme * default. */ style?: Partial; } // ────────────────────────────────────────────────────────────────────────────── // Theme typing helpers // ────────────────────────────────────────────────────────────────────────────── /** Convenience alias for the skeleton style slice in the theme object. */ type SkeletonStyle = CometChatTheme["groupStyles"]["skeletonStyle"]; /** * 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: K, overrides: Partial | undefined, theme: CometChatTheme ): SkeletonStyle[K] { return (overrides?.[key] as SkeletonStyle[K]) ?? 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: string) => 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 ( ); }), [fill] ); // ────────────────────────────────────────────────────────────────────────────── // Component implementation // ────────────────────────────────────────────────────────────────────────────── export const Skeleton: React.FC = ({ style }) => { const theme = useTheme(); /** Resolved style helpers (with theme‑fallback). */ const get = (key: K) => 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") as string); // ────────────────────────────────────────────────────────────────────────── // Render // ────────────────────────────────────────────────────────────────────────── // Note: Providing a backgroundColor override will be used for the top mask layer, // which may hide the gradient effect defined by linearGradientColors. return ( {/* Bottom gradient layer */} {/* Reusable linear gradient */} {rowShapesBottom} {/* Shimmer highlight (runs twice for smoother effect) */} {[0, SCREEN_WIDTH / 2].map((offset) => ( ))} {/* Top solid layer (masks shimmer to placeholder shapes) */} {rowShapesTop} ); }; // ────────────────────────────────────────────────────────────────────────────── // Styles // ────────────────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ shimmer: { position: "absolute", width: "25%", // Narrow highlight bar top: 0, bottom: 0, }, });