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