tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
Feeds page
whey.party
1 month ago
b31b9e53
394cb76d
+145
-3
1 changed file
expand all
collapse all
unified
split
src
routes
feeds.tsx
+145
-3
src/routes/feeds.tsx
···
1
1
-
import { createFileRoute } from "@tanstack/react-router";
1
1
+
import * as ATPAPI from "@atproto/api";
2
2
+
import { createFileRoute, Link } from "@tanstack/react-router";
3
3
+
import { useAtom } from "jotai";
4
4
+
import * as React from "react";
2
5
3
6
import { Header } from "~/components/Header";
7
7
+
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
8
+
import { imgCDNAtom, quickAuthAtom } from "~/utils/atoms";
9
9
+
import {
10
10
+
useQueryArbitrary,
11
11
+
useQueryIdentity,
12
12
+
useQueryPreferences,
13
13
+
} from "~/utils/useQuery";
4
14
5
15
export const Route = createFileRoute("/feeds")({
6
16
component: Feeds,
7
17
});
8
18
9
19
export function Feeds() {
20
20
+
const { agent, status } = useAuth();
21
21
+
const [quickAuth] = useAtom(quickAuthAtom);
22
22
+
const isAuthRestoring = quickAuth ? status === "loading" : false;
23
23
+
24
24
+
const identityresultmaybe = useQueryIdentity(
25
25
+
!isAuthRestoring ? agent?.did : undefined,
26
26
+
);
27
27
+
const identity = identityresultmaybe?.data;
28
28
+
29
29
+
const prefsresultmaybe = useQueryPreferences({
30
30
+
agent: !isAuthRestoring ? (agent ?? undefined) : undefined,
31
31
+
pdsUrl: !isAuthRestoring ? identity?.pds : undefined,
32
32
+
});
33
33
+
const prefs = prefsresultmaybe?.data;
34
34
+
35
35
+
const savedFeeds = React.useMemo(() => {
36
36
+
const savedFeedsPref = prefs?.preferences?.find(
37
37
+
(p: any) => p?.$type === "app.bsky.actor.defs#savedFeedsPrefV2",
38
38
+
);
39
39
+
return savedFeedsPref?.items || [];
40
40
+
}, [prefs]);
41
41
+
42
42
+
const pinnedFeeds = React.useMemo(() => {
43
43
+
return savedFeeds.filter((feed: any) => feed.pinned);
44
44
+
}, [savedFeeds]);
45
45
+
46
46
+
const nonPinnedFeeds = React.useMemo(() => {
47
47
+
return savedFeeds.filter((feed: any) => !feed.pinned);
48
48
+
}, [savedFeeds]);
49
49
+
10
50
return (
11
51
<div className="">
12
52
<Header
···
18
58
window.location.assign("/");
19
59
}
20
60
}}
21
21
-
bottomBorderDisabled={true}
61
61
+
bottomBorderDisabled={false}
22
62
/>
23
23
-
Feeds page (coming soon)
63
63
+
<div className="py-4">
64
64
+
{pinnedFeeds.length > 0 && (
65
65
+
<div className="mb-6">
66
66
+
<h2 className="text-lg font-semibold mb-3 px-4">Pinned Feeds</h2>
67
67
+
<div className="flex flex-col">
68
68
+
{pinnedFeeds.map((feed: any) => (
69
69
+
<FeedItem key={feed.value} feedUri={feed.value} />
70
70
+
))}
71
71
+
</div>
72
72
+
</div>
73
73
+
)}
74
74
+
75
75
+
{nonPinnedFeeds.length > 0 && (
76
76
+
<div>
77
77
+
<h2 className="text-lg font-semibold mb-3 px-4">Saved Feeds</h2>
78
78
+
<div className="flex flex-col">
79
79
+
{nonPinnedFeeds.map((feed: any) => (
80
80
+
<FeedItem key={feed.value} feedUri={feed.value} />
81
81
+
))}
82
82
+
</div>
83
83
+
</div>
84
84
+
)}
85
85
+
86
86
+
{savedFeeds.length === 0 && (
87
87
+
<div className="text-center text-gray-500 py-8 px-4">
88
88
+
<p>No feeds saved yet.</p>
89
89
+
<p className="mt-2">
90
90
+
Save feeds from the home page to see them here.
91
91
+
</p>
92
92
+
</div>
93
93
+
)}
94
94
+
</div>
24
95
</div>
25
96
);
26
97
}
98
98
+
99
99
+
function FeedItem({ feedUri }: { feedUri: string }) {
100
100
+
const { data: feedData } = useQueryArbitrary(feedUri);
101
101
+
const feed = feedData?.value as ATPAPI.AppBskyFeedGenerator.Record;
102
102
+
const [imgcdn] = useAtom(imgCDNAtom);
103
103
+
let aturi: ATPAPI.AtUri | null = null;
104
104
+
try {
105
105
+
aturi = new ATPAPI.AtUri(feedUri);
106
106
+
} catch (err) {
107
107
+
// todo terrible hack lmaoo (hack type: forcing following feed to fallback to rinds fresh feed)
108
108
+
aturi = new ATPAPI.AtUri("at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.generator/rinds");
109
109
+
}
110
110
+
111
111
+
function getAvatarUrl() {
112
112
+
const link = feed?.avatar?.ref?.["$link"];
113
113
+
if (!link) return null;
114
114
+
return `https://${imgcdn}/img/avatar/plain/${aturi?.host}/${link}@jpeg`;
115
115
+
}
116
116
+
117
117
+
const avatarUrl = getAvatarUrl();
118
118
+
119
119
+
return (
120
120
+
<Link
121
121
+
className="p-4 border-t border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer transition-colors"
122
122
+
to="/profile/$did/feed/$rkey"
123
123
+
params={{ did: aturi?.host, rkey: aturi?.rkey }}
124
124
+
onClick={(e) => {
125
125
+
e.stopPropagation();
126
126
+
}}
127
127
+
//disabled={feedUri === "following"}
128
128
+
>
129
129
+
<div className="flex items-center justify-between">
130
130
+
<div className="flex gap-3">
131
131
+
<img
132
132
+
src={avatarUrl || "/defaultpfp.png"}
133
133
+
alt={feed?.displayName || "Feed avatar"}
134
134
+
className="w-10 h-10 rounded-sm object-cover"
135
135
+
onError={(e) => {
136
136
+
const target = e.target as HTMLImageElement;
137
137
+
target.onerror = null;
138
138
+
target.src = "/defaultpfp.png";
139
139
+
}}
140
140
+
/>
141
141
+
<div>
142
142
+
<h3 className="font-medium text-gray-900 dark:text-gray-100">
143
143
+
{feed?.displayName || feedUri.split("/").pop()}
144
144
+
</h3>
145
145
+
<p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-1">
146
146
+
{feedUri === "following" ? "(not implemented, if clicked will open an alternative)" : feed?.description || "No description"}
147
147
+
</p>
148
148
+
</div>
149
149
+
</div>
150
150
+
<div className="text-gray-400">
151
151
+
<svg
152
152
+
xmlns="http://www.w3.org/2000/svg"
153
153
+
width="24"
154
154
+
height="24"
155
155
+
viewBox="0 0 24 24"
156
156
+
fill="none"
157
157
+
stroke="currentColor"
158
158
+
strokeWidth="2"
159
159
+
strokeLinecap="round"
160
160
+
strokeLinejoin="round"
161
161
+
>
162
162
+
<path d="M9 18l6-6-6-6"></path>
163
163
+
</svg>
164
164
+
</div>
165
165
+
</div>
166
166
+
</Link>
167
167
+
);
168
168
+
}