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.

Show threads in feeds

SharpMars 576f1220 672ac75e

+90 -10
+2 -2
src/app/(authenticated)/index.tsx
··· 12 12 import { Client, FetchHandler, ok } from "@atcute/client"; 13 13 import { StatusBar } from "expo-status-bar"; 14 14 import { useMaterial3Theme } from "@pchmn/expo-material3-theme"; 15 - import Post from "@/src/components/Post"; 16 15 import { useOAuthSession } from "@/src/components/SessionProvider"; 17 16 import { Did, ResourceUri } from "@atcute/lexicons"; 18 17 import { AppBskyActorDefs, AppBskyFeedDefs, AppBskyGraphDefs } from "@atcute/bluesky"; ··· 37 36 import { Image } from "expo-image"; 38 37 import { getRuntimeKind, runOnJS, RuntimeKind } from "react-native-worklets"; 39 38 import { ScrollView } from "react-native-reanimated/lib/typescript/Animated"; 39 + import FeedPost from "@/src/components/FeedPost"; 40 40 41 41 const CurrentPageContext = createContext<number>(0); 42 42 ··· 455 455 ListFooterComponent={<View />} 456 456 ListFooterComponentStyle={{ height: 64 }} 457 457 onRefresh={() => feedQuery.refetch()} 458 - renderItem={({ item }) => <Post post={item.post} />} 458 + renderItem={({ item }) => <FeedPost feedPost={item} />} 459 459 /> 460 460 </View> 461 461 )}
+2 -2
src/app/(authenticated)/profile/[userId].tsx
··· 21 21 withSpring, 22 22 } from "react-native-reanimated"; 23 23 import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; 24 - import Post from "@/src/components/Post"; 24 + import FeedPost from "@/src/components/FeedPost"; 25 25 import { usePagerView } from "react-native-pager-view"; 26 26 import { Image } from "expo-image"; 27 27 import { MaterialIcons } from "@expo/vector-icons"; ··· 546 546 ListFooterComponent={<View />} 547 547 ListFooterComponentStyle={{ height: 64 }} 548 548 onRefresh={() => feedQuery.refetch()} 549 - renderItem={({ item }) => <Post post={item.post} />} 549 + renderItem={({ item }) => <FeedPost feedPost={item} />} 550 550 /> 551 551 </View> 552 552 )}
+86 -6
src/components/Post.tsx src/components/FeedPost.tsx
··· 10 10 import { Client, FetchHandler, ok } from "@atcute/client"; 11 11 import { Did } from "@atcute/lexicons"; 12 12 13 - export default function Post(props: { post: AppBskyFeedDefs.PostView }) { 13 + export default function FeedPost(props: { feedPost: AppBskyFeedDefs.FeedViewPost }) { 14 + const colorScheme = useColorScheme(); 15 + const { theme } = useMaterial3Theme({ sourceColor: "#f4983c" }); 16 + const reply = props.feedPost.reply; 17 + 18 + return ( 19 + <View> 20 + {reply?.root.$type === "app.bsky.feed.defs#postView" && <Post hasReply post={reply.root} />} 21 + {reply?.parent.$type === "app.bsky.feed.defs#postView" && 22 + (reply.parent.record as AppBskyFeedPost.Main).reply && 23 + (reply.parent.record as AppBskyFeedPost.Main).reply?.parent.uri !== reply.root.uri && ( 24 + <View style={{ flexDirection: "row", padding: 8, alignSelf: "flex-start", gap: 8 }}> 25 + <View style={{ width: 44 }}> 26 + <View style={{ gap: 2, flex: 1 }}> 27 + <View 28 + style={{ 29 + flex: 1, 30 + width: 2, 31 + borderRadius: 8, 32 + backgroundColor: theme[colorScheme!].elevation.level5, 33 + alignSelf: "center", 34 + }} 35 + /> 36 + <View 37 + style={{ 38 + flex: 1, 39 + width: 2, 40 + borderRadius: 8, 41 + backgroundColor: theme[colorScheme!].elevation.level5, 42 + alignSelf: "center", 43 + }} 44 + /> 45 + <View 46 + style={{ 47 + flex: 1, 48 + width: 2, 49 + borderRadius: 8, 50 + backgroundColor: theme[colorScheme!].elevation.level5, 51 + alignSelf: "center", 52 + }} 53 + /> 54 + <View 55 + style={{ 56 + bottom: -12, 57 + left: 21, 58 + position: "absolute", 59 + flex: 1, 60 + width: 2, 61 + height: 6, 62 + borderRadius: 8, 63 + backgroundColor: theme[colorScheme!].elevation.level5, 64 + }} 65 + /> 66 + </View> 67 + </View> 68 + <Text style={{ color: colorScheme === "light" ? "black" : "white" }}>View full thread</Text> 69 + </View> 70 + )} 71 + {reply?.parent.$type === "app.bsky.feed.defs#postView" && reply.parent.uri !== reply.root.uri && ( 72 + <Post hasReply post={reply.parent} /> 73 + )} 74 + <Post post={props.feedPost.post} /> 75 + </View> 76 + ); 77 + } 78 + 79 + function Post(props: { post: AppBskyFeedDefs.PostView; hasReply?: true }) { 14 80 const colorScheme = useColorScheme(); 15 81 const { theme } = useMaterial3Theme({ sourceColor: "#f4983c" }); 16 82 const session = useOAuthSession(); ··· 47 113 48 114 return ( 49 115 <View style={{ flex: 1, flexDirection: "row", gap: 8, padding: 8, minWidth: "100%", flexShrink: 0 }}> 50 - <Link href={("/profile/" + props.post.author.did) as Href} asChild> 51 - <Pressable> 52 - <Image source={props.post.author.avatar} style={{ width: 44, height: 44, borderRadius: 19 }} /> 53 - </Pressable> 54 - </Link> 116 + <View style={{ gap: 8 }}> 117 + <Link href={("/profile/" + props.post.author.did) as Href} asChild> 118 + <Pressable> 119 + <Image source={props.post.author.avatar} style={{ width: 44, height: 44, borderRadius: 19 }} /> 120 + </Pressable> 121 + </Link> 122 + {props.hasReply && ( 123 + <View 124 + style={{ 125 + flex: 1, 126 + width: 2, 127 + borderRadius: 8, 128 + backgroundColor: theme[colorScheme!].elevation.level5, 129 + alignSelf: "center", 130 + marginBottom: -8, 131 + }} 132 + ></View> 133 + )} 134 + </View> 55 135 <View style={{ flex: 1, gap: 4 }}> 56 136 <View style={{ flex: 1, flexDirection: "row", gap: 4 }}> 57 137 <Text