tangled
alpha
login
or
join now
bunware.org
/
pin.to.it
6
fork
atom
Scrapboard.org client
6
fork
atom
overview
issues
pulls
pipelines
feat: feeds
TurtlePaw
7 months ago
e7e776ac
2559e853
+119
-48
3 changed files
expand all
collapse all
unified
split
src
app
page.tsx
lib
hooks
useTimeline.tsx
useAuth.tsx
+25
-8
src/app/page.tsx
···
3
import { Feed } from "@/components/Feed";
4
import { useFetchTimeline } from "@/lib/hooks/useTimeline";
5
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
6
-
import { useRef, useEffect } from "react";
7
import { useFeedStore } from "@/lib/stores/feeds";
8
import { useFeeds } from "@/lib/hooks/useFeeds";
9
import { LoaderCircle } from "lucide-react";
···
18
const { feeds } = useFeedDefsStore();
19
const { session } = useAuth();
20
const sentinelRef = useRef<HTMLDivElement>(null);
0
21
22
useEffect(() => {
23
const observer = new IntersectionObserver(
24
(entries) => {
25
if (entries[0].isIntersecting) {
26
-
fetchFeed();
27
}
28
},
29
{ rootMargin: "200px" }
···
58
<main className="px-5">
59
<Tabs defaultValue="timeline" className="w-full">
60
<TabsList
61
-
className="flex w-full overflow-x-auto whitespace-nowrap no-scrollbar px-4 space-x-4"
62
style={{ justifyItems: "unset" }}
63
>
64
-
<TabsTrigger value="timeline" className="shrink-0 ml-10">
0
0
0
0
65
Timeline
66
</TabsTrigger>
67
{Object.entries(feeds).map(([value, it]) => (
68
-
<TabsTrigger key={value} value={value} className="shrink-0">
0
0
0
0
0
69
{it?.displayName}
70
</TabsTrigger>
71
))}
···
78
/>
79
</TabsContent>
80
81
-
{Object.entries(feeds).map(([value]) => (
82
-
<TabsContent key={value} value={value}></TabsContent>
83
-
))}
0
0
0
0
0
0
0
84
</Tabs>
85
86
<div ref={sentinelRef} className="h-1" />
···
3
import { Feed } from "@/components/Feed";
4
import { useFetchTimeline } from "@/lib/hooks/useTimeline";
5
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
6
+
import { useRef, useEffect, useState } from "react";
7
import { useFeedStore } from "@/lib/stores/feeds";
8
import { useFeeds } from "@/lib/hooks/useFeeds";
9
import { LoaderCircle } from "lucide-react";
···
18
const { feeds } = useFeedDefsStore();
19
const { session } = useAuth();
20
const sentinelRef = useRef<HTMLDivElement>(null);
21
+
const [feed, setFeed] = useState<"timeline" | string>("timeline");
22
23
useEffect(() => {
24
const observer = new IntersectionObserver(
25
(entries) => {
26
if (entries[0].isIntersecting) {
27
+
fetchFeed(feed);
28
}
29
},
30
{ rootMargin: "200px" }
···
59
<main className="px-5">
60
<Tabs defaultValue="timeline" className="w-full">
61
<TabsList
62
+
className="overflow-x-auto w-full justify-start" //"flex w-full overflow-x-auto whitespace-nowrap no-scrollbar pl-10 pr-4 space-x-4"
63
style={{ justifyItems: "unset" }}
64
>
65
+
<TabsTrigger
66
+
onClick={() => setFeed("timeline")}
67
+
value="timeline"
68
+
className="shrink-0"
69
+
>
70
Timeline
71
</TabsTrigger>
72
{Object.entries(feeds).map(([value, it]) => (
73
+
<TabsTrigger
74
+
onClick={() => setFeed(value)}
75
+
key={value}
76
+
value={value}
77
+
className="shrink-0"
78
+
>
79
{it?.displayName}
80
</TabsTrigger>
81
))}
···
88
/>
89
</TabsContent>
90
91
+
{Object.entries(feeds)
92
+
.filter((it) => feedStore.customFeeds?.[it[0]] != null)
93
+
.map(([value]) => (
94
+
<TabsContent key={value} value={value}>
95
+
<Feed
96
+
feed={feedStore.customFeeds[value].posts}
97
+
isLoading={feedStore.customFeeds[value].isLoading}
98
+
/>
99
+
</TabsContent>
100
+
))}
101
</Tabs>
102
103
<div ref={sentinelRef} className="h-1" />
+82
-40
src/lib/hooks/useTimeline.tsx
···
1
// lib/hooks/useFetchTimeline.ts
2
-
import { useEffect, useRef, useCallback } from "react";
3
-
import { AppBskyEmbedImages } from "@atproto/api";
4
import { useAuth } from "@/lib/useAuth";
5
import { useFeedStore } from "../stores/feeds";
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
6
7
export function useFetchTimeline() {
8
const { agent } = useAuth();
9
-
const { timeline, setTimeline, appendTimeline, setTimelineLoading } =
10
-
useFeedStore();
0
0
0
0
0
0
11
const seenImageUrls = useRef<Set<string>>(new Set());
12
13
-
const fetchFeed = useCallback(async () => {
14
-
if (!agent || timeline.isLoading) return;
15
-
setTimelineLoading(true);
16
-
try {
17
-
const response = await agent.getTimeline({
18
-
cursor: timeline.cursor,
19
-
limit: 100,
20
-
});
21
-
if (!response.success) throw new Error("Failed to fetch timeline");
22
-
23
-
const newCursor = response.data.cursor;
0
0
0
0
0
0
0
0
0
24
25
-
const filtered = response.data.feed.filter((it) => {
26
-
if (it.reason?.$type === "app.bsky.feed.defs#reasonRepost")
27
-
return false;
28
-
if (
29
-
!(
30
-
AppBskyEmbedImages.isMain(it.post.embed) ||
31
-
AppBskyEmbedImages.isView(it.post.embed)
32
-
)
33
-
)
34
-
return false;
0
0
0
0
35
36
-
const images = (it.post.embed as AppBskyEmbedImages.View)?.images || [];
37
-
const hasNew = images.some(
38
-
(img) => !seenImageUrls.current.has(img.fullsize)
39
-
);
40
-
if (!hasNew) return false;
41
42
-
images.forEach((img) => seenImageUrls.current.add(img.fullsize));
43
-
return true;
44
-
});
0
45
46
-
appendTimeline(filtered, newCursor);
47
-
} catch (err) {
48
-
console.error("Fetch failed", err);
49
-
} finally {
50
-
setTimelineLoading(false);
51
-
}
52
-
}, [agent, timeline]);
0
0
0
0
0
53
54
useEffect(() => {
55
const loadMinimum = async () => {
···
1
// lib/hooks/useFetchTimeline.ts
2
+
import { useEffect, useRef, useCallback, Ref } from "react";
3
+
import { AppBskyEmbedImages, AtUri } from "@atproto/api";
4
import { useAuth } from "@/lib/useAuth";
5
import { useFeedStore } from "../stores/feeds";
6
+
import { FeedViewPost } from "@atproto/api/dist/client/types/app/bsky/feed/defs";
7
+
8
+
function filterPosts(posts: FeedViewPost[], seenImageUrls: Set<string>) {
9
+
return posts.filter((it) => {
10
+
if (it.reason?.$type === "app.bsky.feed.defs#reasonRepost") return false;
11
+
if (
12
+
!(
13
+
AppBskyEmbedImages.isMain(it.post.embed) ||
14
+
AppBskyEmbedImages.isView(it.post.embed)
15
+
)
16
+
)
17
+
return false;
18
+
19
+
const images = (it.post.embed as AppBskyEmbedImages.View)?.images || [];
20
+
const hasNew = images.some((img) => !seenImageUrls.has(img.fullsize));
21
+
if (!hasNew) return false;
22
+
23
+
images.forEach((img) => seenImageUrls.add(img.fullsize));
24
+
return true;
25
+
});
26
+
}
27
28
export function useFetchTimeline() {
29
const { agent } = useAuth();
30
+
const {
31
+
timeline,
32
+
setTimeline,
33
+
appendTimeline,
34
+
setTimelineLoading,
35
+
setCustomFeed,
36
+
setCustomFeedLoading,
37
+
} = useFeedStore();
38
const seenImageUrls = useRef<Set<string>>(new Set());
39
40
+
const fetchFeed = useCallback(
41
+
async (feed?: string | undefined) => {
42
+
console.log("loading", feed);
43
+
if (!agent || timeline.isLoading) return;
44
+
if (feed) {
45
+
setCustomFeedLoading(feed, true);
46
+
} else setTimelineLoading(true);
47
+
try {
48
+
if (feed && feed != "timeline") {
49
+
const response = feed.includes("list")
50
+
? await agent.app.bsky.feed.getListFeed({
51
+
cursor: timeline.cursor,
52
+
limit: 100,
53
+
list: feed,
54
+
})
55
+
: await agent.app.bsky.feed.getFeed({
56
+
cursor: timeline.cursor,
57
+
limit: 100,
58
+
feed: feed,
59
+
});
60
61
+
if (!response.success) throw new Error("Failed to fetch timeline");
62
+
setCustomFeed(
63
+
feed,
64
+
filterPosts(response.data.feed, seenImageUrls.current).map(
65
+
(it) => it.post
66
+
),
67
+
response.data.cursor
68
+
);
69
+
} else {
70
+
const response = await agent.getTimeline({
71
+
cursor: timeline.cursor,
72
+
limit: 100,
73
+
});
74
+
if (!response.success) throw new Error("Failed to fetch timeline");
75
76
+
const newCursor = response.data.cursor;
0
0
0
0
77
78
+
const filtered = filterPosts(
79
+
response.data.feed,
80
+
seenImageUrls.current
81
+
);
82
83
+
appendTimeline(filtered, newCursor);
84
+
}
85
+
} catch (err) {
86
+
console.error("Fetch failed", err);
87
+
} finally {
88
+
if (feed) {
89
+
setCustomFeedLoading(feed, false);
90
+
} else setTimelineLoading(false);
91
+
}
92
+
},
93
+
[agent, timeline]
94
+
);
95
96
useEffect(() => {
97
const loadMinimum = async () => {
+12
src/lib/useAuth.tsx
···
66
try {
67
const result = await c.init();
68
if (result?.session) {
0
0
69
const ag = new Agent(result.session);
70
setSession(result.session);
71
setAgent(ag);
0
0
0
0
0
0
0
0
0
0
72
}
73
} catch (err) {
74
console.error("OAuth init failed", err);
···
66
try {
67
const result = await c.init();
68
if (result?.session) {
69
+
console.log("session found", result);
70
+
localStorage.setItem("did", result.session.did);
71
const ag = new Agent(result.session);
72
setSession(result.session);
73
setAgent(ag);
74
+
} else {
75
+
const did = localStorage.getItem("did");
76
+
77
+
console.log("restoring", did);
78
+
if (did != null) {
79
+
const result = await c.restore(did);
80
+
const ag = new Agent(result);
81
+
setSession(result);
82
+
setAgent(ag);
83
+
}
84
}
85
} catch (err) {
86
console.error("OAuth init failed", err);