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.

Move post to its own file and add stats

SharpMars c6a9a730 7b8c8661

+89 -74
+2 -74
src/app/(tabs)/_layout.tsx
··· 3 3 import { SafeAreaView } from "react-native-safe-area-context"; 4 4 import { Client, ok, simpleFetchHandler } from "@atcute/client"; 5 5 import { FlashList } from "@shopify/flash-list"; 6 - import { AppBskyFeedPost, AppBskyRichtextFacet } from "@atcute/bluesky"; 7 6 import { StatusBar } from "expo-status-bar"; 8 - import EmbedView from "@/src/components/EmbedView"; 9 - import { Image } from "expo-image"; 10 - import { segmentize } from "@atcute/bluesky-richtext-segmenter"; 11 - import { Link } from "expo-router"; 12 7 import { useMaterial3Theme } from "@pchmn/expo-material3-theme"; 8 + import Post from "@/src/components/Post"; 13 9 14 10 export default function TabLayout() { 15 11 const colorScheme = useColorScheme(); ··· 52 48 ListFooterComponent={<View />} 53 49 ListFooterComponentStyle={{ height: 64 }} 54 50 onRefresh={() => feedQuery.refetch()} 55 - renderItem={({ item }) => { 56 - return ( 57 - <View style={{ flex: 1, flexDirection: "row", gap: 8, padding: 8, minWidth: "100%", flexShrink: 0 }}> 58 - <Image source={item.post.author.avatar} style={{ width: 44, height: 44, borderRadius: 19 }} /> 59 - <View style={{ flex: 1, gap: 4 }}> 60 - <View style={{ flex: 1, flexDirection: "row", gap: 4 }}> 61 - <Text 62 - ellipsizeMode="tail" 63 - numberOfLines={1} 64 - style={{ 65 - fontSize: 16, 66 - fontWeight: 700, 67 - overflow: "hidden", 68 - flexShrink: 1, 69 - color: colorScheme === "light" ? "black" : "white", 70 - }} 71 - > 72 - {item.post.author.displayName} 73 - </Text> 74 - <Text 75 - ellipsizeMode="tail" 76 - numberOfLines={1} 77 - style={{ 78 - fontSize: 16, 79 - opacity: 0.75, 80 - overflow: "hidden", 81 - flexShrink: 1, 82 - color: colorScheme === "light" ? "black" : "white", 83 - }} 84 - > 85 - {"@" + item.post.author.handle} 86 - </Text> 87 - </View> 88 - {item.post.author.pronouns && ( 89 - <Text 90 - style={{ 91 - color: colorScheme === "light" ? "black" : "white", 92 - backgroundColor: theme[colorScheme!].elevation.level2, 93 - paddingVertical: 2, 94 - paddingHorizontal: 4, 95 - borderRadius: 4, 96 - alignSelf: "flex-start", 97 - }} 98 - > 99 - {item.post.author.pronouns} 100 - </Text> 101 - )} 102 - {(item.post.record as AppBskyFeedPost.Main).text.trim().length > 0 && ( 103 - <Text selectable style={{ fontSize: 16, color: colorScheme === "light" ? "black" : "white" }}> 104 - {segmentize( 105 - (item.post.record as AppBskyFeedPost.Main).text, 106 - (item.post.record as AppBskyFeedPost.Main).facets 107 - ).map((val, i) => { 108 - if (val.features?.at(0)?.$type === "app.bsky.richtext.facet#link") 109 - return ( 110 - <Link key={i} href={(val.features!.at(0)! as any).uri} style={{ color: "#1974D2" }}> 111 - {val.text} 112 - </Link> 113 - ); 114 - 115 - return val.text; 116 - })} 117 - </Text> 118 - )} 119 - {item.post.embed && <EmbedView embed={item.post.embed} />} 120 - </View> 121 - </View> 122 - ); 123 - }} 51 + renderItem={({ item }) => <Post post={item.post} />} 124 52 /> 125 53 </View> 126 54 )}
+87
src/components/Post.tsx
··· 1 + import { AppBskyFeedDefs, AppBskyFeedPost } from "@atcute/bluesky"; 2 + import { Image } from "expo-image"; 3 + import { Link } from "expo-router"; 4 + import { View, Text, useColorScheme } from "react-native"; 5 + import EmbedView from "./EmbedView"; 6 + import { segmentize } from "@atcute/bluesky-richtext-segmenter"; 7 + import { useMaterial3Theme } from "@pchmn/expo-material3-theme"; 8 + 9 + export default function Post(props: { post: AppBskyFeedDefs.PostView }) { 10 + const colorScheme = useColorScheme(); 11 + const { theme } = useMaterial3Theme({ sourceColor: "#f4983c" }); 12 + 13 + return ( 14 + <View style={{ flex: 1, flexDirection: "row", gap: 8, padding: 8, minWidth: "100%", flexShrink: 0 }}> 15 + <Image source={props.post.author.avatar} style={{ width: 44, height: 44, borderRadius: 19 }} /> 16 + <View style={{ flex: 1, gap: 4 }}> 17 + <View style={{ flex: 1, flexDirection: "row", gap: 4 }}> 18 + <Text 19 + ellipsizeMode="tail" 20 + numberOfLines={1} 21 + style={{ 22 + fontSize: 16, 23 + fontWeight: 700, 24 + overflow: "hidden", 25 + flexShrink: 1, 26 + color: colorScheme === "light" ? "black" : "white", 27 + }} 28 + > 29 + {props.post.author.displayName} 30 + </Text> 31 + <Text 32 + ellipsizeMode="tail" 33 + numberOfLines={1} 34 + style={{ 35 + fontSize: 16, 36 + opacity: 0.75, 37 + overflow: "hidden", 38 + flexShrink: 1, 39 + color: colorScheme === "light" ? "black" : "white", 40 + }} 41 + > 42 + {"@" + props.post.author.handle} 43 + </Text> 44 + </View> 45 + {props.post.author.pronouns && ( 46 + <Text 47 + style={{ 48 + color: colorScheme === "light" ? "black" : "white", 49 + backgroundColor: theme[colorScheme!].elevation.level2, 50 + paddingVertical: 2, 51 + paddingHorizontal: 4, 52 + borderRadius: 4, 53 + alignSelf: "flex-start", 54 + }} 55 + > 56 + {props.post.author.pronouns} 57 + </Text> 58 + )} 59 + {(props.post.record as AppBskyFeedPost.Main).text.trim().length > 0 && ( 60 + <Text selectable style={{ fontSize: 16, color: colorScheme === "light" ? "black" : "white" }}> 61 + {segmentize( 62 + (props.post.record as AppBskyFeedPost.Main).text, 63 + (props.post.record as AppBskyFeedPost.Main).facets 64 + ).map((val, i) => { 65 + if (val.features?.at(0)?.$type === "app.bsky.richtext.facet#link") 66 + return ( 67 + <Link key={i} href={(val.features!.at(0)! as any).uri} style={{ color: "#1974D2" }}> 68 + {val.text} 69 + </Link> 70 + ); 71 + 72 + return val.text; 73 + })} 74 + </Text> 75 + )} 76 + {props.post.embed && <EmbedView embed={props.post.embed} />} 77 + <View style={{ flexDirection: "row", justifyContent: "space-around" }}> 78 + <Text style={{ color: colorScheme === "light" ? "black" : "white" }}>{props.post.replyCount ?? 0}</Text> 79 + <Text style={{ color: colorScheme === "light" ? "black" : "white" }}> 80 + {(props.post.repostCount ?? 0) + (props.post.quoteCount ?? 0)} 81 + </Text> 82 + <Text style={{ color: colorScheme === "light" ? "black" : "white" }}>{props.post.likeCount ?? 0}</Text> 83 + </View> 84 + </View> 85 + </View> 86 + ); 87 + }