1import { Code } from '@expo/html-elements'; 2import React, { PropsWithChildren, useCallback, useState, useRef, useEffect } from 'react'; 3import { 4 View, 5 StyleSheet, 6 TouchableOpacity, 7 Text, 8 Animated, 9 StyleProp, 10 ViewStyle, 11} from 'react-native'; 12 13import Colors from '../constants/Colors'; 14 15type Props = PropsWithChildren<{ 16 /** 17 * Countdown timeout. 18 */ 19 timeout?: number; 20 21 /** 22 * Called when the countdown ended or close button is pressed. 23 */ 24 onCountdownEnded: () => void; 25 26 style?: StyleProp<ViewStyle>; 27}>; 28 29function MonoTextWithCountdown({ style, children, timeout = 8000, onCountdownEnded }: Props) { 30 const animatedValue = useRef(new Animated.Value(1)).current; 31 const [countdownInterrupted, setCountdownInterrupted] = useState(false); 32 const [valueUponPause, setValueUponPause] = useState(1); 33 34 useEffect(() => { 35 if (countdownInterrupted) { 36 animatedValue.stopAnimation((value) => { 37 setValueUponPause(value); 38 }); 39 } else { 40 Animated.timing(animatedValue, { 41 toValue: 0, 42 duration: valueUponPause * timeout, 43 useNativeDriver: false, 44 }).start(({ finished }) => { 45 if (finished) { 46 onCountdownEnded(); 47 } 48 }); 49 } 50 }, [countdownInterrupted, valueUponPause, onCountdownEnded]); 51 52 const triggerCountdownEnd = useCallback(() => { 53 onCountdownEnded(); 54 }, [onCountdownEnded]); 55 const toggleCountdown = useCallback(() => { 56 setCountdownInterrupted((previousValue) => !previousValue); 57 }, [countdownInterrupted]); 58 59 return ( 60 <View style={[styles.container, style]}> 61 <Code style={styles.monoText}>{children}</Code> 62 <View style={styles.buttonsContainer}> 63 <IconButton icon={countdownInterrupted ? '▶️' : '⏸'} onPress={toggleCountdown} /> 64 <IconButton icon="❌" onPress={triggerCountdownEnd} /> 65 </View> 66 <CountdownBar width={animatedValue} /> 67 </View> 68 ); 69} 70 71const styles = StyleSheet.create({ 72 container: { 73 borderWidth: 2, 74 borderColor: '#00AA00', 75 backgroundColor: '#fff', 76 }, 77 78 monoText: { 79 fontSize: 10, 80 padding: 6, 81 }, 82 83 countdownBar: { 84 height: 3, 85 backgroundColor: Colors.tintColor, 86 position: 'absolute', 87 top: 0, 88 left: 0, 89 right: 0, 90 }, 91 92 buttonsContainer: { 93 position: 'absolute', 94 top: 0, 95 right: 0, 96 flexDirection: 'row', 97 }, 98 buttonIcon: { 99 paddingVertical: 5, 100 paddingHorizontal: 3, 101 }, 102}); 103 104type IconButtonProps = { 105 icon: string; 106 onPress: () => void; 107}; 108 109function IconButton({ icon, onPress }: IconButtonProps) { 110 return ( 111 <TouchableOpacity onPress={onPress}> 112 <Text style={styles.buttonIcon}>{icon}</Text> 113 </TouchableOpacity> 114 ); 115} 116 117type CountdownBarProps = { 118 width: Animated.Value; 119}; 120 121function CountdownBar({ width }: CountdownBarProps) { 122 return ( 123 <Animated.View 124 style={[ 125 styles.countdownBar, 126 { 127 width: width.interpolate({ 128 inputRange: [0, 1], 129 outputRange: ['0%', '100%'], 130 }), 131 }, 132 ]} 133 /> 134 ); 135} 136 137export default MonoTextWithCountdown; 138