diff --git a/src/routes/notifications.tsx b/src/routes/notifications.tsx index 4fd5c3d..eefd301 100644 --- a/src/routes/notifications.tsx +++ b/src/routes/notifications.tsx @@ -19,6 +19,7 @@ import { import { useAuth } from "~/providers/UnifiedAuthProvider"; import { constellationURLAtom, + enableBitesAtom, imgCDNAtom, postInteractionsFiltersAtom, } from "~/utils/atoms"; @@ -56,6 +57,7 @@ export const Route = createFileRoute("/notifications")({ }); export default function NotificationsTabs() { + const [bitesEnabled] = useAtom(enableBitesAtom); return ( , Follows: , "Post Interactions": , + ...bitesEnabled ? { + Bites: , + } : {} }} /> ); @@ -200,6 +205,76 @@ export function FollowsTab({did}:{did?:string}) { ); } + +export function BitesTab({did}:{did?:string}) { + const { agent } = useAuth(); + const userdidunsafe = did ?? agent?.did; + const { data: identity} = useQueryIdentity(userdidunsafe); + const userdid = identity?.did; + + const [constellationurl] = useAtom(constellationURLAtom); + const infinitequeryresults = useInfiniteQuery({ + ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( + { + constellation: constellationurl, + method: "/links", + target: "at://"+userdid, + collection: "net.wafrn.feed.bite", + path: ".subject", + staleMult: 0 // safe fun + } + ), + enabled: !!userdid, + }); + + const { + data: infiniteFollowsData, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + isError, + error, + } = infinitequeryresults; + + const followsAturis = React.useMemo(() => { + // Get all replies from the standard infinite query + return ( + infiniteFollowsData?.pages.flatMap( + (page) => + page?.linking_records.map( + (r) => `at://${r.did}/${r.collection}/${r.rkey}` + ) ?? [] + ) ?? [] + ); + }, [infiniteFollowsData]); + + useReusableTabScrollRestore("Notifications"); + + if (isLoading) return ; + if (isError) return ; + + if (!followsAturis?.length) return ; + + return ( + <> + {followsAturis.map((m) => ( + + ))} + + {hasNextPage && ( + + )} + + ); +} + function PostInteractionsTab() { const { agent } = useAuth(); const { data: identity } = useQueryIdentity(agent?.did); @@ -499,6 +574,7 @@ function InteractionsButton({ export function NotificationItem({ notification }: { notification: string }) { const aturi = new AtUri(notification); + const bite = aturi.collection === "net.wafrn.feed.bite"; const navigate = useNavigate(); const { data: identity } = useQueryIdentity(aturi.host); const resolvedDid = identity?.did; diff --git a/src/routes/profile.$did/index.tsx b/src/routes/profile.$did/index.tsx index 3d6e0ed..705b99f 100644 --- a/src/routes/profile.$did/index.tsx +++ b/src/routes/profile.$did/index.tsx @@ -1,5 +1,6 @@ -import { RichText } from "@atproto/api"; +import { Agent, RichText } from "@atproto/api"; import * as ATPAPI from "@atproto/api"; +import { TID } from "@atproto/common-web"; import { useQueryClient } from "@tanstack/react-query"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useAtom } from "jotai"; @@ -16,7 +17,7 @@ import { UniversalPostRendererATURILoader, } from "~/components/UniversalPostRenderer"; import { useAuth } from "~/providers/UnifiedAuthProvider"; -import { imgCDNAtom, profileChipsAtom } from "~/utils/atoms"; +import { enableBitesAtom, imgCDNAtom, profileChipsAtom } from "~/utils/atoms"; import { toggleFollow, useGetFollowState, @@ -143,6 +144,7 @@ function ProfileComponent() {
+ {/* todo: full follow and unfollow backfill (along with partial likes backfill, just enough for it to be useful) @@ -811,6 +813,60 @@ export function FollowButton({ ); } +export function BiteButton({ + targetdidorhandle, +}: { + targetdidorhandle: string; +}) { + const { agent } = useAuth(); + const { data: identity } = useQueryIdentity(targetdidorhandle); + const [show] = useAtom(enableBitesAtom); + + if (!show) return + + return ( + <> + + + ); +} + +function sendBite({ + agent, + targetDid, +}: { + agent?: Agent; + targetDid?: string; +}) { + if (!agent?.did || !targetDid) return; + const newRecord = { + repo: agent.did, + collection: "net.wafrn.feed.bite", + rkey: TID.next().toString(), + record: { + $type: "net.wafrn.feed.bite", + subject: "at://"+targetDid, + createdAt: new Date().toISOString(), + }, + }; + + agent.com.atproto.repo.createRecord(newRecord).catch((err) => { + console.error("Bite failed:", err); + }); +} + + export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) { const { agent } = useAuth(); const { data: identity } = useQueryIdentity(targetdidorhandle); diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx index f6552d9..f9becfd 100644 --- a/src/routes/settings.tsx +++ b/src/routes/settings.tsx @@ -1,6 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; -import { useAtom } from "jotai"; -import { Slider } from "radix-ui"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { Slider, Switch } from "radix-ui"; +import { useEffect,useState } from "react"; import { Header } from "~/components/Header"; import Login from "~/components/Login"; @@ -11,6 +12,7 @@ import { defaultImgCDN, defaultslingshotURL, defaultVideoCDN, + enableBitesAtom, hueAtom, imgCDNAtom, slingshotURLAtom, @@ -68,12 +70,63 @@ export function Settings() { /> +

please restart/refresh the app if changes arent applying correctly

); } + +export function SwitchSetting({ + atom, + title, + description, +}: { + atom: typeof enableBitesAtom; + title?: string; + description?: string; +}) { + const value = useAtomValue(atom); + const setValue = useSetAtom(atom); + + const [hydrated, setHydrated] = useState(false); + // eslint-disable-next-line react-hooks/set-state-in-effect + useEffect(() => setHydrated(true), []); + + if (!hydrated) { + // Avoid rendering Switch until we know storage is loaded + return null; + } + + return ( +
+
+ + {description} +
+ + setValue(v)} + className="w-10 h-6 bg-gray-300 rounded-full relative data-[state=checked]:bg-blue-500 transition-colors" + > + + +
+ ); +} + function Hue() { const [hue, setHue] = useAtom(hueAtom); return ( diff --git a/src/utils/atoms.ts b/src/utils/atoms.ts index 470a30d..e831180 100644 --- a/src/utils/atoms.ts +++ b/src/utils/atoms.ts @@ -128,3 +128,12 @@ hueAtom.onMount = (setAtom) => { // console.log("atom get ", initial); // document.documentElement.style.setProperty(cssVar, initial.toString()); // } + + + +// fun stuff + +export const enableBitesAtom = atomWithStorage( + "enableBitesAtom", + false +); diff --git a/src/utils/useQuery.ts b/src/utils/useQuery.ts index 6991db5..0d3d10a 100644 --- a/src/utils/useQuery.ts +++ b/src/utils/useQuery.ts @@ -654,8 +654,10 @@ export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfi method: '/links' target?: string collection: string - path: string + path: string, + staleMult?: number }) { + const safemult = query?.staleMult || 1; // console.log( // 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', // query, @@ -697,7 +699,7 @@ export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfi return (lastPage as any)?.cursor ?? undefined }, initialPageParam: undefined, - staleTime: 5 * 60 * 1000, - gcTime: 5 * 60 * 1000, + staleTime: 5 * 60 * 1000 * safemult, + gcTime: 5 * 60 * 1000 * safemult, }) } \ No newline at end of file