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 type { ColorValue } from "react-native"; import { CometChatTheme } from "../theme/type"; /** * Animated skeleton placeholder mimicking a *user list* in CometChat UI‑Kit. * * The component respects the defaults provided by `theme.userStyles.skeletonStyle`, * but every visual property can be **overridden per instance** using the * `style` prop. * * @example * ```tsx * * ``` */ export interface SkeletonProps { /** Partial style overrides (theme fallback for omitted keys). */ style?: Partial; } /** Alias for the skeleton style slice inside the theme. */ type SkeletonStyle = CometChatTheme["userStyles"]["skeletonStyle"]; // ────────────────────────────────────────────────────────────────────────────── // Utility helpers // ────────────────────────────────────────────────────────────────────────────── function getStyleValue( key: K, overrides: Partial | undefined, theme: CometChatTheme ): NonNullable { // Guaranteed fallback to theme defaults; cast is safe as UI‑Kit defines them. return ((overrides?.[key] as SkeletonStyle[K]) ?? theme.userStyles.skeletonStyle[key]) as NonNullable; } // ────────────────────────────────────────────────────────────────────────────── // 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_SPACING = 54; // gap between items (includes avatar & text) const LIST_ITEM_COUNT = 14; /** Total height required for the SVG canvas */ const TOTAL_HEIGHT = PADDING + LIST_ITEM_COUNT * (LIST_ITEM_HEIGHT + LIST_ITEM_SPACING); // ────────────────────────────────────────────────────────────────────────────── // SVG rows factory (memoized for perf) // ────────────────────────────────────────────────────────────────────────────── const useRows = (fill: ColorValue) => useMemo( () => Array.from({ length: LIST_ITEM_COUNT }).map((_, index) => { const y = PADDING + index * (LIST_ITEM_HEIGHT + LIST_ITEM_SPACING) - 20; return ( // eslint-disable-next-line react/no-array-index-key ); }), [fill] ); // ────────────────────────────────────────────────────────────────────────────── // Component Implementation // ────────────────────────────────────────────────────────────────────────────── export const Skeleton: React.FC = ({ style }) => { const theme = useTheme(); const get = (key: K) => getStyleValue(key, style, theme); // Shimmer animation ------------------------------------------ const translate = useRef(new Animated.Value(0)).current; useEffect(() => { const speed = get("speed"); const duration = 1000 / speed; const loop = Animated.loop( Animated.timing(translate, { toValue: 1, duration, easing: Easing.linear, useNativeDriver: false, // SVG not yet compatible with native driver }) ); loop.start(); return () => loop.stop(); }, [get("speed"), translate]); const translateX = translate.interpolate({ inputRange: [0, 1], outputRange: [-SCREEN_WIDTH * 2, SCREEN_WIDTH], }); // Pre‑build row shapes (bottom gradient + top mask) const rowsGradient = useRows("url(#gradient)"); const rowsSolid = useRows(String(get("backgroundColor"))); return ( {/* Bottom layer (gradient fill) */} {(() => { const colors = get("linearGradientColors"); return [ , , ]; })()} {rowsGradient} {/* Moving shimmer highlight (rendered twice for coverage) */} {[0, SCREEN_WIDTH / 2].map((offset) => ( // eslint-disable-next-line react/no-array-index-key ))} {/* Top mask – solid shapes clip the shimmer to list items */} {rowsSolid} ); }; // ────────────────────────────────────────────────────────────────────────────── // Styles // ────────────────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ shimmer: { position: "absolute", width: "25%", // thin bar for highlight top: 0, bottom: 0, }, });