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