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
wafrn bites
rimar1337
4 months ago
208521f9
48a6f09a
+203
-7
5 changed files
expand all
collapse all
unified
split
src
routes
notifications.tsx
profile.$did
index.tsx
settings.tsx
utils
atoms.ts
useQuery.ts
+76
src/routes/notifications.tsx
···
19
19
import { useAuth } from "~/providers/UnifiedAuthProvider";
20
20
import {
21
21
constellationURLAtom,
22
22
+
enableBitesAtom,
22
23
imgCDNAtom,
23
24
postInteractionsFiltersAtom,
24
25
} from "~/utils/atoms";
···
56
57
});
57
58
58
59
export default function NotificationsTabs() {
60
60
+
const [bitesEnabled] = useAtom(enableBitesAtom);
59
61
return (
60
62
<ReusableTabRoute
61
63
route={`Notifications`}
···
63
65
Mentions: <MentionsTab />,
64
66
Follows: <FollowsTab />,
65
67
"Post Interactions": <PostInteractionsTab />,
68
68
+
...bitesEnabled ? {
69
69
+
Bites: <BitesTab />,
70
70
+
} : {}
66
71
}}
67
72
/>
68
73
);
···
180
185
if (isError) return <ErrorState error={error} />;
181
186
182
187
if (!followsAturis?.length) return <EmptyState text="No follows yet." />;
188
188
+
189
189
+
return (
190
190
+
<>
191
191
+
{followsAturis.map((m) => (
192
192
+
<NotificationItem key={m} notification={m} />
193
193
+
))}
194
194
+
195
195
+
{hasNextPage && (
196
196
+
<button
197
197
+
onClick={() => fetchNextPage()}
198
198
+
disabled={isFetchingNextPage}
199
199
+
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 disabled:opacity-50"
200
200
+
>
201
201
+
{isFetchingNextPage ? "Loading..." : "Load More"}
202
202
+
</button>
203
203
+
)}
204
204
+
</>
205
205
+
);
206
206
+
}
207
207
+
208
208
+
209
209
+
export function BitesTab({did}:{did?:string}) {
210
210
+
const { agent } = useAuth();
211
211
+
const userdidunsafe = did ?? agent?.did;
212
212
+
const { data: identity} = useQueryIdentity(userdidunsafe);
213
213
+
const userdid = identity?.did;
214
214
+
215
215
+
const [constellationurl] = useAtom(constellationURLAtom);
216
216
+
const infinitequeryresults = useInfiniteQuery({
217
217
+
...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(
218
218
+
{
219
219
+
constellation: constellationurl,
220
220
+
method: "/links",
221
221
+
target: "at://"+userdid,
222
222
+
collection: "net.wafrn.feed.bite",
223
223
+
path: ".subject",
224
224
+
staleMult: 0 // safe fun
225
225
+
}
226
226
+
),
227
227
+
enabled: !!userdid,
228
228
+
});
229
229
+
230
230
+
const {
231
231
+
data: infiniteFollowsData,
232
232
+
fetchNextPage,
233
233
+
hasNextPage,
234
234
+
isFetchingNextPage,
235
235
+
isLoading,
236
236
+
isError,
237
237
+
error,
238
238
+
} = infinitequeryresults;
239
239
+
240
240
+
const followsAturis = React.useMemo(() => {
241
241
+
// Get all replies from the standard infinite query
242
242
+
return (
243
243
+
infiniteFollowsData?.pages.flatMap(
244
244
+
(page) =>
245
245
+
page?.linking_records.map(
246
246
+
(r) => `at://${r.did}/${r.collection}/${r.rkey}`
247
247
+
) ?? []
248
248
+
) ?? []
249
249
+
);
250
250
+
}, [infiniteFollowsData]);
251
251
+
252
252
+
useReusableTabScrollRestore("Notifications");
253
253
+
254
254
+
if (isLoading) return <LoadingState text="Loading bites..." />;
255
255
+
if (isError) return <ErrorState error={error} />;
256
256
+
257
257
+
if (!followsAturis?.length) return <EmptyState text="No bites yet." />;
183
258
184
259
return (
185
260
<>
···
499
574
500
575
export function NotificationItem({ notification }: { notification: string }) {
501
576
const aturi = new AtUri(notification);
577
577
+
const bite = aturi.collection === "net.wafrn.feed.bite";
502
578
const navigate = useNavigate();
503
579
const { data: identity } = useQueryIdentity(aturi.host);
504
580
const resolvedDid = identity?.did;
+58
-2
src/routes/profile.$did/index.tsx
···
1
1
-
import { RichText } from "@atproto/api";
1
1
+
import { Agent, RichText } from "@atproto/api";
2
2
import * as ATPAPI from "@atproto/api";
3
3
+
import { TID } from "@atproto/common-web";
3
4
import { useQueryClient } from "@tanstack/react-query";
4
5
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
5
6
import { useAtom } from "jotai";
···
16
17
UniversalPostRendererATURILoader,
17
18
} from "~/components/UniversalPostRenderer";
18
19
import { useAuth } from "~/providers/UnifiedAuthProvider";
19
19
-
import { imgCDNAtom, profileChipsAtom } from "~/utils/atoms";
20
20
+
import { enableBitesAtom, imgCDNAtom, profileChipsAtom } from "~/utils/atoms";
20
21
import {
21
22
toggleFollow,
22
23
useGetFollowState,
···
143
144
</div>
144
145
145
146
<div className="absolute right-[16px] top-[170px] flex flex-row gap-2.5">
147
147
+
<BiteButton targetdidorhandle={did} />
146
148
{/*
147
149
todo: full follow and unfollow backfill (along with partial likes backfill,
148
150
just enough for it to be useful)
···
810
812
</>
811
813
);
812
814
}
815
815
+
816
816
+
export function BiteButton({
817
817
+
targetdidorhandle,
818
818
+
}: {
819
819
+
targetdidorhandle: string;
820
820
+
}) {
821
821
+
const { agent } = useAuth();
822
822
+
const { data: identity } = useQueryIdentity(targetdidorhandle);
823
823
+
const [show] = useAtom(enableBitesAtom);
824
824
+
825
825
+
if (!show) return
826
826
+
827
827
+
return (
828
828
+
<>
829
829
+
<button
830
830
+
onClick={(e) => {
831
831
+
e.stopPropagation();
832
832
+
sendBite({
833
833
+
agent: agent || undefined,
834
834
+
targetDid: identity?.did,
835
835
+
});
836
836
+
}}
837
837
+
className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"
838
838
+
>
839
839
+
Bite
840
840
+
</button>
841
841
+
</>
842
842
+
);
843
843
+
}
844
844
+
845
845
+
function sendBite({
846
846
+
agent,
847
847
+
targetDid,
848
848
+
}: {
849
849
+
agent?: Agent;
850
850
+
targetDid?: string;
851
851
+
}) {
852
852
+
if (!agent?.did || !targetDid) return;
853
853
+
const newRecord = {
854
854
+
repo: agent.did,
855
855
+
collection: "net.wafrn.feed.bite",
856
856
+
rkey: TID.next().toString(),
857
857
+
record: {
858
858
+
$type: "net.wafrn.feed.bite",
859
859
+
subject: "at://"+targetDid,
860
860
+
createdAt: new Date().toISOString(),
861
861
+
},
862
862
+
};
863
863
+
864
864
+
agent.com.atproto.repo.createRecord(newRecord).catch((err) => {
865
865
+
console.error("Bite failed:", err);
866
866
+
});
867
867
+
}
868
868
+
813
869
814
870
export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) {
815
871
const { agent } = useAuth();
+55
-2
src/routes/settings.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
2
-
import { useAtom } from "jotai";
3
3
-
import { Slider } from "radix-ui";
2
2
+
import { useAtom, useAtomValue, useSetAtom } from "jotai";
3
3
+
import { Slider, Switch } from "radix-ui";
4
4
+
import { useEffect,useState } from "react";
4
5
5
6
import { Header } from "~/components/Header";
6
7
import Login from "~/components/Login";
···
11
12
defaultImgCDN,
12
13
defaultslingshotURL,
13
14
defaultVideoCDN,
15
15
+
enableBitesAtom,
14
16
hueAtom,
15
17
imgCDNAtom,
16
18
slingshotURLAtom,
···
68
70
/>
69
71
70
72
<Hue />
73
73
+
<SwitchSetting
74
74
+
atom={enableBitesAtom}
75
75
+
title={"Bites"}
76
76
+
description={"Enable Wafrn Bites"}
77
77
+
//init={false}
78
78
+
/>
71
79
<p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm">
72
80
please restart/refresh the app if changes arent applying correctly
73
81
</p>
74
82
</>
75
83
);
76
84
}
85
85
+
86
86
+
export function SwitchSetting({
87
87
+
atom,
88
88
+
title,
89
89
+
description,
90
90
+
}: {
91
91
+
atom: typeof enableBitesAtom;
92
92
+
title?: string;
93
93
+
description?: string;
94
94
+
}) {
95
95
+
const value = useAtomValue(atom);
96
96
+
const setValue = useSetAtom(atom);
97
97
+
98
98
+
const [hydrated, setHydrated] = useState(false);
99
99
+
// eslint-disable-next-line react-hooks/set-state-in-effect
100
100
+
useEffect(() => setHydrated(true), []);
101
101
+
102
102
+
if (!hydrated) {
103
103
+
// Avoid rendering Switch until we know storage is loaded
104
104
+
return null;
105
105
+
}
106
106
+
107
107
+
return (
108
108
+
<div className="flex items-center gap-4 px-4 py-2">
109
109
+
<div className="flex flex-col">
110
110
+
<label htmlFor="switch-demo" className="text-lg">
111
111
+
{title}
112
112
+
</label>
113
113
+
<span className="text-sm">{description}</span>
114
114
+
</div>
115
115
+
116
116
+
<Switch.Root
117
117
+
id="switch-demo"
118
118
+
checked={value}
119
119
+
onCheckedChange={(v) => setValue(v)}
120
120
+
className="w-10 h-6 bg-gray-300 rounded-full relative data-[state=checked]:bg-blue-500 transition-colors"
121
121
+
>
122
122
+
<Switch.Thumb
123
123
+
className="block w-5 h-5 bg-white rounded-full shadow-sm transition-transform translate-x-[2px] data-[state=checked]:translate-x-[20px]"
124
124
+
/>
125
125
+
</Switch.Root>
126
126
+
</div>
127
127
+
);
128
128
+
}
129
129
+
77
130
function Hue() {
78
131
const [hue, setHue] = useAtom(hueAtom);
79
132
return (
+9
src/utils/atoms.ts
···
128
128
// console.log("atom get ", initial);
129
129
// document.documentElement.style.setProperty(cssVar, initial.toString());
130
130
// }
131
131
+
132
132
+
133
133
+
134
134
+
// fun stuff
135
135
+
136
136
+
export const enableBitesAtom = atomWithStorage<boolean>(
137
137
+
"enableBitesAtom",
138
138
+
false
139
139
+
);
+5
-3
src/utils/useQuery.ts
···
654
654
method: '/links'
655
655
target?: string
656
656
collection: string
657
657
-
path: string
657
657
+
path: string,
658
658
+
staleMult?: number
658
659
}) {
660
660
+
const safemult = query?.staleMult || 1;
659
661
// console.log(
660
662
// 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks',
661
663
// query,
···
697
699
return (lastPage as any)?.cursor ?? undefined
698
700
},
699
701
initialPageParam: undefined,
700
700
-
staleTime: 5 * 60 * 1000,
701
701
-
gcTime: 5 * 60 * 1000,
702
702
+
staleTime: 5 * 60 * 1000 * safemult,
703
703
+
gcTime: 5 * 60 * 1000 * safemult,
702
704
})
703
705
}