2025-05-10 14:16:34 -04:00
|
|
|
import { Trans } from "@lingui/react/macro";
|
|
|
|
|
import { Box, Stack } from "@mantine/core";
|
|
|
|
|
import { Constants } from "app/constants";
|
2024-06-13 21:54:14 +02:00
|
|
|
import {
|
2025-05-10 14:16:34 -04:00
|
|
|
redirectToCategory,
|
|
|
|
|
redirectToCategoryDetails,
|
|
|
|
|
redirectToFeed,
|
|
|
|
|
redirectToFeedDetails,
|
|
|
|
|
redirectToTag,
|
|
|
|
|
redirectToTagDetails,
|
|
|
|
|
} from "app/redirect/thunks";
|
|
|
|
|
import { useAppDispatch, useAppSelector } from "app/store";
|
|
|
|
|
import { collapseTreeCategory } from "app/tree/thunks";
|
2025-05-15 20:19:05 -04:00
|
|
|
import type { Category, Subscription, TreeSubscription } from "app/types";
|
2025-05-10 14:16:34 -04:00
|
|
|
import { categoryUnreadCount, flattenCategoryTree } from "app/utils";
|
|
|
|
|
import { Loader } from "components/Loader";
|
|
|
|
|
import { OnDesktop } from "components/responsive/OnDesktop";
|
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
|
|
|
import {
|
|
|
|
|
TbChevronDown,
|
|
|
|
|
TbChevronRight,
|
|
|
|
|
TbInbox,
|
|
|
|
|
TbStar,
|
|
|
|
|
TbTag,
|
|
|
|
|
} from "react-icons/tb";
|
|
|
|
|
import { TreeNode } from "./TreeNode";
|
|
|
|
|
import { TreeSearch } from "./TreeSearch";
|
|
|
|
|
|
|
|
|
|
const allIcon = <TbInbox size={16} />;
|
|
|
|
|
const starredIcon = <TbStar size={16} />;
|
|
|
|
|
const tagIcon = <TbTag size={16} />;
|
|
|
|
|
const expandedIcon = <TbChevronDown size={16} />;
|
|
|
|
|
const collapsedIcon = <TbChevronRight size={16} />;
|
|
|
|
|
|
|
|
|
|
const errorThreshold = 9;
|
2024-06-13 21:54:14 +02:00
|
|
|
|
|
|
|
|
export function Tree() {
|
2025-05-10 14:16:34 -04:00
|
|
|
const root = useAppSelector((state) => state.tree.rootCategory);
|
|
|
|
|
const source = useAppSelector((state) => state.entries.source);
|
|
|
|
|
const tags = useAppSelector((state) => state.user.tags);
|
|
|
|
|
const showRead = useAppSelector((state) => state.user.settings?.showRead);
|
|
|
|
|
const dispatch = useAppDispatch();
|
2024-09-06 09:05:43 +02:00
|
|
|
|
2025-05-10 14:16:34 -04:00
|
|
|
const isFeedDisplayed = (feed: Subscription) => {
|
|
|
|
|
const isCurrentFeed =
|
|
|
|
|
source.type === "feed" && source.id === String(feed.id);
|
|
|
|
|
return isCurrentFeed || feed.unread > 0 || showRead;
|
|
|
|
|
};
|
2024-09-06 09:05:43 +02:00
|
|
|
|
2025-05-10 14:16:34 -04:00
|
|
|
const isCategoryDisplayed = (category: Category): boolean => {
|
|
|
|
|
const isCurrentCategory =
|
|
|
|
|
source.type === "category" && source.id === category.id;
|
|
|
|
|
return (
|
|
|
|
|
isCurrentCategory ||
|
|
|
|
|
showRead ||
|
|
|
|
|
category.children.some((c) => isCategoryDisplayed(c)) ||
|
|
|
|
|
category.feeds.some((f) => isFeedDisplayed(f))
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const feedClicked = (e: React.MouseEvent, id: string) => {
|
|
|
|
|
if (e.detail === 2) {
|
|
|
|
|
dispatch(redirectToFeedDetails(id));
|
|
|
|
|
} else {
|
|
|
|
|
dispatch(redirectToFeed(id));
|
2024-06-13 21:54:14 +02:00
|
|
|
}
|
2025-05-10 14:16:34 -04:00
|
|
|
};
|
|
|
|
|
const categoryClicked = (e: React.MouseEvent, id: string) => {
|
|
|
|
|
if (e.detail === 2) {
|
|
|
|
|
dispatch(redirectToCategoryDetails(id));
|
|
|
|
|
} else {
|
|
|
|
|
dispatch(redirectToCategory(id));
|
2024-06-13 21:54:14 +02:00
|
|
|
}
|
2025-05-10 14:16:34 -04:00
|
|
|
};
|
|
|
|
|
const categoryIconClicked = (e: React.MouseEvent, category: Category) => {
|
|
|
|
|
e.stopPropagation();
|
2024-06-13 21:54:14 +02:00
|
|
|
|
2025-05-10 14:16:34 -04:00
|
|
|
dispatch(
|
|
|
|
|
collapseTreeCategory({
|
|
|
|
|
id: +category.id,
|
|
|
|
|
collapse: category.expanded,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
const tagClicked = (e: React.MouseEvent, id: string) => {
|
|
|
|
|
if (e.detail === 2) {
|
|
|
|
|
dispatch(redirectToTagDetails(id));
|
|
|
|
|
} else {
|
|
|
|
|
dispatch(redirectToTag(id));
|
2024-06-13 21:54:14 +02:00
|
|
|
}
|
2025-05-10 14:16:34 -04:00
|
|
|
};
|
|
|
|
|
|
2025-05-15 20:19:05 -04:00
|
|
|
console.log(root?.feeds.map(f => f.hasNewEntries));
|
2024-06-13 21:54:14 +02:00
|
|
|
|
2025-05-10 14:16:34 -04:00
|
|
|
const allCategoryNode = () => (
|
|
|
|
|
|
|
|
|
|
<TreeNode
|
|
|
|
|
id={Constants.categories.all.id}
|
|
|
|
|
type="category"
|
|
|
|
|
name={<Trans>All</Trans>}
|
|
|
|
|
icon={allIcon}
|
|
|
|
|
unread={categoryUnreadCount(root)}
|
|
|
|
|
selected={
|
|
|
|
|
source.type === "category" && source.id === Constants.categories.all.id
|
|
|
|
|
}
|
|
|
|
|
expanded={false}
|
|
|
|
|
level={0}
|
|
|
|
|
hasError={false}
|
|
|
|
|
onClick={categoryClicked}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
const starredCategoryNode = () => (
|
|
|
|
|
<TreeNode
|
|
|
|
|
id={Constants.categories.starred.id}
|
|
|
|
|
type="category"
|
|
|
|
|
name={<Trans>Starred</Trans>}
|
|
|
|
|
icon={starredIcon}
|
|
|
|
|
unread={0}
|
|
|
|
|
selected={
|
|
|
|
|
source.type === "category" &&
|
|
|
|
|
source.id === Constants.categories.starred.id
|
|
|
|
|
}
|
|
|
|
|
expanded={false}
|
|
|
|
|
level={0}
|
|
|
|
|
hasError={false}
|
|
|
|
|
onClick={categoryClicked}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const categoryNode = (category: Category, level = 0) => {
|
|
|
|
|
if (!isCategoryDisplayed(category)) return null;
|
|
|
|
|
|
|
|
|
|
const hasError =
|
|
|
|
|
!category.expanded &&
|
|
|
|
|
flattenCategoryTree(category).some((c) =>
|
|
|
|
|
c.feeds.some((f) => f.errorCount > errorThreshold)
|
|
|
|
|
);
|
2024-06-13 21:54:14 +02:00
|
|
|
return (
|
2025-05-10 14:16:34 -04:00
|
|
|
<TreeNode
|
|
|
|
|
id={category.id}
|
|
|
|
|
type="category"
|
|
|
|
|
name={category.name}
|
|
|
|
|
icon={category.expanded ? expandedIcon : collapsedIcon}
|
|
|
|
|
unread={categoryUnreadCount(category)}
|
|
|
|
|
selected={source.type === "category" && source.id === category.id}
|
|
|
|
|
expanded={category.expanded}
|
|
|
|
|
level={level}
|
|
|
|
|
hasError={hasError}
|
|
|
|
|
onClick={categoryClicked}
|
|
|
|
|
onIconClick={(e) => categoryIconClicked(e, category)}
|
|
|
|
|
key={category.id}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-15 20:19:05 -04:00
|
|
|
const feedNode = (feed: TreeSubscription, level = 0) => {
|
2025-05-10 14:16:34 -04:00
|
|
|
if (!isFeedDisplayed(feed)) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<TreeNode
|
|
|
|
|
id={String(feed.id)}
|
|
|
|
|
type="feed"
|
|
|
|
|
name={feed.name}
|
|
|
|
|
icon={feed.iconUrl}
|
|
|
|
|
unread={feed.unread}
|
|
|
|
|
selected={source.type === "feed" && source.id === String(feed.id)}
|
|
|
|
|
level={level}
|
|
|
|
|
hasError={feed.errorCount > errorThreshold}
|
|
|
|
|
onClick={feedClicked}
|
|
|
|
|
key={feed.id}
|
2025-05-15 20:19:05 -04:00
|
|
|
newMessages={feed.hasNewEntries}
|
2025-05-10 14:16:34 -04:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const tagNode = (tag: string) => (
|
|
|
|
|
<TreeNode
|
|
|
|
|
id={tag}
|
|
|
|
|
type="tag"
|
|
|
|
|
name={tag}
|
|
|
|
|
icon={tagIcon}
|
|
|
|
|
unread={0}
|
|
|
|
|
selected={source.type === "tag" && source.id === tag}
|
|
|
|
|
level={0}
|
|
|
|
|
hasError={false}
|
|
|
|
|
onClick={tagClicked}
|
|
|
|
|
key={tag}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const recursiveCategoryNode = (category: Category, level = 0) => (
|
|
|
|
|
<React.Fragment key={`recursiveCategoryNode-${category.id}`}>
|
|
|
|
|
{categoryNode(category, level)}
|
|
|
|
|
{category.expanded &&
|
|
|
|
|
category.children.map((c) => recursiveCategoryNode(c, level + 1))}
|
|
|
|
|
{category.expanded && category.feeds.map((f) => feedNode(f, level + 1))}
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!root) return <Loader />;
|
|
|
|
|
const feeds = flattenCategoryTree(root).flatMap((c) => c.feeds);
|
|
|
|
|
return (
|
|
|
|
|
<Stack>
|
|
|
|
|
<OnDesktop>
|
|
|
|
|
<TreeSearch feeds={feeds} />
|
|
|
|
|
</OnDesktop>
|
|
|
|
|
<Box className="cf-tree">
|
|
|
|
|
{allCategoryNode()}
|
|
|
|
|
{starredCategoryNode()}
|
|
|
|
|
{root.children.map((c) => recursiveCategoryNode(c))}
|
|
|
|
|
{root.feeds.map((f) => feedNode(f))}
|
|
|
|
|
{tags?.map((tag) => tagNode(tag))}
|
|
|
|
|
</Box>
|
|
|
|
|
</Stack>
|
|
|
|
|
);
|
2024-06-13 21:54:14 +02:00
|
|
|
}
|