tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
28
pulls
pipelines
add infinite scrolling feed
awarm.space
5 months ago
4467bdf4
abcb6ae0
+75
-7
4 changed files
expand all
collapse all
unified
split
app
reader
ReaderContent.tsx
SubscriptionsContent.tsx
getReaderFeed.ts
page.tsx
+72
-5
app/reader/ReaderContent.tsx
···
15
15
import { blobRefToSrc } from "src/utils/blobRefToSrc";
16
16
import { Json } from "supabase/database.types";
17
17
import type { Post } from "./getReaderFeed";
18
18
+
import useSWRInfinite from "swr/infinite";
19
19
+
import { getReaderFeed } from "./getReaderFeed";
20
20
+
import { useEffect, useRef } from "react";
21
21
+
import { useRouter } from "next/navigation";
18
22
19
23
export const ReaderContent = (props: {
20
24
root_entity: string;
21
25
posts: Post[];
26
26
+
nextCursor: string | null;
22
27
}) => {
23
23
-
if (props.posts.length === 0) return <ReaderEmpty />;
28
28
+
const getKey = (
29
29
+
pageIndex: number,
30
30
+
previousPageData: { posts: Post[]; nextCursor: string | null } | null,
31
31
+
) => {
32
32
+
// Reached the end
33
33
+
if (previousPageData && !previousPageData.nextCursor) return null;
34
34
+
35
35
+
// First page, we don't have previousPageData
36
36
+
if (pageIndex === 0) return ["reader-feed", null];
37
37
+
38
38
+
// Add the cursor to the key
39
39
+
return ["reader-feed", previousPageData?.nextCursor];
40
40
+
};
41
41
+
42
42
+
const { data, error, size, setSize, isValidating } = useSWRInfinite(
43
43
+
getKey,
44
44
+
([_, cursor]) => getReaderFeed(cursor),
45
45
+
{
46
46
+
fallbackData: [{ posts: props.posts, nextCursor: props.nextCursor }],
47
47
+
revalidateFirstPage: false,
48
48
+
},
49
49
+
);
50
50
+
51
51
+
const loadMoreRef = useRef<HTMLDivElement>(null);
52
52
+
53
53
+
// Set up intersection observer to load more when trigger element is visible
54
54
+
useEffect(() => {
55
55
+
const observer = new IntersectionObserver(
56
56
+
(entries) => {
57
57
+
if (entries[0].isIntersecting && !isValidating) {
58
58
+
const hasMore = data && data[data.length - 1]?.nextCursor;
59
59
+
if (hasMore) {
60
60
+
setSize(size + 1);
61
61
+
}
62
62
+
}
63
63
+
},
64
64
+
{ threshold: 0.1 },
65
65
+
);
66
66
+
67
67
+
if (loadMoreRef.current) {
68
68
+
observer.observe(loadMoreRef.current);
69
69
+
}
70
70
+
71
71
+
return () => observer.disconnect();
72
72
+
}, [data, size, setSize, isValidating]);
73
73
+
74
74
+
const allPosts = data ? data.flatMap((page) => page.posts) : [];
75
75
+
76
76
+
if (allPosts.length === 0 && !isValidating) return <ReaderEmpty />;
77
77
+
24
78
return (
25
25
-
<div className="flex flex-col gap-3">
26
26
-
{props.posts?.map((p) => <Post {...p} key={p.documents.uri} />)}
79
79
+
<div className="flex flex-col gap-3 relative">
80
80
+
{allPosts.map((p) => (
81
81
+
<Post {...p} key={p.documents.uri} />
82
82
+
))}
83
83
+
{/* Trigger element for loading more posts */}
84
84
+
<div
85
85
+
ref={loadMoreRef}
86
86
+
className="absolute bottom-0 left-0 w-full h-px pointer-events-none"
87
87
+
aria-hidden="true"
88
88
+
/>
89
89
+
{isValidating && (
90
90
+
<div className="text-center text-tertiary py-4">
91
91
+
Loading more posts...
92
92
+
</div>
93
93
+
)}
27
94
</div>
28
95
);
29
96
};
···
69
136
`}
70
137
>
71
138
<SpeedyLink
72
72
-
className="h-full w-full absolute top-0 left-0 "
139
139
+
className="h-full w-full absolute top-0 left-0"
73
140
href={`${props.publication.href}/${postUri.rkey}`}
74
141
/>
75
142
<div
76
76
-
className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2 z-1 `}
143
143
+
className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`}
77
144
style={{
78
145
backgroundColor: showPageBackground
79
146
? "rgba(var(--bg-page), var(--bg-page-alpha))"
+1
-1
app/reader/SubscriptionsContent.tsx
···
16
16
17
17
return (
18
18
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3">
19
19
-
{props.publications?.map((p) => <PubListing {...p} />)}
19
19
+
{props.publications?.map((p) => <PubListing key={p.uri} {...p} />)}
20
20
</div>
21
21
);
22
22
};
+1
-1
app/reader/getReaderFeed.ts
···
86
86
});
87
87
88
88
export async function getReaderFeed(
89
89
-
cursor?: string,
89
89
+
cursor?: string | null,
90
90
): Promise<{ posts: Post[]; nextCursor: string | null }> {
91
91
let auth_res = await getIdentityData();
92
92
if (!auth_res?.atp_did) return { posts: [], nextCursor: null };
+1
app/reader/page.tsx
···
79
79
content: (
80
80
<ReaderContent
81
81
root_entity={root_entity}
82
82
+
nextCursor={posts.nextCursor}
82
83
posts={posts.posts}
83
84
/>
84
85
),