an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
at main 4.9 kB view raw
1import { useQueryClient } from "@tanstack/react-query"; 2import * as React from "react"; 3 4//import { useInView } from "react-intersection-observer"; 5import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 6import { useAuth } from "~/providers/UnifiedAuthProvider"; 7import { 8 useInfiniteQueryFeedSkeleton, 9 // useQueryArbitrary, 10 // useQueryIdentity, 11} from "~/utils/useQuery"; 12 13interface InfiniteCustomFeedProps { 14 feedUri: string; 15 pdsUrl?: string; 16 feedServiceDid?: string; 17 authedOverride?: boolean; 18 unauthedfeedurl?: string; 19} 20 21export function InfiniteCustomFeed({ 22 feedUri, 23 pdsUrl, 24 feedServiceDid, 25 authedOverride, 26 unauthedfeedurl, 27}: InfiniteCustomFeedProps) { 28 const { agent } = useAuth(); 29 const authed = authedOverride || !!agent?.did; 30 31 // const identityresultmaybe = useQueryIdentity(agent?.did); 32 // const identity = identityresultmaybe?.data; 33 // const feedGenGetRecordQuery = useQueryArbitrary(feedUri); 34 35 const { 36 data, 37 error, 38 isLoading, 39 isError, 40 hasNextPage, 41 fetchNextPage, 42 isFetchingNextPage, 43 refetch, 44 isRefetching, 45 queryKey, 46 } = useInfiniteQueryFeedSkeleton({ 47 feedUri: feedUri, 48 agent: agent ?? undefined, 49 isAuthed: authed ?? false, 50 pdsUrl: pdsUrl, 51 feedServiceDid: feedServiceDid, 52 unauthedfeedurl: unauthedfeedurl, 53 }); 54 const queryClient = useQueryClient(); 55 56 57 const handleRefresh = () => { 58 queryClient.removeQueries({queryKey: queryKey}); 59 //queryClient.invalidateQueries(["infinite-feed", feedUri] as const); 60 refetch(); 61 }; 62 63 const allPosts = React.useMemo(() => { 64 const flattenedPosts = data?.pages.flatMap((page) => page?.feed) ?? []; 65 66 const seenUris = new Set<string>(); 67 68 return flattenedPosts.filter((item) => { 69 if (!item?.post) return false; 70 71 if (seenUris.has(item.post)) { 72 return false; 73 } 74 75 seenUris.add(item.post); 76 77 return true; 78 }); 79 }, [data]); 80 81 //const { ref, inView } = useInView(); 82 83 // React.useEffect(() => { 84 // if (inView && hasNextPage && !isFetchingNextPage) { 85 // fetchNextPage(); 86 // } 87 // }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); 88 89 if (isLoading) { 90 return <div className="p-4 text-center text-gray-500">Loading feed...</div>; 91 } 92 93 if (isError) { 94 return ( 95 <div className="p-4 text-center text-red-500">Error: {error.message}</div> 96 ); 97 } 98 99 // const allPosts = 100 // data?.pages.flatMap((page) => { 101 // if (page) return page.feed; 102 // }) ?? []; 103 104 if (!allPosts || typeof allPosts !== "object" || allPosts.length === 0) { 105 return ( 106 <div className="p-4 text-center text-gray-500"> 107 No posts in this feed. 108 </div> 109 ); 110 } 111 112 return ( 113 <> 114 {allPosts.map((item, i) => { 115 if (item) 116 return ( 117 <UniversalPostRendererATURILoader 118 key={item.post || i} 119 atUri={item.post} 120 feedviewpost={true} 121 repostedby={!!item.reason?.$type && (item.reason as any)?.repost} 122 /> 123 ); 124 })} 125 {/* allPosts?: {allPosts ? "true" : "false"} 126 hasNextPage?: {hasNextPage ? "true" : "false"} 127 isFetchingNextPage?: {isFetchingNextPage ? "true" : "false"} */} 128 {isFetchingNextPage && ( 129 <div className="p-4 text-center text-gray-500">Loading more...</div> 130 )} 131 {hasNextPage && !isFetchingNextPage && ( 132 <button 133 onClick={() => fetchNextPage()} 134 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" 135 > 136 Load More Posts 137 </button> 138 )} 139 {!hasNextPage && ( 140 <div className="p-4 text-center text-gray-500">End of feed.</div> 141 )} 142 <button 143 onClick={handleRefresh} 144 disabled={isRefetching} 145 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" 146 aria-label="Refresh feed" 147 > 148 <RefreshIcon 149 className={`h-6 w-6 text-gray-600 dark:text-gray-400 ${isRefetching && "animate-spin"}`} 150 /> 151 </button> 152 </> 153 ); 154} 155 156const RefreshIcon = (props: React.SVGProps<SVGSVGElement>) => ( 157 <svg 158 xmlns="http://www.w3.org/2000/svg" 159 //width={360} 160 //height={360} 161 viewBox="0 0 24 24" 162 {...props} 163 > 164 <path 165 fill="none" 166 stroke="currentColor" 167 strokeLinecap="round" 168 strokeLinejoin="round" 169 strokeWidth={2} 170 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" 171 ></path> 172 </svg> 173);