import { useEffect, useMemo, useState } from "react";
import { FlatList, Pressable, View } from "react-native";
import * as Haptics from "expo-haptics";
import { Link, router, Stack } from "expo-router";
import FullPageError from "@/components/FullPageError";
import ChevronRight from "@/components/ui/ChevronRight";
import FullPageSpinner from "@/components/ui/FullPageSpinner";
import { Text } from "@/components/ui/Text";
import { useColorScheme } from "@/lib/useColorScheme";
import { condProps } from "@/lib/utils";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Plus } from "lucide-react-native";
import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists";
import { useTRPC } from "@karakeep/shared-react/trpc";
import { ZBookmarkListTreeNode } from "@karakeep/shared/utils/listUtils";
function HeaderRight({ openNewListModal }: { openNewListModal: () => void }) {
return (
{
Haptics.selectionAsync();
openNewListModal();
}}
>
);
}
interface ListLink {
id: string;
logo: string;
name: string;
href: string;
level: number;
parent?: string;
numChildren: number;
collapsed: boolean;
isSharedSection?: boolean;
numBookmarks?: number;
}
function traverseTree(
node: ZBookmarkListTreeNode,
links: ListLink[],
showChildrenOf: Record,
listStats?: Map,
parent?: string,
level = 0,
) {
links.push({
id: node.item.id,
logo: node.item.icon,
name: node.item.name,
href: `/dashboard/lists/${node.item.id}`,
level,
parent,
numChildren: node.children?.length ?? 0,
collapsed: !showChildrenOf[node.item.id],
numBookmarks: listStats?.get(node.item.id),
});
if (node.children && showChildrenOf[node.item.id]) {
node.children.forEach((child) =>
traverseTree(
child,
links,
showChildrenOf,
listStats,
node.item.id,
level + 1,
),
);
}
}
export default function Lists() {
const { colors } = useColorScheme();
const [refreshing, setRefreshing] = useState(false);
const { data: lists, isPending, error, refetch } = useBookmarkLists();
const [showChildrenOf, setShowChildrenOf] = useState>(
{},
);
const api = useTRPC();
const queryClient = useQueryClient();
const { data: listStats } = useQuery(api.lists.stats.queryOptions());
// Check if there are any shared lists
const hasSharedLists = useMemo(() => {
return lists?.data.some((list) => list.userRole !== "owner") ?? false;
}, [lists?.data]);
// Check if any list has children to determine if we need chevron spacing
const hasAnyListsWithChildren = useMemo(() => {
const checkForChildren = (node: ZBookmarkListTreeNode): boolean => {
if (node.children && node.children.length > 0) return true;
return false;
};
return (
Object.values(lists?.root ?? {}).some(checkForChildren) || hasSharedLists
);
}, [lists?.root, hasSharedLists]);
useEffect(() => {
setRefreshing(isPending);
}, [isPending]);
if (error) {
return refetch()} />;
}
if (!lists) {
return ;
}
const onRefresh = () => {
queryClient.invalidateQueries(api.lists.list.pathFilter());
queryClient.invalidateQueries(api.lists.stats.pathFilter());
};
const links: ListLink[] = [
{
id: "fav",
logo: "⭐️",
name: "Favourites",
href: "/dashboard/favourites",
level: 0,
numChildren: 0,
collapsed: false,
},
{
id: "arch",
logo: "🗄️",
name: "Archive",
href: "/dashboard/archive",
level: 0,
numChildren: 0,
collapsed: false,
},
];
// Add shared lists section if there are any
if (hasSharedLists) {
// Count shared lists to determine if section has children
const sharedListsCount = Object.values(lists.root).filter(
(list) => list.item.userRole !== "owner",
).length;
links.push({
id: "shared-section",
logo: "👥",
name: "Shared Lists",
href: "#",
level: 0,
numChildren: sharedListsCount,
collapsed: !showChildrenOf["shared-section"],
isSharedSection: true,
});
// Add shared lists as children if section is expanded
if (showChildrenOf["shared-section"]) {
Object.values(lists.root).forEach((list) => {
if (list.item.userRole !== "owner") {
traverseTree(
list,
links,
showChildrenOf,
listStats?.stats,
"shared-section",
1,
);
}
});
}
}
// Add owned lists only
Object.values(lists.root).forEach((list) => {
if (list.item.userRole === "owner") {
traverseTree(list, links, showChildrenOf, listStats?.stats);
}
});
return (
<>
(
router.push("/dashboard/lists/new")}
/>
),
}}
/>
(
0,
props: { marginLeft: l.item.level * 20 },
}),
}}
>
{hasAnyListsWithChildren && (
{l.item.numChildren > 0 && (
{
setShowChildrenOf((prev) => ({
...prev,
[l.item.id]: !prev[l.item.id],
}));
}}
>
)}
)}
{l.item.isSharedSection ? (
{
setShowChildrenOf((prev) => ({
...prev,
[l.item.id]: !prev[l.item.id],
}));
}}
>
{l.item.logo} {l.item.name}
) : (
{l.item.logo} {l.item.name}
{l.item.numBookmarks !== undefined && (
{l.item.numBookmarks}
)}
)}
)}
data={links}
refreshing={refreshing}
onRefresh={onRefresh}
/>
>
);
}