Hopefully feature-complete Android Bluesky client written in Expo
atproto bluesky
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial UI for the post composer

SharpMars fa183b8c 2783fd0a

+352 -175
src/app/(authenticated)/(feed)/_layout.tsx src/app/(authenticated)/(tabs)/(feed)/_layout.tsx
src/app/(authenticated)/(feed)/index.tsx src/app/(authenticated)/(tabs)/(feed)/index.tsx
src/app/(authenticated)/(feed,profile,notifications,search)/profile/[userId]/index.tsx src/app/(authenticated)/(tabs)/(feed,profile,notifications,search)/profile/[userId]/index.tsx
src/app/(authenticated)/(feed,profile,notifications,search)/profile/[userId]/post/[postId].tsx src/app/(authenticated)/(tabs)/(feed,profile,notifications,search)/profile/[userId]/post/[postId].tsx
src/app/(authenticated)/(notifications)/_layout.tsx src/app/(authenticated)/(tabs)/(notifications)/_layout.tsx
src/app/(authenticated)/(notifications)/index.tsx src/app/(authenticated)/(tabs)/(notifications)/index.tsx
src/app/(authenticated)/(profile)/_layout.tsx src/app/(authenticated)/(tabs)/(profile)/_layout.tsx
src/app/(authenticated)/(profile)/index.tsx src/app/(authenticated)/(tabs)/(profile)/index.tsx
src/app/(authenticated)/(search)/_layout.tsx src/app/(authenticated)/(tabs)/(search)/_layout.tsx
src/app/(authenticated)/(search)/index.tsx src/app/(authenticated)/(tabs)/(search)/index.tsx
+180
src/app/(authenticated)/(tabs)/_layout.tsx
··· 1 + import { InteractionCacheProvider } from "@/src/components/InteractionCacheProvider"; 2 + import { ShareSheetProvider } from "@/src/components/ShareSheetProvider"; 3 + import { InteractionCache } from "@/src/misc/interactionCache"; 4 + import { BottomSheetModal, BottomSheetModalProvider } from "@gorhom/bottom-sheet"; 5 + import { useEffect, useRef } from "react"; 6 + import { Pressable, View } from "react-native"; 7 + import { DissmissingModalSheetBackdrop } from "@/src/components/DissmissingModalSheetBackdrop"; 8 + import { ShareSheetView } from "@/src/components/ShareSheetView"; 9 + import { useMaterial3Theme } from "@pchmn/expo-material3-theme"; 10 + import { useColorScheme } from "@/src/hooks/useColorScheme"; 11 + import { Tabs, TabList, TabTrigger, TabSlot, TabTriggerSlotProps } from "expo-router/ui"; 12 + import { AnimatedMaterialSymbols, MaterialSymbols } from "@/src/components/MaterialSymbols"; 13 + import { useSafeAreaInsets } from "react-native-safe-area-context"; 14 + import Animated, { 15 + clamp, 16 + interpolateColor, 17 + useAnimatedRef, 18 + useAnimatedStyle, 19 + useDerivedValue, 20 + useSharedValue, 21 + WigglySpringConfigWithDuration, 22 + withClamp, 23 + withSpring, 24 + } from "react-native-reanimated"; 25 + import { BottomTabBarOffsetProvider } from "@/src/components/BottomTabBarOffsetProvider"; 26 + import { Href, Link } from "expo-router"; 27 + 28 + const interactionCache = new InteractionCache(); 29 + 30 + export default function AuthenticatedLayout() { 31 + const bottomSheetRef = useRef<BottomSheetModal>(null!); 32 + const colorScheme = useColorScheme(); 33 + const { theme } = useMaterial3Theme({ sourceColor: "#f4983c" }); 34 + const insets = useSafeAreaInsets(); 35 + const tabBarRef = useAnimatedRef(); 36 + const tabBarOffset = useSharedValue(0); 37 + const tabBarHeight = useSharedValue(0); 38 + 39 + const tabBarAnimatedStyle = useAnimatedStyle(() => { 40 + return { 41 + transform: [ 42 + { 43 + translateY: withClamp({ min: 0, max: tabBarHeight.get() }, withSpring(tabBarOffset.get(), { duration: 1 })), 44 + }, 45 + ], 46 + }; 47 + }); 48 + 49 + return ( 50 + <ShareSheetProvider sheetRef={bottomSheetRef}> 51 + <InteractionCacheProvider cache={interactionCache}> 52 + <BottomSheetModalProvider> 53 + <BottomTabBarOffsetProvider 54 + deltaAdd={(val) => tabBarOffset.set((curr) => (curr = clamp(curr + val, 0, tabBarHeight.get())))} 55 + set={(val) => tabBarOffset.set(val)} 56 + > 57 + <Tabs options={{ screenListeners: { focus: () => tabBarOffset.set(0) } }}> 58 + <TabSlot /> 59 + <Animated.View 60 + ref={tabBarRef} 61 + onLayout={(ev) => tabBarHeight.set(ev.nativeEvent.layout.height)} 62 + style={[ 63 + { 64 + paddingTop: 6, 65 + paddingBottom: insets.bottom, 66 + paddingHorizontal: 16, 67 + justifyContent: "space-evenly", 68 + backgroundColor: theme[colorScheme].surfaceContainer, 69 + gap: 8, 70 + flexDirection: "row", 71 + position: "absolute", 72 + bottom: 0, 73 + width: "100%", 74 + }, 75 + tabBarAnimatedStyle, 76 + ]} 77 + > 78 + <TabTrigger name="Home" asChild> 79 + <CustomTabButton name="home" /> 80 + </TabTrigger> 81 + <TabTrigger name="Search" asChild> 82 + <CustomTabButton name="search" /> 83 + </TabTrigger> 84 + <View 85 + style={{ 86 + overflow: "hidden", 87 + borderRadius: 16, 88 + height: 48, 89 + width: 64, 90 + }} 91 + > 92 + <Link href={"/new-post"} asChild> 93 + <Pressable 94 + style={{ 95 + alignItems: "center", 96 + overflow: "hidden", 97 + backgroundColor: theme[colorScheme].primary, 98 + height: "100%", 99 + justifyContent: "center", 100 + }} 101 + android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 102 + > 103 + <MaterialSymbols name="add" size={28} color={theme[colorScheme].onPrimary} /> 104 + </Pressable> 105 + </Link> 106 + </View> 107 + <TabTrigger name="Notifications" asChild> 108 + <CustomTabButton name="notifications" /> 109 + </TabTrigger> 110 + <TabTrigger name="Profile" asChild> 111 + <CustomTabButton name="person" /> 112 + </TabTrigger> 113 + </Animated.View> 114 + <TabList style={{ display: "none" }}> 115 + <TabTrigger name="Home" href={"/(authenticated)/(feed)"} /> 116 + <TabTrigger name="Search" href={"/(authenticated)/(search)"} /> 117 + <TabTrigger name="Notifications" href={"/(authenticated)/(notifications)"} /> 118 + <TabTrigger name="Profile" href={"/(authenticated)/(profile)"} /> 119 + </TabList> 120 + </Tabs> 121 + <BottomSheetModal 122 + ref={bottomSheetRef} 123 + enableDismissOnClose 124 + backdropComponent={(props) => ( 125 + <DissmissingModalSheetBackdrop 126 + {...props} 127 + style={{ position: "absolute", width: "100%", height: "100%" }} 128 + /> 129 + )} 130 + backgroundStyle={{ backgroundColor: theme[colorScheme].surfaceContainerLow }} 131 + handleIndicatorStyle={{ backgroundColor: theme[colorScheme].onSurfaceVariant, opacity: 0.4 }} 132 + > 133 + {(data) => <ShareSheetView did={data.data.did} uri={data.data.uri} />} 134 + </BottomSheetModal> 135 + </BottomTabBarOffsetProvider> 136 + </BottomSheetModalProvider> 137 + </InteractionCacheProvider> 138 + </ShareSheetProvider> 139 + ); 140 + } 141 + 142 + function CustomTabButton( 143 + props: React.PropsWithChildren & TabTriggerSlotProps & { name: keyof typeof MaterialSymbols.glyphMap } 144 + ) { 145 + const colorScheme = useColorScheme(); 146 + const { theme } = useMaterial3Theme({ sourceColor: "#f4983c" }); 147 + const focusTransition = useSharedValue(props.isFocused ? 1 : 0); 148 + 149 + useEffect(() => { 150 + focusTransition.set(withSpring(props.isFocused ? 1 : 0, { ...WigglySpringConfigWithDuration, duration: 250 })); 151 + }, [props.isFocused, focusTransition]); 152 + 153 + const iconColor = useDerivedValue(() => { 154 + return interpolateColor( 155 + focusTransition.get(), 156 + [0, 1], 157 + [theme[colorScheme].onSurfaceVariant, theme[colorScheme].onSecondaryContainer] 158 + ); 159 + }); 160 + 161 + const backgroundStyle = useAnimatedStyle(() => { 162 + return { 163 + backgroundColor: theme[colorScheme].secondaryContainer, 164 + position: "absolute", 165 + width: "100%", 166 + height: "100%", 167 + transform: [{ scale: focusTransition.get() }], 168 + borderRadius: 19, 169 + }; 170 + }); 171 + 172 + return ( 173 + <Pressable {...props} style={{ alignItems: "center", gap: 2, justifyContent: "center" }} ref={props.ref}> 174 + <View style={[{ width: 64, height: 38, alignItems: "center", justifyContent: "center", borderRadius: 16 }]}> 175 + <Animated.View style={backgroundStyle} /> 176 + <AnimatedMaterialSymbols name={props.name} size={26} color={iconColor as any} /> 177 + </View> 178 + </Pressable> 179 + ); 180 + }
+3 -175
src/app/(authenticated)/_layout.tsx
··· 1 - import { InteractionCacheProvider } from "@/src/components/InteractionCacheProvider"; 2 - import { ShareSheetProvider } from "@/src/components/ShareSheetProvider"; 3 - import { InteractionCache } from "@/src/misc/interactionCache"; 4 - import { BottomSheetModal, BottomSheetModalProvider } from "@gorhom/bottom-sheet"; 5 - import { useEffect, useRef } from "react"; 6 - import { Pressable, View } from "react-native"; 7 - import { DissmissingModalSheetBackdrop } from "@/src/components/DissmissingModalSheetBackdrop"; 8 - import { ShareSheetView } from "@/src/components/ShareSheetView"; 9 - import { useMaterial3Theme } from "@pchmn/expo-material3-theme"; 10 - import { useColorScheme } from "@/src/hooks/useColorScheme"; 11 - import { Tabs, TabList, TabTrigger, TabSlot, TabTriggerSlotProps } from "expo-router/ui"; 12 - import { AnimatedMaterialSymbols, MaterialSymbols } from "@/src/components/MaterialSymbols"; 13 - import { useSafeAreaInsets } from "react-native-safe-area-context"; 14 - import Animated, { 15 - clamp, 16 - interpolateColor, 17 - useAnimatedRef, 18 - useAnimatedStyle, 19 - useDerivedValue, 20 - useSharedValue, 21 - WigglySpringConfigWithDuration, 22 - withClamp, 23 - withSpring, 24 - } from "react-native-reanimated"; 25 - import { BottomTabBarOffsetProvider } from "@/src/components/BottomTabBarOffsetProvider"; 26 - 27 - const interactionCache = new InteractionCache(); 28 - 29 - export default function AuthenticatedLayout() { 30 - const bottomSheetRef = useRef<BottomSheetModal>(null!); 31 - const colorScheme = useColorScheme(); 32 - const { theme } = useMaterial3Theme({ sourceColor: "#f4983c" }); 33 - const insets = useSafeAreaInsets(); 34 - const tabBarRef = useAnimatedRef(); 35 - const tabBarOffset = useSharedValue(0); 36 - const tabBarHeight = useSharedValue(0); 37 - 38 - const tabBarAnimatedStyle = useAnimatedStyle(() => { 39 - return { 40 - transform: [ 41 - { 42 - translateY: withClamp({ min: 0, max: tabBarHeight.get() }, withSpring(tabBarOffset.get(), { duration: 1 })), 43 - }, 44 - ], 45 - }; 46 - }); 47 - 48 - return ( 49 - <ShareSheetProvider sheetRef={bottomSheetRef}> 50 - <InteractionCacheProvider cache={interactionCache}> 51 - <BottomSheetModalProvider> 52 - <BottomTabBarOffsetProvider 53 - deltaAdd={(val) => tabBarOffset.set((curr) => (curr = clamp(curr + val, 0, tabBarHeight.get())))} 54 - set={(val) => tabBarOffset.set(val)} 55 - > 56 - <Tabs options={{ screenListeners: { focus: () => tabBarOffset.set(0) } }}> 57 - <TabSlot /> 58 - <Animated.View 59 - ref={tabBarRef} 60 - onLayout={(ev) => tabBarHeight.set(ev.nativeEvent.layout.height)} 61 - style={[ 62 - { 63 - paddingTop: 6, 64 - paddingBottom: insets.bottom, 65 - paddingHorizontal: 16, 66 - justifyContent: "space-evenly", 67 - backgroundColor: theme[colorScheme].surfaceContainer, 68 - gap: 8, 69 - flexDirection: "row", 70 - position: "absolute", 71 - bottom: 0, 72 - width: "100%", 73 - }, 74 - tabBarAnimatedStyle, 75 - ]} 76 - > 77 - <TabTrigger name="Home" asChild> 78 - <CustomTabButton name="home" /> 79 - </TabTrigger> 80 - <TabTrigger name="Search" asChild> 81 - <CustomTabButton name="search" /> 82 - </TabTrigger> 83 - <View 84 - style={{ 85 - overflow: "hidden", 86 - borderRadius: 16, 87 - height: 48, 88 - width: 64, 89 - }} 90 - > 91 - <Pressable 92 - style={{ 93 - alignItems: "center", 94 - overflow: "hidden", 95 - backgroundColor: theme[colorScheme].primary, 96 - height: "100%", 97 - justifyContent: "center", 98 - }} 99 - android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 100 - > 101 - <MaterialSymbols name="add" size={28} color={theme[colorScheme].onPrimary} /> 102 - </Pressable> 103 - </View> 104 - <TabTrigger name="Notifications" asChild> 105 - <CustomTabButton name="notifications" /> 106 - </TabTrigger> 107 - <TabTrigger name="Profile" asChild> 108 - <CustomTabButton name="person" /> 109 - </TabTrigger> 110 - </Animated.View> 111 - <TabList style={{ display: "none" }}> 112 - <TabTrigger name="Home" href={"/(authenticated)/(feed)"} /> 113 - <TabTrigger name="Search" href={"/(authenticated)/(search)"} /> 114 - <TabTrigger name="Notifications" href={"/(authenticated)/(notifications)"} /> 115 - <TabTrigger name="Profile" href={"/(authenticated)/(profile)"} /> 116 - </TabList> 117 - </Tabs> 118 - <BottomSheetModal 119 - ref={bottomSheetRef} 120 - enableDismissOnClose 121 - backdropComponent={(props) => ( 122 - <DissmissingModalSheetBackdrop 123 - {...props} 124 - style={{ position: "absolute", width: "100%", height: "100%" }} 125 - /> 126 - )} 127 - backgroundStyle={{ backgroundColor: theme[colorScheme].surfaceContainerLow }} 128 - handleIndicatorStyle={{ backgroundColor: theme[colorScheme].onSurfaceVariant, opacity: 0.4 }} 129 - > 130 - {(data) => <ShareSheetView did={data.data.did} uri={data.data.uri} />} 131 - </BottomSheetModal> 132 - </BottomTabBarOffsetProvider> 133 - </BottomSheetModalProvider> 134 - </InteractionCacheProvider> 135 - </ShareSheetProvider> 136 - ); 137 - } 1 + import { Stack } from "expo-router"; 138 2 139 - function CustomTabButton( 140 - props: React.PropsWithChildren & TabTriggerSlotProps & { name: keyof typeof MaterialSymbols.glyphMap } 141 - ) { 142 - const colorScheme = useColorScheme(); 143 - const { theme } = useMaterial3Theme({ sourceColor: "#f4983c" }); 144 - const focusTransition = useSharedValue(props.isFocused ? 1 : 0); 145 - 146 - useEffect(() => { 147 - focusTransition.set(withSpring(props.isFocused ? 1 : 0, { ...WigglySpringConfigWithDuration, duration: 250 })); 148 - }, [props.isFocused, focusTransition]); 149 - 150 - const iconColor = useDerivedValue(() => { 151 - return interpolateColor( 152 - focusTransition.get(), 153 - [0, 1], 154 - [theme[colorScheme].onSurfaceVariant, theme[colorScheme].onSecondaryContainer] 155 - ); 156 - }); 157 - 158 - const backgroundStyle = useAnimatedStyle(() => { 159 - return { 160 - backgroundColor: theme[colorScheme].secondaryContainer, 161 - position: "absolute", 162 - width: "100%", 163 - height: "100%", 164 - transform: [{ scale: focusTransition.get() }], 165 - borderRadius: 19, 166 - }; 167 - }); 168 - 169 - return ( 170 - <Pressable {...props} style={{ alignItems: "center", gap: 2, justifyContent: "center" }} ref={props.ref}> 171 - <View style={[{ width: 64, height: 38, alignItems: "center", justifyContent: "center", borderRadius: 16 }]}> 172 - <Animated.View style={backgroundStyle} /> 173 - <AnimatedMaterialSymbols name={props.name} size={26} color={iconColor as any} /> 174 - </View> 175 - </Pressable> 176 - ); 3 + export default function Layout() { 4 + return <Stack screenOptions={{ headerShown: false }} />; 177 5 }
+169
src/app/(authenticated)/new-post.tsx
··· 1 + import { MaterialSymbols } from "@/src/components/MaterialSymbols"; 2 + import { useOAuthSession } from "@/src/components/SessionProvider"; 3 + import { useColorScheme } from "@/src/hooks/useColorScheme"; 4 + import { Client, FetchHandler, ok } from "@atcute/client"; 5 + import { useMaterial3Theme } from "@pchmn/expo-material3-theme"; 6 + import { useQuery } from "@tanstack/react-query"; 7 + import { Image } from "expo-image"; 8 + import { Stack } from "expo-router"; 9 + import { Pressable, Text, View } from "react-native"; 10 + import { useSafeAreaInsets } from "react-native-safe-area-context"; 11 + 12 + export default function NewPost() { 13 + const insets = useSafeAreaInsets(); 14 + const colorScheme = useColorScheme(); 15 + const { theme } = useMaterial3Theme({ sourceColor: "#f4983c" }); 16 + const session = useOAuthSession(); 17 + 18 + const profileQuery = useQuery({ 19 + queryKey: ["profile", session.did], 20 + queryFn: async ({ queryKey, signal }) => { 21 + const wrapper: FetchHandler = async (pathname, init) => { 22 + return session.fetchHandler(pathname, init); 23 + }; 24 + 25 + const client = new Client({ 26 + handler: wrapper, 27 + proxy: { did: "did:web:api.bsky.app", serviceId: "#bsky_appview" }, 28 + }); 29 + 30 + return await ok(client.get("app.bsky.actor.getProfile", { signal, params: { actor: session.did } })); 31 + }, 32 + }); 33 + 34 + return ( 35 + <View style={{ flex: 1, backgroundColor: theme[colorScheme].background }}> 36 + <Stack.Screen 37 + options={{ 38 + presentation: "modal", 39 + animation: "fade_from_bottom", 40 + headerShown: true, 41 + header: () => { 42 + return ( 43 + <View 44 + style={{ 45 + paddingTop: insets.top, 46 + paddingBottom: 4, 47 + paddingHorizontal: 4, 48 + gap: 8, 49 + backgroundColor: theme[colorScheme].surfaceContainer, 50 + flexDirection: "row", 51 + justifyContent: "space-between", 52 + }} 53 + > 54 + <View style={{ overflow: "hidden", borderRadius: 48, width: 48, height: 48 }}> 55 + <Pressable 56 + style={{ flex: 1, justifyContent: "center", alignItems: "center", borderRadius: 48 }} 57 + android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 58 + > 59 + <MaterialSymbols color={theme[colorScheme].onSurface} name="close" size={26} /> 60 + </Pressable> 61 + </View> 62 + <View style={{ overflow: "hidden", borderRadius: 32 }}> 63 + <Pressable 64 + style={{ 65 + flex: 1, 66 + justifyContent: "center", 67 + alignItems: "center", 68 + flexDirection: "row", 69 + gap: 4, 70 + paddingVertical: 4, 71 + paddingHorizontal: 8, 72 + }} 73 + android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 74 + > 75 + <Text style={{ color: theme[colorScheme].primary, fontSize: 16, fontWeight: 500 }}>Post</Text> 76 + <MaterialSymbols color={theme[colorScheme].primary} name="send" size={26} /> 77 + </Pressable> 78 + </View> 79 + </View> 80 + ); 81 + }, 82 + }} 83 + /> 84 + {profileQuery.isSuccess && ( 85 + <View style={{ width: "100%", padding: 12, flexDirection: "row", gap: 8 }}> 86 + <Image source={profileQuery.data.avatar} style={{ width: 48, height: 48, borderRadius: 12 }} /> 87 + <View style={{ flex: 1, justifyContent: "center" }}> 88 + <Text style={{ fontSize: 16, fontWeight: 500, color: colorScheme === "light" ? "black" : "white" }}> 89 + {profileQuery.data.displayName} 90 + </Text> 91 + <Text style={{ color: colorScheme === "light" ? "black" : "white", opacity: 0.75 }}> 92 + {"@" + profileQuery.data.handle} 93 + </Text> 94 + </View> 95 + </View> 96 + )} 97 + <View style={{ flex: 1, paddingVertical: 4, paddingHorizontal: 12 }}> 98 + <Text style={{ color: "white", opacity: 0.75, fontSize: 16 }}>Type something fun</Text> 99 + </View> 100 + <View 101 + style={{ 102 + width: "100%", 103 + paddingHorizontal: 4, 104 + paddingTop: 4, 105 + paddingBottom: insets.bottom + 4, 106 + backgroundColor: theme[colorScheme].surfaceContainer, 107 + gap: 4, 108 + flexDirection: "row", 109 + justifyContent: "space-between", 110 + }} 111 + > 112 + <View style={{ flexDirection: "row", gap: 4 }}> 113 + <View style={{ overflow: "hidden", borderRadius: 48, width: 48, height: 48 }}> 114 + <Pressable 115 + style={{ flex: 1, justifyContent: "center", alignItems: "center", borderRadius: 48 }} 116 + android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 117 + > 118 + <MaterialSymbols color={theme[colorScheme].onSurface} name="add_photo_alternate" size={26} /> 119 + </Pressable> 120 + </View> 121 + <View style={{ overflow: "hidden", borderRadius: 48, width: 48, height: 48 }}> 122 + <Pressable 123 + style={{ flex: 1, justifyContent: "center", alignItems: "center", borderRadius: 48 }} 124 + android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 125 + > 126 + <MaterialSymbols color={theme[colorScheme].onSurface} name="emoji_emotions" size={26} /> 127 + </Pressable> 128 + </View> 129 + <View style={{ overflow: "hidden", borderRadius: 48, width: 48, height: 48 }}> 130 + <Pressable 131 + style={{ flex: 1, justifyContent: "center", alignItems: "center", borderRadius: 48 }} 132 + android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 133 + > 134 + <MaterialSymbols color={theme[colorScheme].onSurface} name="gif_box" size={26} /> 135 + </Pressable> 136 + </View> 137 + <View style={{ overflow: "hidden", borderRadius: 48, width: 48, height: 48 }}> 138 + <Pressable 139 + style={{ flex: 1, justifyContent: "center", alignItems: "center", borderRadius: 48 }} 140 + android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 141 + > 142 + <MaterialSymbols color={theme[colorScheme].onSurface} name="record_voice_over" size={26} /> 143 + </Pressable> 144 + </View> 145 + </View> 146 + <View style={{ flexDirection: "row", alignItems: "center", paddingRight: 16, gap: 8 }}> 147 + <View style={{ overflow: "hidden", borderRadius: 32 }}> 148 + <Pressable 149 + style={{ 150 + flex: 1, 151 + justifyContent: "center", 152 + alignItems: "center", 153 + flexDirection: "row", 154 + gap: 4, 155 + paddingVertical: 4, 156 + paddingHorizontal: 8, 157 + }} 158 + android_ripple={{ color: theme[colorScheme].inverseSurface + "46", foreground: true }} 159 + > 160 + <MaterialSymbols color={theme[colorScheme].onSurface} name="language" size={26} /> 161 + <Text style={{ color: theme[colorScheme].onSurface, fontWeight: 500 }}>English</Text> 162 + </Pressable> 163 + </View> 164 + <Text style={{ color: theme[colorScheme].onSurface, fontWeight: 500, fontSize: 16 }}>300</Text> 165 + </View> 166 + </View> 167 + </View> 168 + ); 169 + }