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 ? (

) : (
)}
{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."}
);