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