this repo has no description
0
fork

Configure Feed

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

split posts up

+120 -107
+22 -4
apps/expo/src/app/(app)/(home)/timeline.tsx
··· 1 + import { useMemo } from "react"; 1 2 import { ActivityIndicator, Text, View } from "react-native"; 2 3 import { Stack } from "expo-router"; 3 4 import { FlashList } from "@shopify/flash-list"; ··· 20 21 getNextPageParam: (lastPage) => lastPage.cursor, 21 22 }); 22 23 24 + const data = useMemo(() => { 25 + if (timeline.status !== "success") return []; 26 + const flat = timeline.data.pages.flatMap((page) => page.feed); 27 + return flat 28 + .map((item) => 29 + item.reply 30 + ? [ 31 + { item: { post: item.reply.parent }, hasReply: true }, 32 + { item, hasReply: false }, 33 + ] 34 + : [{ item, hasReply: false }], 35 + ) 36 + .flat(); 37 + }, [timeline]); 38 + 23 39 switch (timeline.status) { 24 40 case "loading": 25 41 return ( ··· 51 67 refreshing={timeline.isRefetching} 52 68 onEndReachedThreshold={0.5} 53 69 onEndReached={() => void timeline.fetchNextPage()} 54 - data={timeline.data.pages.flatMap((page) => page.feed)} 55 - estimatedItemSize={367} 56 - renderItem={({ item }) => <FeedPost item={item} />} 57 - keyExtractor={(item) => item.post.uri} 70 + data={data} 71 + estimatedItemSize={91} 72 + renderItem={({ item: { hasReply, item } }) => ( 73 + <FeedPost item={item} hasReply={hasReply} /> 74 + )} 75 + keyExtractor={({ item }) => item.post.uri} 58 76 /> 59 77 </> 60 78 );
+98 -103
apps/expo/src/components/feed-post.tsx
··· 32 32 // TODO - don't nest feedposts! 33 33 34 34 return ( 35 - <View> 36 - {item.reply?.parent && ( 37 - <FeedPost item={{ post: item.reply.parent }} hasReply /> 35 + <View 36 + className={cx( 37 + "bg-white px-2 pt-2", 38 + item.reply?.parent && "pt-0", 39 + !hasReply && "border-b border-b-neutral-200", 38 40 )} 39 - <View 40 - className={cx( 41 - "bg-white px-2 pt-2", 42 - item.reply?.parent && "pt-0", 43 - !hasReply && "border-b border-b-neutral-200", 44 - )} 45 - // onLayout={(x) => console.log(x.nativeEvent.layout)} 46 - > 47 - {item.reason && ( 48 - <View className="mb-1 ml-16 flex-1 flex-row items-center"> 49 - {reasonToText(item.reason as AppBskyFeedDefs.ReasonRepost)} 50 - </View> 51 - )} 52 - <View className="flex-1 flex-row"> 53 - {/* left col */} 54 - <View className="flex flex-col items-center px-2"> 55 - <Link href={profileHref} asChild> 56 - <TouchableOpacity> 57 - {item.post.author.avatar ? ( 58 - <Image 59 - source={{ uri: item.post.author.avatar }} 60 - alt={item.post.author.handle} 61 - className="h-12 w-12 rounded-full" 62 - /> 63 - ) : ( 64 - <View className="h-12 w-12 items-center justify-center rounded-full bg-neutral-100"> 65 - <User size={32} color="#1C1C1E" /> 66 - </View> 67 - )} 68 - </TouchableOpacity> 69 - </Link> 70 - <Link href={postHref} asChild> 71 - <TouchableOpacity className="w-full grow items-center"> 72 - {hasReply && <View className="w-1 grow bg-neutral-200" />} 73 - </TouchableOpacity> 74 - </Link> 75 - </View> 41 + // onLayout={(x) => console.log(x.nativeEvent.layout)} 42 + > 43 + {item.reason && ( 44 + <View className="mb-1 ml-16 flex-1 flex-row items-center"> 45 + {reasonToText(item.reason as AppBskyFeedDefs.ReasonRepost)} 46 + </View> 47 + )} 48 + <View className="flex-1 flex-row"> 49 + {/* left col */} 50 + <View className="flex flex-col items-center px-2"> 51 + <Link href={profileHref} asChild> 52 + <TouchableOpacity> 53 + {item.post.author.avatar ? ( 54 + <Image 55 + source={{ uri: item.post.author.avatar }} 56 + alt={item.post.author.handle} 57 + className="h-12 w-12 rounded-full" 58 + /> 59 + ) : ( 60 + <View className="h-12 w-12 items-center justify-center rounded-full bg-neutral-100"> 61 + <User size={32} color="#1C1C1E" /> 62 + </View> 63 + )} 64 + </TouchableOpacity> 65 + </Link> 66 + <Link href={postHref} asChild> 67 + <TouchableOpacity className="w-full grow items-center"> 68 + {hasReply && <View className="w-1 grow bg-neutral-200" />} 69 + </TouchableOpacity> 70 + </Link> 71 + </View> 76 72 77 - {/* right col */} 78 - <View className="flex-1 pb-2.5 pl-1 pr-2"> 79 - <Link href={profileHref} asChild> 80 - <TouchableOpacity className="flex-row items-center"> 81 - <Text numberOfLines={1} className="max-w-[85%] text-base"> 82 - <Text className="font-semibold"> 83 - {item.post.author.displayName} 84 - </Text> 85 - <Text className="text-neutral-500"> 86 - {` @${item.post.author.handle}`} 87 - </Text> 73 + {/* right col */} 74 + <View className="flex-1 pb-2.5 pl-1 pr-2"> 75 + <Link href={profileHref} asChild> 76 + <TouchableOpacity className="flex-row items-center"> 77 + <Text numberOfLines={1} className="max-w-[85%] text-base"> 78 + <Text className="font-semibold"> 79 + {item.post.author.displayName} 88 80 </Text> 89 - {/* get age of post - e.g. 5m */} 90 - <Text className="text-base text-neutral-500"> 91 - {" "} 92 - · {timeSince(new Date(item.post.indexedAt))} 81 + <Text className="text-neutral-500"> 82 + {` @${item.post.author.handle}`} 93 83 </Text> 94 - </TouchableOpacity> 95 - </Link> 96 - {/* text content */} 97 - <Link href={postHref} asChild> 98 - <TouchableOpacity> 99 - <RichText value={item.post.record.text} /> 100 - </TouchableOpacity> 101 - </Link> 102 - {/* embeds */} 103 - {item.post.embed && <Embed content={item.post.embed} />} 104 - {/* actions */} 105 - <View className="mt-2 flex-row justify-between"> 106 - <TouchableOpacity className="flex-row items-center gap-2"> 107 - <MessageSquare size={16} color="#1C1C1E" /> 108 - <Text>{item.post.replyCount}</Text> 109 - </TouchableOpacity> 110 - <TouchableOpacity 111 - disabled={toggleRepost.isLoading} 112 - onPress={() => toggleRepost.mutate()} 113 - className="flex-row items-center gap-2" 84 + </Text> 85 + {/* get age of post - e.g. 5m */} 86 + <Text className="text-base text-neutral-500"> 87 + {" "} 88 + · {timeSince(new Date(item.post.indexedAt))} 89 + </Text> 90 + </TouchableOpacity> 91 + </Link> 92 + {/* text content */} 93 + <Link href={postHref} asChild> 94 + <TouchableOpacity> 95 + <RichText value={item.post.record.text} /> 96 + </TouchableOpacity> 97 + </Link> 98 + {/* embeds */} 99 + {item.post.embed && <Embed content={item.post.embed} />} 100 + {/* actions */} 101 + <View className="mt-2 flex-row justify-between"> 102 + <TouchableOpacity className="flex-row items-center gap-2"> 103 + <MessageSquare size={16} color="#1C1C1E" /> 104 + <Text>{item.post.replyCount}</Text> 105 + </TouchableOpacity> 106 + <TouchableOpacity 107 + disabled={toggleRepost.isLoading} 108 + onPress={() => toggleRepost.mutate()} 109 + className="flex-row items-center gap-2" 110 + > 111 + <Repeat size={16} color={reposted ? "#2563eb" : "#1C1C1E"} /> 112 + <Text 113 + style={{ 114 + color: reposted ? "#2563eb" : "#1C1C1E", 115 + }} 114 116 > 115 - <Repeat size={16} color={reposted ? "#2563eb" : "#1C1C1E"} /> 116 - <Text 117 - style={{ 118 - color: reposted ? "#2563eb" : "#1C1C1E", 119 - }} 120 - > 121 - {repostCount} 122 - </Text> 123 - </TouchableOpacity> 124 - <TouchableOpacity 125 - disabled={toggleLike.isLoading} 126 - onPress={() => toggleLike.mutate()} 127 - className="flex-row items-center gap-2" 117 + {repostCount} 118 + </Text> 119 + </TouchableOpacity> 120 + <TouchableOpacity 121 + disabled={toggleLike.isLoading} 122 + onPress={() => toggleLike.mutate()} 123 + className="flex-row items-center gap-2" 124 + > 125 + <Heart 126 + size={16} 127 + fill={liked ? "#dc2626" : "transparent"} 128 + color={liked ? "#dc2626" : "#1C1C1E"} 129 + /> 130 + <Text 131 + style={{ 132 + color: liked ? "#dc2626" : "#1C1C1E", 133 + }} 128 134 > 129 - <Heart 130 - size={16} 131 - fill={liked ? "#dc2626" : "transparent"} 132 - color={liked ? "#dc2626" : "#1C1C1E"} 133 - /> 134 - <Text 135 - style={{ 136 - color: liked ? "#dc2626" : "#1C1C1E", 137 - }} 138 - > 139 - {likeCount} 140 - </Text> 141 - </TouchableOpacity> 142 - <View className="w-8" /> 143 - </View> 135 + {likeCount} 136 + </Text> 137 + </TouchableOpacity> 138 + <View className="w-8" /> 144 139 </View> 145 140 </View> 146 141 </View>