this repo has no description
0
fork

Configure Feed

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

move folder structure around

Ideally the inner stack will be nested inside the tabs

+175 -98
apps/expo/src/app/(app)/(home)/_layout.tsx apps/expo/src/app/(app)/(tabs)/_layout.tsx
-40
apps/expo/src/app/(app)/(home)/profile.tsx
··· 1 - import { ActivityIndicator, ScrollView, View } from "react-native"; 2 - import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; 3 - 4 - import { ProfileInfo } from "../../../components/profile-info"; 5 - import { useAuthedAgent } from "../../../lib/agent"; 6 - 7 - export default function ProfilePage() { 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 - const profilePosts = useInfiniteQuery({ 18 - queryKey: ["profile", agent.session.handle, "posts"], 19 - queryFn: async ({ pageParam }) => { 20 - const timeline = await agent.getAuthorFeed({ 21 - actor: agent.session.handle, 22 - cursor: pageParam as string | undefined, 23 - }); 24 - return timeline.data; 25 - }, 26 - getNextPageParam: (lastPage) => lastPage.cursor, 27 - }); 28 - 29 - return ( 30 - <ScrollView> 31 - {profile.data ? ( 32 - <ProfileInfo profile={profile.data} /> 33 - ) : ( 34 - <View className="flex-1 items-center justify-center"> 35 - <ActivityIndicator /> 36 - </View> 37 - )} 38 - </ScrollView> 39 - ); 40 - }
apps/expo/src/app/(app)/(home)/timeline.tsx apps/expo/src/app/(app)/(tabs)/timeline.tsx
+9
apps/expo/src/app/(app)/(tabs)/(stack)/profile/[handle]/index.tsx
··· 1 + import { useLocalSearchParams } from "expo-router"; 2 + 3 + import { ProfileView } from "../../../../../../components/profile-view"; 4 + 5 + export default function ProfilePage() { 6 + const { handle } = useLocalSearchParams() as { handle: string }; 7 + 8 + return <ProfileView handle={handle} />; 9 + }
+8
apps/expo/src/app/(app)/(tabs)/profile.tsx
··· 1 + import { ProfileView } from "../../../components/profile-view"; 2 + import { useAuthedAgent } from "../../../lib/agent"; 3 + 4 + export default function ProfilePage() { 5 + const agent = useAuthedAgent(); 6 + 7 + return <ProfileView handle={agent.session.handle} />; 8 + }
-40
apps/expo/src/app/(app)/profile/[handle]/index.tsx
··· 1 - import { ActivityIndicator, ScrollView, View } from "react-native"; 2 - import { Stack, useLocalSearchParams } from "expo-router"; 3 - import { useQuery } from "@tanstack/react-query"; 4 - 5 - import { ProfileInfo } from "../../../../components/profile-info"; 6 - import { useAuthedAgent } from "../../../../lib/agent"; 7 - 8 - export default function ProfilePage() { 9 - const { handle } = useLocalSearchParams() as { handle: string }; 10 - const agent = useAuthedAgent(); 11 - 12 - const profile = useQuery(["profile", handle], async () => { 13 - const profile = await agent.getProfile({ 14 - actor: handle, 15 - }); 16 - return profile.data; 17 - }); 18 - 19 - return ( 20 - <ScrollView> 21 - <Stack.Screen 22 - options={{ 23 - headerTransparent: true, 24 - headerTitle: "", 25 - headerBackTitle: "", 26 - headerStyle: { 27 - backgroundColor: "transparent", 28 - }, 29 - }} 30 - /> 31 - {profile.data ? ( 32 - <ProfileInfo profile={profile.data} /> 33 - ) : ( 34 - <View className="flex-1 items-center justify-center"> 35 - <ActivityIndicator /> 36 - </View> 37 - )} 38 - </ScrollView> 39 - ); 40 - }
+5 -4
apps/expo/src/app/(app)/profile/[handle]/post/[id].tsx apps/expo/src/app/(app)/(tabs)/(stack)/profile/[handle]/post/[id].tsx
··· 4 4 import { FlashList } from "@shopify/flash-list"; 5 5 import { useQuery } from "@tanstack/react-query"; 6 6 7 - import { FeedPost } from "../../../../../components/feed-post"; 8 - import { Post } from "../../../../../components/post"; 9 - import { useAuthedAgent } from "../../../../../lib/agent"; 10 - import { assert } from "../../../../../lib/utils/assert"; 7 + import { FeedPost } from "../../../../../../../components/feed-post"; 8 + import { Post } from "../../../../../../../components/post"; 9 + import { useAuthedAgent } from "../../../../../../../lib/agent"; 10 + import { assert } from "../../../../../../../lib/utils/assert"; 11 11 12 12 type Posts = { 13 13 post: AppBskyFeedDefs.PostView; ··· 130 130 initialScrollIndex={thread.data.index} 131 131 // estimatedFirstItemOffset={thread.data.index * 91} 132 132 estimatedItemSize={91} 133 + getItemType={(item) => (item.primary ? "big" : "small")} 133 134 renderItem={({ item }) => 134 135 item.primary ? ( 135 136 <Post post={item.post} hasParent={item.hasParent} />
+13 -11
apps/expo/src/components/embed.tsx
··· 176 176 className="aspect-square w-[32%]" 177 177 /> 178 178 ))} 179 - <ImageBackground 180 - source={{ uri: content.images[2]!.thumb }} 181 - alt={content.images[2]!.alt} 182 - className="flex aspect-square w-[32%] flex-row" 183 - > 184 - <View className="h-full w-full items-center justify-center bg-black/60 p-1"> 185 - <Text className="text-center text-base font-bold text-white"> 186 - +{content.images.length - 2} 187 - </Text> 188 - </View> 189 - </ImageBackground> 179 + <View className="aspect-square w-[32%]"> 180 + <ImageBackground 181 + source={{ uri: content.images[2]!.thumb }} 182 + alt={content.images[2]!.alt} 183 + resizeMode="cover" 184 + > 185 + <View className="h-full w-full items-center justify-center bg-black/60 p-1"> 186 + <Text className="text-center text-base font-bold text-white"> 187 + +{content.images.length - 2} 188 + </Text> 189 + </View> 190 + </ImageBackground> 191 + </View> 190 192 </View> 191 193 ); 192 194 }
+3 -3
apps/expo/src/components/profile-info.tsx
··· 29 29 <View> 30 30 <Image 31 31 source={{ uri: profile.banner }} 32 - className="h-48 w-full" 32 + className="h-32 w-full" 33 33 alt="banner image" 34 34 /> 35 - <View className="relative bg-white px-4 pb-4"> 35 + <View className="relative border-b border-b-neutral-200 bg-white px-4 pb-4"> 36 36 <View className="h-10 flex-row items-center justify-end"> 37 - <View className="absolute -top-10 left-0 rounded-full border-4 border-white"> 37 + <View className="absolute -top-11 left-0 rounded-full border-4 border-white"> 38 38 <Image 39 39 source={{ uri: profile.avatar }} 40 40 className="h-20 w-20 rounded-full"
+137
apps/expo/src/components/profile-view.tsx
··· 1 + import { useMemo, useState } from "react"; 2 + import { ActivityIndicator, Text, View } from "react-native"; 3 + import { SafeAreaView } from "react-native-safe-area-context"; 4 + import { Stack } from "expo-router"; 5 + import { FlashList } from "@shopify/flash-list"; 6 + import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; 7 + 8 + import { useAuthedAgent } from "../lib/agent"; 9 + import { FeedPost } from "./feed-post"; 10 + import { ProfileInfo } from "./profile-info"; 11 + 12 + interface Props { 13 + handle: string; 14 + } 15 + 16 + export const ProfileView = ({ handle }: Props) => { 17 + const [withReplies] = useState(false); 18 + const [atTop, setAtTop] = useState(true); 19 + const agent = useAuthedAgent(); 20 + 21 + const profile = useQuery(["profile", handle], async () => { 22 + const profile = await agent.getProfile({ 23 + actor: handle, 24 + }); 25 + return profile.data; 26 + }); 27 + 28 + const timeline = useInfiniteQuery({ 29 + queryKey: ["profile", handle, "feed"], 30 + queryFn: async ({ pageParam }) => { 31 + const timeline = await agent.getAuthorFeed({ 32 + actor: handle, 33 + cursor: pageParam as string | undefined, 34 + }); 35 + return timeline.data; 36 + }, 37 + getNextPageParam: (lastPage) => lastPage.cursor, 38 + }); 39 + 40 + const data = useMemo(() => { 41 + if (timeline.status !== "success") return []; 42 + const flat = timeline.data.pages.flatMap((page) => page.feed); 43 + return flat 44 + .map((item) => 45 + item.reply 46 + ? withReplies 47 + ? [ 48 + { item: { post: item.reply.parent }, hasReply: true }, 49 + { item, hasReply: false }, 50 + ] 51 + : [] 52 + : [{ item, hasReply: false }], 53 + ) 54 + .flat() 55 + .filter(Boolean); 56 + }, [timeline, withReplies]); 57 + 58 + switch (profile.status) { 59 + case "loading": 60 + return ( 61 + <View className="flex-1 items-center justify-center"> 62 + <Stack.Screen 63 + options={{ 64 + headerTitle: "", 65 + headerBackTitleVisible: false, 66 + }} 67 + /> 68 + <Stack.Screen options={{ headerTitle: "Post" }} /> 69 + <ActivityIndicator /> 70 + </View> 71 + ); 72 + case "error": 73 + return ( 74 + <View className="flex-1 items-center justify-center p-4"> 75 + <Stack.Screen 76 + options={{ 77 + headerTitle: "", 78 + headerBackTitleVisible: false, 79 + }} 80 + /> 81 + <Text className="text-center text-xl"> 82 + {(profile.error as Error).message || "An error occurred"} 83 + </Text> 84 + </View> 85 + ); 86 + case "success": 87 + return ( 88 + <SafeAreaView className="flex-1" edges={["top", "left", "right"]}> 89 + <Stack.Screen 90 + options={{ 91 + headerTransparent: true, 92 + headerTitle: "", 93 + headerBackTitleVisible: false, 94 + ...(!atTop 95 + ? { 96 + headerBlurEffect: "systemThinMaterialLight", 97 + } 98 + : { 99 + headerStyle: { 100 + backgroundColor: atTop ? "transparent" : undefined, 101 + }, 102 + }), 103 + }} 104 + /> 105 + <FlashList 106 + data={data} 107 + renderItem={({ item: { hasReply, item } }) => ( 108 + <FeedPost item={item} hasReply={hasReply} /> 109 + )} 110 + onEndReachedThreshold={0.5} 111 + onEndReached={() => void timeline.fetchNextPage()} 112 + // onRefresh={() => { 113 + // if (!timeline.isRefetching) void timeline.refetch(); 114 + // }} 115 + // refreshing={timeline.isRefetching} 116 + estimatedItemSize={91} 117 + onScroll={(evt) => { 118 + const { contentOffset } = evt.nativeEvent; 119 + setAtTop(contentOffset.y <= 30); 120 + }} 121 + ListHeaderComponent={<ProfileInfo profile={profile.data} />} 122 + ListFooterComponent={ 123 + timeline.isFetching ? ( 124 + <View className="w-full items-center py-4"> 125 + <ActivityIndicator /> 126 + </View> 127 + ) : ( 128 + <View className="py-16"> 129 + <Text className="text-center">That&apos;s everything!</Text> 130 + </View> 131 + ) 132 + } 133 + /> 134 + </SafeAreaView> 135 + ); 136 + } 137 + };