an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
1import * as React from "react"; 2 3//import { useInView } from "react-intersection-observer"; 4import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 5import { useAuth } from "~/providers/UnifiedAuthProvider"; 6import { 7 useInfiniteQueryFeedSkeleton, 8 // useQueryArbitrary, 9 // useQueryIdentity, 10} from "~/utils/useQuery"; 11 12interface InfiniteCustomFeedProps { 13 feedUri: string; 14 pdsUrl?: string; 15 feedServiceDid?: string; 16} 17 18export function InfiniteCustomFeed({ 19 feedUri, 20 pdsUrl, 21 feedServiceDid, 22}: InfiniteCustomFeedProps) { 23 const { agent } = useAuth(); 24 const authed = !!agent?.did; 25 26 // const identityresultmaybe = useQueryIdentity(agent?.did); 27 // const identity = identityresultmaybe?.data; 28 // const feedGenGetRecordQuery = useQueryArbitrary(feedUri); 29 30 const { 31 data, 32 error, 33 isLoading, 34 isError, 35 hasNextPage, 36 fetchNextPage, 37 isFetchingNextPage, 38 refetch, 39 isRefetching, 40 } = useInfiniteQueryFeedSkeleton({ 41 feedUri: feedUri, 42 agent: agent ?? undefined, 43 isAuthed: authed ?? false, 44 pdsUrl: pdsUrl, 45 feedServiceDid: feedServiceDid, 46 }); 47 48 const handleRefresh = () => { 49 refetch(); 50 }; 51 52 //const { ref, inView } = useInView(); 53 54 // React.useEffect(() => { 55 // if (inView && hasNextPage && !isFetchingNextPage) { 56 // fetchNextPage(); 57 // } 58 // }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); 59 60 if (isLoading) { 61 return <div className="p-4 text-center text-gray-500">Loading feed...</div>; 62 } 63 64 if (isError) { 65 return ( 66 <div className="p-4 text-center text-red-500">Error: {error.message}</div> 67 ); 68 } 69 70 const allPosts = 71 data?.pages.flatMap((page) => { 72 if (page) return page.feed; 73 }) ?? []; 74 75 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 76 return ( 77 <div className="p-4 text-center text-gray-500"> 78 No posts in this feed. 79 </div> 80 ); 81 } 82 83 return ( 84 <> 85 {allPosts.map((item, i) => { 86 if (item) 87 return ( 88 <UniversalPostRendererATURILoader 89 key={item.post || i} 90 atUri={item.post} 91 feedviewpost={true} 92 repostedby={!!item.reason?.$type && (item.reason as any)?.repost} 93 /> 94 ); 95 })} 96 {/* allPosts?: {allPosts ? "true" : "false"} 97 hasNextPage?: {hasNextPage ? "true" : "false"} 98 isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */} 99 {isFetchingNextPage && ( 100 <div className="p-4 text-center text-gray-500">Loading more...</div> 101 )} 102 {hasNextPage && !isFetchingNextPage && ( 103 <button 104 onClick={() => fetchNextPage()} 105 className="w-[calc(100%-2rem)] mx-4 my-4 px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 font-semibold" 106 > 107 Load More Posts 108 </button> 109 )} 110 {!hasNextPage && ( 111 <div className="p-4 text-center text-gray-500">End of feed.</div> 112 )} 113 <button 114 onClick={handleRefresh} 115 disabled={isRefetching} 116 className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:dark:bg-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed" 117 aria-label="Refresh feed" 118 > 119 <RefreshIcon className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} /> 120 </button> 121 </> 122 ); 123} 124 125const RefreshIcon = (props: React.SVGProps<SVGSVGElement>) => ( 126 <svg 127 xmlns="http://www.w3.org/2000/svg" 128 //width={360} 129 //height={360} 130 viewBox="0 0 24 24" 131 {...props} 132 > 133 <path 134 fill="none" 135 stroke="currentColor" 136 strokeLinecap="round" 137 strokeLinejoin="round" 138 strokeWidth={2} 139 d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" 140 ></path> 141 </svg> 142);