+76
src/routes/notifications.tsx
+76
src/routes/notifications.tsx
···
19
19
import { useAuth } from "~/providers/UnifiedAuthProvider";
20
20
import {
21
21
constellationURLAtom,
22
+
enableBitesAtom,
22
23
imgCDNAtom,
23
24
postInteractionsFiltersAtom,
24
25
} from "~/utils/atoms";
···
56
57
});
57
58
58
59
export default function NotificationsTabs() {
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
+
...bitesEnabled ? {
69
+
Bites: <BitesTab />,
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
+
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." />;
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
+
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
+58
-2
src/routes/profile.$did/index.tsx
···
1
-
import { RichText } from "@atproto/api";
1
+
import { Agent, RichText } from "@atproto/api";
2
2
import * as ATPAPI from "@atproto/api";
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
-
import { imgCDNAtom, profileChipsAtom } from "~/utils/atoms";
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
+
<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
+
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
+
813
869
814
870
export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) {
815
871
const { agent } = useAuth();
+55
-2
src/routes/settings.tsx
+55
-2
src/routes/settings.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
-
import { useAtom } from "jotai";
3
-
import { Slider } from "radix-ui";
2
+
import { useAtom, useAtomValue, useSetAtom } from "jotai";
3
+
import { Slider, Switch } from "radix-ui";
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
+
enableBitesAtom,
14
16
hueAtom,
15
17
imgCDNAtom,
16
18
slingshotURLAtom,
···
68
70
/>
69
71
70
72
<Hue />
73
+
<SwitchSetting
74
+
atom={enableBitesAtom}
75
+
title={"Bites"}
76
+
description={"Enable Wafrn Bites"}
77
+
//init={false}
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
+
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
+
77
130
function Hue() {
78
131
const [hue, setHue] = useAtom(hueAtom);
79
132
return (
+9
src/utils/atoms.ts
+9
src/utils/atoms.ts
+5
-3
src/utils/useQuery.ts
+5
-3
src/utils/useQuery.ts
···
654
654
method: '/links'
655
655
target?: string
656
656
collection: string
657
-
path: string
657
+
path: string,
658
+
staleMult?: number
658
659
}) {
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
-
staleTime: 5 * 60 * 1000,
701
-
gcTime: 5 * 60 * 1000,
702
+
staleTime: 5 * 60 * 1000 * safemult,
703
+
gcTime: 5 * 60 * 1000 * safemult,
702
704
})
703
705
}