/** * Skeleton.tsx * * A lightweight, theme-aware shimmer “skeleton” placeholder for * CometChat conversation lists. * –––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– * • 20 rows (top + bottom layers) drawn with react-native-svg. * • Two Animated views slide across on a loop to create * the shimmer illusion. * • Colours, speed, opacity and gradient come from the active * CometChatTheme but can be overridden per-instance via `style`. */ import React, { useEffect, useRef } from "react"; import { Animated, Dimensions, Easing, ScrollView, StyleSheet, View } from "react-native"; import Svg, { Defs, G, LinearGradient, Path, Stop } from "react-native-svg"; import { useTheme } from "../theme"; import { CometChatTheme } from "../theme/type"; // ─────────────────────────────────────────────────────────────── // Consts & Types // ─────────────────────────────────────────────────────────────── const { width: screenWidth } = Dimensions.get("window"); const SKELETON_ROW_COUNT = 20; // How many rows to render type SkeletonStyle = CometChatTheme["conversationStyles"]["skeletonStyle"]; interface SkeletonProps { style?: SkeletonStyle; // Optional per-instance overrides } // ─────────────────────────────────────────────────────────────── // Reusable SVG snippets // ─────────────────────────────────────────────────────────────── /** * Bottom SVG layer – pure grey rectangles that will be *under* the shimmer. */ const SkeletonItemBottom: React.FC = ({ style }) => { const theme = useTheme(); return ( {/* Gradient definition */} ); }; /** * Top SVG “cookie-cutter” layer – punched out shapes that mask out the * shimmer where content will eventually appear. */ const SkeletonItemTop: React.FC = ({ style }) => { const theme = useTheme(); return ( {/* The giant path draws avatar circle + text bars */} ); }; // ─────────────────────────────────────────────────────────────── // Main Component // ─────────────────────────────────────────────────────────────── export const Skeleton: React.FC = ({ style }) => { const theme = useTheme(); const animatedValue = useRef(new Animated.Value(0)).current; /* Kick-off the shimmer animation once on mount */ useEffect(() => { animatedValue.setValue(0); Animated.loop( Animated.timing(animatedValue, { toValue: 1, duration: (1 / (style?.speed || theme.conversationStyles.skeletonStyle.speed)) * 1000, easing: Easing.linear, useNativeDriver: false, // translateX is layout-related }) ).start(); }, [animatedValue, style?.speed || theme.conversationStyles.skeletonStyle.speed]); /* Map 0-1 → off-screen-left → off-screen-right */ const shimmerTranslateX = animatedValue.interpolate({ inputRange: [0, 1], outputRange: [-screenWidth * 2, screenWidth], }); /* Render ----------------------------------------------------- */ return ( {/* Bottom layer rectangles */} {Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => ( ))} {/* Two angled shimmer bars */} {[0, screenWidth / 2].map((offset, i) => ( ))} {/* Top mask layer – sits above shimmer to carve out shapes */} {Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => ( ))} ); }; // ─────────────────────────────────────────────────────────────── // Styles // ─────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ animatedView: { width: "25%", // narrow bar looks nicer at an angle top: 0, bottom: 0, position: "absolute", }, });