an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 1414d177a29f7b2587dbbd6fb69d278459835467 173 lines 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);