an appview-less Bluesky client using Constellation and PDS Queries
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
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);