this repo has no description
0
fork

Configure Feed

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

add profile info component

+167 -69
+1 -5
apps/expo/src/app/(app)/(home)/_layout.tsx
··· 1 1 import { Stack, Tabs } from "expo-router"; 2 2 import { Cloudy, User } from "lucide-react-native"; 3 3 4 - import { useAgent } from "../../../lib/agent"; 5 - 6 4 export default function AppLayout() { 7 - const agent = useAgent(); 8 5 return ( 9 6 <> 10 7 <Stack.Screen 11 8 options={{ 12 - headerTitle: agent.session?.handle, 9 + headerShown: false, 13 10 animation: "none", 14 - headerBackTitleVisible: false, 15 11 }} 16 12 /> 17 13 <Tabs screenOptions={{ headerShown: false }}>
+24 -4
apps/expo/src/app/(app)/(home)/profile.tsx
··· 1 - import { Text } from "react-native"; 1 + import { ActivityIndicator, ScrollView, View } from "react-native"; 2 + import { useQuery } from "@tanstack/react-query"; 2 3 3 - import { useAgent } from "../../../lib/agent"; 4 + import { ProfileInfo } from "../../../components/profile-info"; 5 + import { useAuthedAgent } from "../../../lib/agent"; 4 6 5 7 export default function ProfilePage() { 6 - const agent = useAgent(); 7 - return <Text>{agent.session?.handle}</Text>; 8 + const agent = useAuthedAgent(); 9 + 10 + const profile = useQuery(["profile", agent.session.handle], async () => { 11 + const profile = await agent.getProfile({ 12 + actor: agent.session.handle, 13 + }); 14 + return profile.data; 15 + }); 16 + 17 + return ( 18 + <ScrollView> 19 + {profile.data ? ( 20 + <ProfileInfo profile={profile.data} /> 21 + ) : ( 22 + <View className="flex-1 items-center justify-center"> 23 + <ActivityIndicator /> 24 + </View> 25 + )} 26 + </ScrollView> 27 + ); 8 28 }
+19 -18
apps/expo/src/app/(app)/(home)/timeline.tsx
··· 1 1 import { ActivityIndicator, Text, View } from "react-native"; 2 + import { Stack } from "expo-router"; 2 3 import { FlashList } from "@shopify/flash-list"; 3 4 import { useInfiniteQuery } from "@tanstack/react-query"; 4 5 ··· 23 24 case "loading": 24 25 return ( 25 26 <View className="flex-1 items-center justify-center"> 26 - <ActivityIndicator size="large" /> 27 + <ActivityIndicator /> 27 28 </View> 28 29 ); 29 30 ··· 41 42 42 43 case "success": 43 44 return ( 44 - <FlashList 45 - onRefresh={() => { 46 - if (!timeline.isRefetching) void timeline.refetch(); 47 - }} 48 - refreshing={timeline.isRefetching} 49 - onEndReachedThreshold={0.5} 50 - onEndReached={() => void timeline.fetchNextPage()} 51 - data={timeline.data.pages.flatMap((page) => page.feed)} 52 - estimatedItemSize={110} 53 - renderItem={({ item }) => ( 54 - // <View className="h-56 w-full border-b bg-white"> 55 - // <Text>{item.post.author.displayName}</Text> 56 - // </View> 57 - <FeedPost item={item} /> 58 - )} 59 - keyExtractor={(item) => item.post.uri} 60 - /> 45 + <> 46 + <Stack.Screen options={{ headerShown: true }} /> 47 + <FlashList 48 + onRefresh={() => { 49 + if (!timeline.isRefetching) void timeline.refetch(); 50 + }} 51 + refreshing={timeline.isRefetching} 52 + onEndReachedThreshold={0.5} 53 + onEndReached={() => void timeline.fetchNextPage()} 54 + data={timeline.data.pages.flatMap((page) => page.feed)} 55 + estimatedItemSize={110} 56 + renderItem={({ item }) => ( 57 + <FeedPost item={item} key={item.post.cid} /> 58 + )} 59 + keyExtractor={(item) => item.post.uri} 60 + /> 61 + </> 61 62 ); 62 63 } 63 64 }
+9 -10
apps/expo/src/app/(app)/profile/[handle]/index.tsx
··· 1 - import { Image, ScrollView, Text } from "react-native"; 2 - import { SafeAreaView } from "react-native-safe-area-context"; 1 + import { ActivityIndicator, ScrollView, View } from "react-native"; 3 2 import { Stack, useSearchParams } from "expo-router"; 4 3 import { useQuery } from "@tanstack/react-query"; 5 4 5 + import { ProfileInfo } from "../../../../components/profile-info"; 6 6 import { useAuthedAgent } from "../../../../lib/agent"; 7 7 8 8 export default function ProfilePage() { ··· 16 16 return profile.data; 17 17 }); 18 18 19 - if (!profile.data) return null; 20 - 21 19 return ( 22 20 <ScrollView> 23 21 <Stack.Screen ··· 29 27 }, 30 28 }} 31 29 /> 32 - <Image 33 - source={{ uri: profile.data.banner }} 34 - className="h-48 w-full" 35 - alt="banner image" 36 - /> 37 - <Text>{JSON.stringify(profile.data, null, 2)}</Text> 30 + {profile.data ? ( 31 + <ProfileInfo profile={profile.data} /> 32 + ) : ( 33 + <View className="flex-1 items-center justify-center"> 34 + <ActivityIndicator /> 35 + </View> 36 + )} 38 37 </ScrollView> 39 38 ); 40 39 }
+40 -28
apps/expo/src/app/(auth)/login.tsx
··· 1 1 import { useState } from "react"; 2 - import { Button, TextInput, View } from "react-native"; 2 + import { Alert, Button, TextInput, View } from "react-native"; 3 3 import { SafeAreaView } from "react-native-safe-area-context"; 4 4 import { Stack } from "expo-router"; 5 5 import { useMutation } from "@tanstack/react-query"; 6 + import { Lock, User } from "lucide-react-native"; 6 7 7 8 import { useAgent } from "../../lib/agent"; 8 9 ··· 15 16 const login = useMutation({ 16 17 mutationKey: ["login"], 17 18 mutationFn: async () => { 18 - try { 19 - await agent.login({ identifier, password }); 20 - } catch (err) { 21 - console.info("caught error"); 22 - console.error((err as Error).stack); 23 - } 19 + await agent.login({ identifier, password }); 24 20 }, 25 21 }); 26 22 27 23 return ( 28 - <SafeAreaView className="flex-1 px-4"> 29 - <Stack.Screen options={{ title: "Log in" }} /> 30 - <View className="gap-4"> 31 - <TextInput 32 - className="rounded bg-white p-2 text-base" 33 - placeholder="Username or email address" 34 - value={identifier} 35 - onChangeText={setIdentifier} 36 - autoCapitalize="none" 37 - /> 38 - <TextInput 39 - className="rounded bg-white p-2 text-base" 40 - placeholder="Password" 41 - value={password} 42 - onChangeText={setPassword} 43 - secureTextEntry 44 - /> 45 - {identifier && password && ( 46 - <View> 24 + <SafeAreaView className="flex-1 bg-white px-4"> 25 + <Stack.Screen options={{ title: "Log in", headerBackTitle: "Back" }} /> 26 + <View className="items-stretch gap-4"> 27 + <View className="flex flex-row items-center rounded border border-neutral-300 px-3 py-2"> 28 + <User size={18} color="rgb(163 163 163)" /> 29 + <TextInput 30 + className="mb-1 ml-2 flex-1 text-base" 31 + placeholder="Username or email address" 32 + value={identifier} 33 + onChangeText={setIdentifier} 34 + autoCapitalize="none" 35 + /> 36 + </View> 37 + <View className="flex flex-row items-center rounded border border-neutral-300 px-3 py-2"> 38 + <Lock size={18} color="rgb(163 163 163)" /> 39 + <TextInput 40 + className="mb-1 ml-2 flex-1 text-base" 41 + placeholder="Password" 42 + value={password} 43 + onChangeText={setPassword} 44 + secureTextEntry 45 + /> 46 + </View> 47 + <View className="flex-row justify-between"> 48 + <Button 49 + onPress={() => 50 + Alert.alert( 51 + "Help", 52 + "You need a bsky.social account in order to log in. If you don't have one, you'll need an invite code.", 53 + ) 54 + } 55 + title="Help" 56 + /> 57 + <View /> 58 + {identifier && password && ( 47 59 <Button 48 60 disabled={login.isLoading} 49 61 onPress={() => login.mutate()} 50 62 title="Log in" 51 63 /> 52 - </View> 53 - )} 64 + )} 65 + </View> 54 66 </View> 55 67 </SafeAreaView> 56 68 );
+3 -4
apps/expo/src/app/_layout.tsx
··· 9 9 type AtpSessionEvent, 10 10 } from "@atproto/api"; 11 11 import AsyncStorage from "@react-native-async-storage/async-storage"; 12 - import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 12 + import { QueryClientProvider } from "@tanstack/react-query"; 13 13 14 14 import { AgentProvider } from "../lib/agent"; 15 + import { queryClient } from "../lib/query-client"; 15 16 import { fetchHandler } from "../lib/utils/polyfills/fetch-polyfill"; 16 - 17 - const queryClient = new QueryClient(); 18 17 19 18 export default function RootLayout() { 20 19 const segments = useSegments(); ··· 39 38 void AsyncStorage.removeItem("session"); 40 39 setSession(null); 41 40 Alert.alert( 42 - "An error occurred", 41 + "Could not log you in", 43 42 "Please check your details and try again", 44 43 ); 45 44 break;
+1
apps/expo/src/app/index.tsx
··· 16 16 export default function LandingPage() { 17 17 return ( 18 18 <View className="flex-1"> 19 + <Stack.Screen options={{ headerShown: false }} /> 19 20 <StatusBar style="light" /> 20 21 <ImageBackground className="flex-1" source={background}> 21 22 <SafeAreaView className="flex-1 items-stretch justify-between p-4">
+67
apps/expo/src/components/profile-info.tsx
··· 1 + import { Button, Image, Text, View } from "react-native"; 2 + import type { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 3 + import { useMutation } from "@tanstack/react-query"; 4 + 5 + import { useAuthedAgent } from "../lib/agent"; 6 + import { queryClient } from "../lib/query-client"; 7 + 8 + interface Props { 9 + profile: ProfileViewDetailed; 10 + } 11 + 12 + export const ProfileInfo = ({ profile }: Props) => { 13 + const agent = useAuthedAgent(); 14 + 15 + const toggleFollow = useMutation({ 16 + mutationKey: ["follow", profile.did], 17 + mutationFn: async () => { 18 + if (profile.viewer?.following) { 19 + await agent.deleteFollow(profile.viewer?.following); 20 + } else { 21 + await agent.follow(profile.did); 22 + } 23 + }, 24 + onSettled: () => 25 + void queryClient.invalidateQueries(["profile", profile.handle]), 26 + }); 27 + 28 + return ( 29 + <View> 30 + <Image 31 + source={{ uri: profile.banner }} 32 + className="h-48 w-full" 33 + alt="banner image" 34 + /> 35 + <View className="relative bg-white px-4 pb-4"> 36 + <View className="h-10 flex-row items-center justify-end"> 37 + <Image 38 + source={{ uri: profile.avatar }} 39 + className="absolute -top-10 left-0 h-20 w-20 rounded-full border-4 border-white" 40 + alt="avatar image" 41 + /> 42 + {agent.session?.handle !== profile.handle && ( 43 + <Button 44 + disabled={toggleFollow.isLoading} 45 + onPress={() => toggleFollow.mutate()} 46 + title={profile.viewer?.following ? "Following" : "Follow"} 47 + /> 48 + )} 49 + </View> 50 + <Text className="mt-1 text-2xl font-medium">{profile.displayName}</Text> 51 + <Text className="text-neutral-500">@{profile.handle}</Text> 52 + <View className="mt-3 flex-row"> 53 + <Text> 54 + <Text className="font-bold">{profile.followersCount}</Text>{" "} 55 + Followers 56 + </Text> 57 + <Text className="ml-4"> 58 + <Text className="font-bold">{profile.followsCount}</Text> Following 59 + </Text> 60 + </View> 61 + {profile.description && ( 62 + <Text className="mt-3">{profile.description}</Text> 63 + )} 64 + </View> 65 + </View> 66 + ); 67 + };
+3
apps/expo/src/lib/query-client.ts
··· 1 + import { QueryClient } from "@tanstack/react-query"; 2 + 3 + export const queryClient = new QueryClient();