import { AtUri } from "@atproto/api"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useAtom } from "jotai"; import * as React from "react"; import defaultpfp from "~/../public/favicon.png"; import { Header } from "~/components/Header"; import { ReusableTabRoute, useReusableTabScrollRestore, } from "~/components/ReusableTabRoute"; import { MdiCardsHeartOutline, MdiCommentOutline, MdiRepeat, UniversalPostRendererATURILoader, } from "~/components/UniversalPostRenderer"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { constellationURLAtom, enableBitesAtom, imgCDNAtom, postInteractionsFiltersAtom, } from "~/utils/atoms"; import { useInfiniteQueryAuthorFeed, useQueryConstellation, useQueryIdentity, useQueryProfile, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, } from "~/utils/useQuery"; import { FollowButton, Mutual } from "./profile.$did"; export function NotificationsComponent() { return (
{ if (window.history.length > 1) { window.history.back(); } else { window.location.assign("/"); } }} bottomBorderDisabled={true} />
); } export const Route = createFileRoute("/notifications")({ component: NotificationsComponent, }); export default function NotificationsTabs() { const [bitesEnabled] = useAtom(enableBitesAtom); return ( , Follows: , "Post Interactions": , ...bitesEnabled ? { Bites: , } : {} }} /> ); } function MentionsTab() { const { agent } = useAuth(); const [constellationurl] = useAtom(constellationURLAtom); const infinitequeryresults = useInfiniteQuery({ ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( { constellation: constellationurl, method: "/links", target: agent?.did, collection: "app.bsky.feed.post", path: ".facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did", } ), enabled: !!agent?.did, }); const { data: infiniteMentionsData, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, error, } = infinitequeryresults; const mentionsAturis = React.useMemo(() => { // Get all replies from the standard infinite query return ( infiniteMentionsData?.pages.flatMap( (page) => page?.linking_records.map( (r) => `at://${r.did}/${r.collection}/${r.rkey}` ) ?? [] ) ?? [] ); }, [infiniteMentionsData]); useReusableTabScrollRestore("Notifications"); if (isLoading) return ; if (isError) return ; if (!mentionsAturis?.length) return ; return ( <> {mentionsAturis.map((m) => ( ))} {hasNextPage && ( )} ); } export function FollowsTab({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: userdid, collection: "app.bsky.graph.follow", path: ".subject", } ), 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 && ( )} ); } 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); const queryClient = useQueryClient(); const { data: postsData, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading: arePostsLoading, } = useInfiniteQueryAuthorFeed(agent?.did, identity?.pds); React.useEffect(() => { if (postsData) { postsData.pages.forEach((page) => { page.records.forEach((record) => { if (!queryClient.getQueryData(["post", record.uri])) { queryClient.setQueryData(["post", record.uri], record); } }); }); } }, [postsData, queryClient]); const posts = React.useMemo( () => postsData?.pages.flatMap((page) => page.records) ?? [], [postsData] ); useReusableTabScrollRestore("Notifications"); const [filters] = useAtom(postInteractionsFiltersAtom); const empty = (!filters.likes && !filters.quotes && !filters.replies && !filters.reposts); return ( <> {!empty && posts.map((m) => ( ))} {hasNextPage && ( )} ); } function PostInteractionsFilterChipBar() { const [filters, setFilters] = useAtom(postInteractionsFiltersAtom); // const empty = (!filters.likes && !filters.quotes && !filters.replies && !filters.reposts); // useEffect(() => { // if (empty) { // setFilters((prev) => ({ // ...prev, // likes: true, // })); // } // }, [ // empty, // setFilters, // ]); const toggle = (key: keyof typeof filters) => { setFilters((prev) => ({ ...prev, [key]: !prev[key], })); }; return (
toggle("likes")} /> toggle("reposts")} /> toggle("replies")} /> toggle("quotes")} /> toggle("showAll")} />
); } export function Chip({ state, text, onClick, }: { state: boolean; text: string; onClick: React.MouseEventHandler; }) { return ( ); } function PostInteractionsItem({ uri }: { uri: string }) { const [filters] = useAtom(postInteractionsFiltersAtom); const { data: links } = useQueryConstellation({ method: "/links/all", target: uri, }); const likes = links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0; const replies = links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"]?.records || 0; const reposts = links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records || 0; const quotes1 = links?.links?.["app.bsky.feed.post"]?.[".embed.record.uri"]?.records || 0; const quotes2 = links?.links?.["app.bsky.feed.post"]?.[".embed.record.record.uri"] ?.records || 0; const quotes = quotes1 + quotes2; const all = likes + replies + reposts + quotes; //const failLikes = filters.likes && likes < 1; //const failReposts = filters.reposts && reposts < 1; //const failReplies = filters.replies && replies < 1; //const failQuotes = filters.quotes && quotes < 1; const showLikes = filters.showAll || filters.likes const showReposts = filters.showAll || filters.reposts const showReplies = filters.showAll || filters.replies const showQuotes = filters.showAll || filters.quotes //const showNone = !showLikes && !showReposts && !showReplies && !showQuotes; //const fail = failLikes || failReposts || failReplies || failQuotes || showNone; const matchesLikes = filters.likes && likes > 0; const matchesReposts = filters.reposts && reposts > 0; const matchesReplies = filters.replies && replies > 0; const matchesQuotes = filters.quotes && quotes > 0; const matchesAnything = // filters.showAll || matchesLikes || matchesReposts || matchesReplies || matchesQuotes; if (!matchesAnything) return null; //if (fail) return; return (
{/* fail likes {failLikes ? "true" : "false"} fail repost {failReposts ? "true" : "false"} fail reply {failReplies ? "true" : "false"} fail qupte {failQuotes ? "true" : "false"} */}
{showLikes &&()} {showReposts && ()} {showReplies && ()} {showQuotes && ()} {!all && (
No interactions yet.
)}
); } function InteractionsButton({ type, uri, count, }: { type: "reply" | "repost" | "like" | "quote"; uri: string; count: number; }) { if (!count) return <>; const aturi = new AtUri(uri); return ( {type === "like" ? ( ) : type === "repost" ? ( ) : type === "reply" ? ( ) : type === "quote" ? ( ) : ( <> )} {type === "like" ? "likes" : type === "reply" ? "replies" : type === "quote" ? "quotes" : type === "repost" ? "reposts" : ""}
{count} ); } export function NotificationItem({ notification, labeler }: { notification: string, labeler?: boolean }) { 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; const profileUri = resolvedDid ? `at://${resolvedDid}/app.bsky.actor.profile/self` : undefined; const { data: profileRecord } = useQueryProfile(profileUri); const profile = profileRecord?.value; const [imgcdn] = useAtom(imgCDNAtom); function getAvatarUrl(p: typeof profile) { const link = p?.avatar?.ref?.["$link"]; if (!link || !resolvedDid) return null; return `https://${imgcdn}/img/avatar/plain/${resolvedDid}/${link}@jpeg`; } const avatar = getAvatarUrl(profile); return (
aturi && navigate({ to: "/profile/$did", params: { did: aturi.host }, }) } > {/*
{aturi.collection === "app.bsky.graph.follow" ? ( ) : aturi.collection === "app.bsky.feed.like" ? ( ) : ( <> )}
*/} {profile ? ( {identity?.handle} ) : (
)}
{profile?.displayName || identity?.handle || "Someone"} @{identity?.handle}
{identity?.did && } {/* followed you */}
{identity?.did && }
); } export const EmptyState = ({ text }: { text: string }) => (
{text}
); export const LoadingState = ({ text }: { text: string }) => (
{text}
); export const ErrorState = ({ error }: { error: unknown }) => (
Error: {(error as Error)?.message || "Something went wrong."}
);