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);