├── .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 |
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 |
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 |
--------------------------------------------------------------------------------