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