├── .gitignore ├── README.md ├── app.json ├── app ├── _layout.tsx └── index.tsx ├── assets ├── fonts │ ├── Inter-Medium.ttf │ └── Inter-SemiBold.ttf └── images │ ├── adaptive-icon.png │ ├── dp.png │ ├── favicon.png │ ├── icon.png │ ├── partial-react-logo.png │ ├── react-logo.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ └── splash-icon.png ├── components ├── Swipable.tsx ├── ThemedText.tsx ├── ThemedView.tsx └── ui │ ├── Arrow.tsx │ ├── MessageBox.tsx │ └── RadialProgress.tsx ├── constants └── Colors.ts ├── hooks ├── useColorScheme.ts ├── useColorScheme.web.ts └── useThemeColor.ts ├── package-lock.json ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | *.orig.* 14 | *.jks 15 | *.p8 16 | *.p12 17 | *.key 18 | *.mobileprovision 19 | 20 | # Metro 21 | .metro-health-check* 22 | 23 | # debug 24 | npm-debug.* 25 | yarn-debug.* 26 | yarn-error.* 27 | 28 | # macOS 29 | .DS_Store 30 | *.pem 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | 38 | app-example 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Preview 2 | https://github.com/user-attachments/assets/2945c697-d358-4cf8-843d-a1df217fb650 3 | 4 | # Welcome to your Expo app 👋 5 | 6 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 7 | 8 | ## Get started 9 | 10 | 1. Install dependencies 11 | 12 | ```bash 13 | npm install 14 | ``` 15 | 16 | 2. Start the app 17 | 18 | ```bash 19 | npx expo start 20 | ``` 21 | 22 | In the output, you'll find options to open the app in a 23 | 24 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 25 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 26 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 27 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 28 | 29 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 30 | 31 | ## Get a fresh project 32 | 33 | When you're ready, run: 34 | 35 | ```bash 36 | npm run reset-project 37 | ``` 38 | 39 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 40 | 41 | ## Learn more 42 | 43 | To learn more about developing your project with Expo, look at the following resources: 44 | 45 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 46 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. 47 | 48 | ## Join the community 49 | 50 | Join our community of developers creating universal apps. 51 | 52 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 53 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. 54 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "wa-swipe-to-talk", 4 | "slug": "wa-swipe-to-talk", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true 13 | }, 14 | "android": { 15 | "adaptiveIcon": { 16 | "foregroundImage": "./assets/images/adaptive-icon.png", 17 | "backgroundColor": "#ffffff" 18 | } 19 | }, 20 | "web": { 21 | "bundler": "metro", 22 | "output": "static", 23 | "favicon": "./assets/images/dp.png" 24 | }, 25 | "plugins": [ 26 | "expo-router", 27 | [ 28 | "expo-splash-screen", 29 | { 30 | "image": "./assets/images/splash-icon.png", 31 | "imageWidth": 200, 32 | "resizeMode": "contain", 33 | "backgroundColor": "#ffffff" 34 | } 35 | ] 36 | ], 37 | "experiments": { 38 | "typedRoutes": true 39 | }, 40 | "extra": { 41 | "router": { 42 | "origin": false 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DarkTheme, 3 | DefaultTheme, 4 | ThemeProvider, 5 | } from "@react-navigation/native"; 6 | import { useFonts } from "expo-font"; 7 | import { Stack } from "expo-router"; 8 | import * as SplashScreen from "expo-splash-screen"; 9 | import { StatusBar } from "expo-status-bar"; 10 | import { useEffect } from "react"; 11 | import "react-native-reanimated"; 12 | import { useColorScheme } from "@/hooks/useColorScheme"; 13 | import { SafeAreaProvider } from "react-native-safe-area-context"; 14 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 15 | import Head from "expo-router/head"; 16 | // Prevent the splash screen from auto-hiding before asset loading is complete. 17 | SplashScreen.preventAutoHideAsync(); 18 | 19 | export default function RootLayout() { 20 | const colorScheme = useColorScheme(); 21 | const [loaded] = useFonts({ 22 | InterMedium: require("../assets/fonts/Inter-Medium.ttf"), 23 | InterSemiBold: require("../assets/fonts/Inter-SemiBold.ttf"), 24 | }); 25 | 26 | useEffect(() => { 27 | const configureSplashScreen = async () => { 28 | try { 29 | await SplashScreen.preventAutoHideAsync(); 30 | } catch (e) { 31 | console.warn("Error configuring splash screen:", e); 32 | } finally { 33 | if (loaded) { 34 | await SplashScreen.hideAsync(); 35 | } 36 | } 37 | }; 38 | 39 | configureSplashScreen(); 40 | }, [loaded]); 41 | 42 | if (!loaded) { 43 | return null; 44 | } 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet } from "react-native"; 3 | import { ThemedView } from "@/components/ThemedView"; 4 | import Swipable from "@/components/Swipable"; 5 | 6 | export default function Index() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | flex: 1, 17 | }, 18 | text: { 19 | fontSize: 24, 20 | fontWeight: "bold", 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /assets/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/dp.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Solarin-Johnson/wa-swipe-to-talk/9b962242199c5760f3845471be5a4a0cc87ed2de/assets/images/splash-icon.png -------------------------------------------------------------------------------- /components/Swipable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Platform, StyleSheet, View } from "react-native"; 3 | import { 4 | Gesture, 5 | GestureDetector, 6 | Pressable, 7 | } from "react-native-gesture-handler"; 8 | import Animated, { 9 | useSharedValue, 10 | useAnimatedStyle, 11 | withSpring, 12 | useDerivedValue, 13 | WithSpringConfig, 14 | withTiming, 15 | withRepeat, 16 | withSequence, 17 | runOnUI, 18 | } from "react-native-reanimated"; 19 | import MessageBox from "./ui/MessageBox"; 20 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 21 | import { FontAwesome6, Ionicons } from "@expo/vector-icons"; 22 | import { useThemeColor } from "@/hooks/useThemeColor"; 23 | import { RadialProgress } from "./ui/RadialProgress"; 24 | import { ThemedText } from "./ThemedText"; 25 | import { Image } from "expo-image"; 26 | import { ArrowUp } from "./ui/Arrow"; 27 | 28 | const MAX_DRAW_OFFSET = 150; 29 | const SPRING_CONFIG: WithSpringConfig = { 30 | damping: 18, 31 | mass: 1, 32 | stiffness: 180, 33 | overshootClamping: true, 34 | restDisplacementThreshold: 0.01, 35 | restSpeedThreshold: 2, 36 | }; 37 | const SPRING_CONFIG_SNAP: WithSpringConfig = { 38 | stiffness: 280, 39 | damping: 32, 40 | mass: 0.4, 41 | }; 42 | const CIRCULAR_WIDTH = 40; 43 | const CONNECT_WIDTH = 150; 44 | const CONNECT_LARGE_WIDTH = 280; 45 | const CONNECT_HEIGHT = 54; 46 | const CONNECT_LARGE_HEIGHT = 92; 47 | 48 | export default function Swipable() { 49 | const translateY = useSharedValue(0); 50 | const connected = useSharedValue(false); 51 | const text = useThemeColor({}, "text"); 52 | const textFade = useThemeColor({}, "textFade"); 53 | const error = useThemeColor({}, "error"); 54 | const { bottom, top } = useSafeAreaInsets(); 55 | const floatOffset = useSharedValue(0); 56 | 57 | const iconProps = { color: text }; 58 | const callBtnProps = { 59 | size: 27, 60 | ...iconProps, 61 | }; 62 | 63 | const progress = useDerivedValue(() => { 64 | return Math.abs(translateY.value) / MAX_DRAW_OFFSET; 65 | }); 66 | 67 | const maxxed = useDerivedValue(() => { 68 | return Math.abs(translateY.value) === MAX_DRAW_OFFSET; 69 | }); 70 | 71 | const radialProgress = useDerivedValue(() => { 72 | return maxxed.value ? withTiming(1, { duration: 1000 }) : 0; 73 | }); 74 | 75 | useEffect(() => { 76 | runOnUI(() => { 77 | floatOffset.value = withRepeat( 78 | withSequence( 79 | withTiming(-15, { duration: 1800 }), 80 | withTiming(0, { duration: 1800 }) 81 | ), 82 | -1, 83 | true 84 | ); 85 | })(); 86 | }, []); 87 | 88 | const countCompleted = useDerivedValue(() => { 89 | return radialProgress.value === 1; 90 | }); 91 | 92 | const panGesture = Gesture.Pan() 93 | .onUpdate((e) => { 94 | if (connected.value) return; 95 | translateY.value = Math.min( 96 | 0, 97 | Math.max(-MAX_DRAW_OFFSET, e.translationY) 98 | ); 99 | }) 100 | .onEnd(() => { 101 | translateY.value = countCompleted.value 102 | ? translateY.value 103 | : withSpring(0, SPRING_CONFIG); 104 | connected.value = countCompleted.value; 105 | }); 106 | 107 | const animatedStyle = useAnimatedStyle(() => ({ 108 | height: -translateY.value, 109 | })); 110 | 111 | const slideAnimatedStyle = useAnimatedStyle(() => ({ 112 | transform: [{ translateY: translateY.value }], 113 | })); 114 | 115 | const msgBoxAnimatedStyle = useAnimatedStyle(() => { 116 | const shouldAnimate = progress.value > 0.2 && !connected.value; 117 | return { 118 | transform: [ 119 | { 120 | translateY: withSpring(shouldAnimate ? 40 : 0, SPRING_CONFIG), 121 | }, 122 | ], 123 | opacity: withSpring(shouldAnimate ? 0 : 1, SPRING_CONFIG), 124 | }; 125 | }); 126 | 127 | const progressAnimatedStyle = useAnimatedStyle(() => { 128 | const prog = maxxed.value ? 1.2 : Math.min(1, progress.value * 2); 129 | return { 130 | transform: [ 131 | { 132 | scale: withSpring(prog, { 133 | ...(prog < 1 ? { duration: 0 } : SPRING_CONFIG), 134 | }), 135 | }, 136 | ], 137 | }; 138 | }); 139 | 140 | const arrowStyle = useAnimatedStyle(() => ({ 141 | opacity: withSpring(maxxed.value ? 0 : 1, SPRING_CONFIG), 142 | })); 143 | 144 | const bubbleStyle = useAnimatedStyle(() => ({ 145 | opacity: withSpring(maxxed.value ? 1 : 0, SPRING_CONFIG), 146 | })); 147 | 148 | const connectAnimatedStyle = useAnimatedStyle(() => { 149 | const scale = countCompleted.value ? 1 : 0; 150 | return { 151 | width: withSpring( 152 | connected.value ? CONNECT_LARGE_WIDTH : CONNECT_WIDTH, 153 | SPRING_CONFIG 154 | ), 155 | height: withSpring( 156 | connected.value ? CONNECT_LARGE_HEIGHT : CONNECT_HEIGHT, 157 | SPRING_CONFIG 158 | ), 159 | transform: [ 160 | { 161 | scale: withSpring(scale, SPRING_CONFIG), 162 | }, 163 | ], 164 | }; 165 | }); 166 | 167 | const connectInnerStyle = useAnimatedStyle(() => ({ 168 | opacity: withSpring(connected.value ? 0 : 1, SPRING_CONFIG_SNAP), 169 | transform: [ 170 | { 171 | translateY: withSpring( 172 | connected.value ? -CONNECT_HEIGHT / 4 : 0, 173 | SPRING_CONFIG_SNAP 174 | ), 175 | }, 176 | ], 177 | })); 178 | 179 | const callConnectStyle = useAnimatedStyle(() => ({ 180 | opacity: withSpring(connected.value ? 1 : 0, SPRING_CONFIG_SNAP), 181 | })); 182 | 183 | const floatAnimatedStyle = useAnimatedStyle(() => ({ 184 | transform: [{ translateY: floatOffset.value }], 185 | })); 186 | 187 | const helperAnimatedStyle1 = useAnimatedStyle(() => { 188 | const show = progress.value > 0.2 && !maxxed.value && !countCompleted.value; 189 | return { 190 | opacity: withSpring(show ? 1 : 0, SPRING_CONFIG), 191 | }; 192 | }); 193 | 194 | const helperAnimatedStyle2 = useAnimatedStyle(() => { 195 | const show = maxxed.value && !countCompleted.value; 196 | return { 197 | opacity: withSpring(show ? 1 : 0, SPRING_CONFIG), 198 | }; 199 | }); 200 | 201 | const helperAnimatedStyle3 = useAnimatedStyle(() => { 202 | const show = countCompleted.value; 203 | return { 204 | opacity: withSpring(show ? 1 : 0, SPRING_CONFIG), 205 | }; 206 | }); 207 | 208 | const onCLose = () => { 209 | translateY.value = withSpring(0, SPRING_CONFIG, () => { 210 | connected.value = false; 211 | }); 212 | }; 213 | 214 | return ( 215 | 216 | 217 | 218 | 225 | 226 | Drag Up! 227 | 228 | 229 | 230 | 242 | 249 | 250 | 251 | 252 | 253 | 258 | 259 | 263 | 264 | 271 | 272 | 277 | Connect 278 | 279 | 280 | 281 | 282 | 283 | 284 | 288 | 292 | Connecting... 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | Swipe up to talk 308 | 309 | 310 | 311 | 312 | Keep holding to talk 313 | 314 | 315 | 316 | 317 | Release to talk 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | ); 327 | } 328 | 329 | type ButtonProps = React.PropsWithChildren<{ 330 | onPress?: () => void; 331 | }>; 332 | 333 | function ActionButton({ onPress, children }: ButtonProps) { 334 | const text = useThemeColor({}, "text"); 335 | return ( 336 | 347 | {children} 348 | 349 | ); 350 | } 351 | 352 | const styles = StyleSheet.create({ 353 | swipable: { 354 | backgroundColor: "#80808020", 355 | padding: 20, 356 | paddingVertical: 42, 357 | borderRadius: 12, 358 | flex: 1, 359 | margin: 12, 360 | alignItems: "center", 361 | justifyContent: "flex-end", 362 | }, 363 | progressIndicator: { 364 | alignSelf: "center", 365 | width: CIRCULAR_WIDTH, 366 | alignItems: "center", 367 | justifyContent: "center", 368 | aspectRatio: 1, 369 | borderRadius: 50, 370 | }, 371 | icon: { 372 | position: "absolute", 373 | }, 374 | connect: { 375 | position: "absolute", 376 | borderRadius: CONNECT_HEIGHT / 2, 377 | alignItems: "center", 378 | overflow: "hidden", 379 | }, 380 | connectInner: { 381 | flexDirection: "row", 382 | gap: 8, 383 | justifyContent: "center", 384 | alignItems: "center", 385 | height: CONNECT_HEIGHT, 386 | width: CONNECT_WIDTH, 387 | position: "absolute", 388 | }, 389 | connectCall: { 390 | width: CONNECT_LARGE_WIDTH, 391 | height: CONNECT_LARGE_HEIGHT, 392 | padding: 8, 393 | flexDirection: "row", 394 | alignItems: "flex-start", 395 | }, 396 | helperContainer: { 397 | ...StyleSheet.absoluteFillObject, 398 | alignItems: "center", 399 | justifyContent: "center", 400 | }, 401 | helperText: { 402 | fontSize: 15, 403 | }, 404 | }); 405 | -------------------------------------------------------------------------------- /components/ThemedText.tsx: -------------------------------------------------------------------------------- 1 | import { Text, type TextProps, StyleSheet } from "react-native"; 2 | import { useThemeColor } from "@/hooks/useThemeColor"; 3 | 4 | export type ThemedTextProps = TextProps & { 5 | lightColor?: string; 6 | darkColor?: string; 7 | type?: "default" | "title" | "defaultSemiBold" | "subtitle" | "link"; 8 | }; 9 | 10 | export function ThemedText({ 11 | style, 12 | lightColor, 13 | darkColor, 14 | type = "default", 15 | ...rest 16 | }: ThemedTextProps) { 17 | const color = useThemeColor({ light: lightColor, dark: darkColor }, "text"); 18 | 19 | return ( 20 | 32 | ); 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | default: { 37 | fontSize: 16, 38 | }, 39 | defaultSemiBold: { 40 | fontSize: 17, 41 | fontFamily: "InterSemiBold", 42 | }, 43 | title: { 44 | fontFamily: "InterSemiBold", 45 | fontSize: 20, 46 | }, 47 | subtitle: { 48 | fontSize: 16, 49 | }, 50 | link: { 51 | lineHeight: 30, 52 | fontSize: 16, 53 | color: "#0a7ea4", 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /components/ThemedView.tsx: -------------------------------------------------------------------------------- 1 | import { View, type ViewProps } from "react-native"; 2 | 3 | import { useThemeColor } from "@/hooks/useThemeColor"; 4 | 5 | export type ThemedViewProps = ViewProps & { 6 | lightColor?: string; 7 | darkColor?: string; 8 | invert?: boolean; 9 | }; 10 | 11 | export function ThemedView({ 12 | style, 13 | lightColor, 14 | darkColor, 15 | invert, 16 | ...otherProps 17 | }: ThemedViewProps) { 18 | const backgroundColor = useThemeColor( 19 | { light: lightColor, dark: darkColor }, 20 | invert ? "text" : "background" 21 | ); 22 | 23 | return ; 24 | } 25 | -------------------------------------------------------------------------------- /components/ui/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import { useThemeColor } from "@/hooks/useThemeColor"; 2 | import { View, Text } from "react-native"; 3 | import Svg, { Path } from "react-native-svg"; 4 | 5 | export function ArrowUp() { 6 | const text = useThemeColor({}, "text"); 7 | return ( 8 | 13 | 14 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/ui/MessageBox.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StyleSheet, 3 | TextInput, 4 | View, 5 | TouchableOpacity, 6 | Pressable, 7 | } from "react-native"; 8 | import React from "react"; 9 | import { Ionicons, MaterialIcons } from "@expo/vector-icons"; 10 | import { useThemeColor } from "@/hooks/useThemeColor"; 11 | 12 | export default function MessageBox() { 13 | const text = useThemeColor({}, "text"); 14 | const barColor = useThemeColor({}, "barColor"); 15 | 16 | return ( 17 | 18 | 19 | 25 | 30 | 31 | 37 | 38 | 39 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | const styles = StyleSheet.create({ 55 | container: { 56 | flexDirection: "row", 57 | alignItems: "flex-end", 58 | padding: 8, 59 | }, 60 | inputBox: { 61 | flex: 1, 62 | flexDirection: "row", 63 | alignItems: "center", 64 | borderRadius: 25, 65 | paddingHorizontal: 10, 66 | marginRight: 8, 67 | minHeight: 48, 68 | }, 69 | icon: { 70 | marginHorizontal: 2, 71 | }, 72 | iconSet: { 73 | marginHorizontal: 6, 74 | }, 75 | textInput: { 76 | flex: 1, 77 | minWidth: 100, 78 | fontSize: 16, 79 | paddingVertical: 8, 80 | paddingHorizontal: 8, 81 | outlineWidth: 0, 82 | }, 83 | micButton: { 84 | backgroundColor: "#25D366", 85 | borderRadius: 25, 86 | width: 48, 87 | height: 48, 88 | alignItems: "center", 89 | justifyContent: "center", 90 | boxShadow: "0 3px 3px #00000025", 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /components/ui/RadialProgress.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | import Svg, { Circle } from "react-native-svg"; 4 | import Animated, { 5 | DerivedValue, 6 | SharedValue, 7 | useAnimatedProps, 8 | } from "react-native-reanimated"; 9 | 10 | const AnimatedCircle = Animated.createAnimatedComponent(Circle); 11 | 12 | type RadialProgressProps = { 13 | progress: DerivedValue; 14 | size?: number; 15 | strokeWidth?: number; 16 | color?: string; 17 | }; 18 | 19 | export const RadialProgress: React.FC = ({ 20 | progress, 21 | size = 100, 22 | strokeWidth = 1.5, 23 | color = "#25D366", 24 | }) => { 25 | const radius = (size - strokeWidth) / 2; 26 | const circumference = 2 * Math.PI * radius; 27 | 28 | const animatedProps = useAnimatedProps(() => ({ 29 | strokeDashoffset: circumference * (1 - progress.value), 30 | })); 31 | 32 | return ( 33 | 34 | 35 | 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 | */ 5 | 6 | const globalColor = { 7 | primary: "#25D366", 8 | }; 9 | 10 | export const Colors = { 11 | light: { 12 | text: "#11181C", 13 | background: "#E9E7E7", 14 | barColor: "#ffffff", 15 | textFade: "#C1C0C1", 16 | error: "#A53640", 17 | ...globalColor, 18 | }, 19 | dark: { 20 | text: "#ECEDEE", 21 | background: "#121212", 22 | barColor: "#222222", 23 | textFade: "#383838", 24 | error: "#FE7272", 25 | ...globalColor, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useColorScheme as useRNColorScheme } from 'react-native'; 3 | 4 | /** 5 | * To support static rendering, this value needs to be re-calculated on the client side for web 6 | */ 7 | export function useColorScheme() { 8 | const [hasHydrated, setHasHydrated] = useState(false); 9 | 10 | useEffect(() => { 11 | setHasHydrated(true); 12 | }, []); 13 | 14 | const colorScheme = useRNColorScheme(); 15 | 16 | if (hasHydrated) { 17 | return colorScheme; 18 | } 19 | 20 | return 'light'; 21 | } 22 | -------------------------------------------------------------------------------- /hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { Colors } from '@/constants/Colors'; 7 | import { useColorScheme } from '@/hooks/useColorScheme'; 8 | 9 | export function useThemeColor( 10 | props: { light?: string; dark?: string }, 11 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 12 | ) { 13 | const theme = useColorScheme() ?? 'light'; 14 | const colorFromProps = props[theme]; 15 | 16 | if (colorFromProps) { 17 | return colorFromProps; 18 | } else { 19 | return Colors[theme][colorName]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wa-swipe-to-talk", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "lint": "expo lint" 11 | }, 12 | "dependencies": { 13 | "@expo/vector-icons": "^14.1.0", 14 | "@react-navigation/bottom-tabs": "^7.2.0", 15 | "@react-navigation/native": "^7.0.14", 16 | "expo": "^53.0.8", 17 | "expo-blur": "~14.1.4", 18 | "expo-constants": "~17.1.6", 19 | "expo-font": "~13.3.1", 20 | "expo-haptics": "~14.1.4", 21 | "expo-image": "~2.1.7", 22 | "expo-linear-gradient": "~14.1.4", 23 | "expo-linking": "~7.1.4", 24 | "expo-navigation-bar": "~4.2.4", 25 | "expo-router": "~5.0.6", 26 | "expo-splash-screen": "~0.30.8", 27 | "expo-status-bar": "~2.2.3", 28 | "expo-symbols": "~0.4.4", 29 | "expo-system-ui": "~5.0.7", 30 | "expo-web-browser": "~14.1.6", 31 | "react": "19.0.0", 32 | "react-dom": "19.0.0", 33 | "react-native": "0.79.2", 34 | "react-native-gesture-handler": "~2.24.0", 35 | "react-native-reanimated": "~3.17.4", 36 | "react-native-safe-area-context": "5.4.0", 37 | "react-native-screens": "~4.10.0", 38 | "react-native-svg": "15.11.2", 39 | "react-native-web": "^0.20.0", 40 | "react-native-webview": "13.13.5" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.25.2", 44 | "@types/react": "~19.0.10", 45 | "typescript": "~5.8.3", 46 | "eslint": "^9.25.0", 47 | "eslint-config-expo": "~9.2.0" 48 | }, 49 | "private": true 50 | } 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------