import React, { useState, useEffect, useRef, useMemo } from "react"; import { TextProps, Text, TextStyle, View, ViewProps } from "react-native"; import Animated, { useSharedValue, useAnimatedStyle, withTiming, withSequence, withDelay, Easing, } from "react-native-reanimated"; import { themeColors, ThemeName } from "../constants/Colors"; import useTheme from "../hooks/useTheme"; type Props = TextProps & { text?: string; speed?: number; onComplete?: () => void; themeScheme?: 'light' | 'dark'; variantColor?: 'default' | 'primary' | 'secondary' | ThemeName; variant?: | 'H1' | 'H2' | 'H3' | 'H4' | 'H5' | 'H6' | 'subtitle1' | 'subtitle2' | 'body1' | 'body2' | 'button' | 'caption' | 'overline'; bold?: TextStyle['fontWeight']; size?: TextStyle['fontSize']; center?: boolean; containerStyle?: ViewProps }; const TextLoader = ({ text = "Generating response...", speed = 30, onComplete, containerStyle, variantColor = "default", variant = "body1", center, themeScheme, bold, size, style, ...otherProps }: Props) => { const [displayedText, setDisplayedText] = useState(""); const textRef = useRef(""); const dotOpacity = useSharedValue(0); const { currentTheme, themeScheme: defaultThemeScheme } = useTheme(); const colorStyle = useMemo(() => { if (variantColor === 'default') { return { color: currentTheme.foreground }; } if (variantColor === 'secondary') { return { color: currentTheme.muted_foreground }; } if (variantColor === 'primary') { return { color: currentTheme.primary }; } const theme = themeColors.find((t) => t.name === variantColor)?.[themeScheme ?? defaultThemeScheme]; return { color: theme?.primary || currentTheme.foreground }; }, [currentTheme, themeScheme, defaultThemeScheme, variantColor]); const textVariantStyle = useMemo(() => { const variants: Record, TextStyle> = { H1: { fontSize: 96, fontWeight: '100', letterSpacing: -1.5 }, H2: { fontSize: 60, fontWeight: '100', letterSpacing: -0.5 }, H3: { fontSize: 48, fontWeight: '400' }, H4: { fontSize: 34, fontWeight: '400', letterSpacing: 0.25 }, H5: { fontSize: 24, fontWeight: '400' }, H6: { fontSize: 20, fontWeight: '500', letterSpacing: 0.15 }, subtitle1: { fontSize: 16, fontWeight: '400', letterSpacing: 0.15 }, subtitle2: { fontSize: 14, fontWeight: '500', letterSpacing: 0.1 }, body1: { fontSize: 16, fontWeight: '400', letterSpacing: 0.5 }, body2: { fontSize: 14, fontWeight: '400', letterSpacing: 0.25 }, button: { fontSize: 14, fontWeight: '500', letterSpacing: 1.25 }, caption: { fontSize: 12, fontWeight: '400', letterSpacing: 0.4 }, overline: { fontSize: 10, fontWeight: '400', letterSpacing: 1.5 }, }; return variants[variant ?? 'body1'] || { fontSize: 16, fontWeight: '400' }; }, [variant]); const combinedStyles = useMemo(() => { return [ { textAlign: center ? 'center' as 'center' : 'auto' as 'auto', opacity: otherProps?.disabled ? 0.5 : 1 }, colorStyle, textVariantStyle, bold ? { fontWeight: bold } : {}, size ? { fontSize: size } : {}, style, ]; }, [center, otherProps?.disabled, colorStyle, textVariantStyle, bold, size, style]); useEffect(() => { setDisplayedText(""); // Reset text textRef.current = ""; // Reset reference let index = 0; const interval = setInterval(() => { if (index < text.length) { textRef.current += text[index]; // Update ref first setDisplayedText(textRef.current); // Use ref for accurate updates index++; } else { clearInterval(interval); onComplete?.(); } }, speed); return () => clearInterval(interval); }, [text, speed, onComplete]); useEffect(() => { dotOpacity.value = withSequence( withTiming(1, { duration: 400, easing: Easing.inOut(Easing.ease) }), withTiming(0.3, { duration: 400, easing: Easing.inOut(Easing.ease) }), withTiming(1, { duration: 400, easing: Easing.inOut(Easing.ease) }), withDelay(500, withTiming(0, { duration: 200 })) ); }, []); const dotStyle = useAnimatedStyle(() => ({ opacity: dotOpacity.value, })); return ( {displayedText} {"..."} ); }; export default TextLoader;