From 0199a362389b994a254374cb710d516200c4e1eb Mon Sep 17 00:00:00 2001 From: Eshwar Tangirala Date: Sun, 18 May 2025 00:03:28 -0400 Subject: [PATCH] Working on indicator feature for new unread feeds --- commafeed-client/src/app/tree/slice.ts | 26 +- .../src/components/sidebar/Tree.tsx | 377 +++++++++--------- .../src/components/sidebar/TreeNode.tsx | 178 ++++----- .../src/components/sidebar/UnreadCount.tsx | 5 +- 4 files changed, 277 insertions(+), 309 deletions(-) diff --git a/commafeed-client/src/app/tree/slice.ts b/commafeed-client/src/app/tree/slice.ts index 323c0195..c9a0d5e8 100644 --- a/commafeed-client/src/app/tree/slice.ts +++ b/commafeed-client/src/app/tree/slice.ts @@ -44,19 +44,25 @@ export const treeSlice = createSlice({ extraReducers: builder => { builder.addCase(reloadTree.fulfilled, (state, action) => { visitCategoryTree(action.payload, category => { - category.feeds = category.feeds.map(feed => { - const storageKey = `feed-${feed.id}-unread` - const prevUnread = parseInt(localStorage.getItem(storageKey) || "0", 10) - const hasNewEntries = feed.unread > prevUnread + category.feeds = category.feeds.map(feed => { + const storageKey = `feed-${feed.id}-unread` + const existing = localStorage.getItem(storageKey) + const prevUnread = Number.parseInt(existing || "0", 10) + const isNewFeed = existing === null - localStorage.setItem(storageKey, feed.unread.toString()) + const hasNewEntries = isNewFeed ? true : feed.unread > prevUnread - return { - ...feed, - hasNewEntries - } + if (!isNewFeed) { + localStorage.setItem(storageKey, feed.unread.toString()) + } + + return { + ...feed, + hasNewEntries, + } + }) }) - }) + state.rootCategory = action.payload }) builder.addCase(collapseTreeCategory.pending, (state, action) => { diff --git a/commafeed-client/src/components/sidebar/Tree.tsx b/commafeed-client/src/components/sidebar/Tree.tsx index a965c2ed..0d805737 100644 --- a/commafeed-client/src/components/sidebar/Tree.tsx +++ b/commafeed-client/src/components/sidebar/Tree.tsx @@ -1,216 +1,195 @@ -import { Trans } from "@lingui/react/macro"; -import { Box, Stack } from "@mantine/core"; -import { Constants } from "app/constants"; +import { Trans } from "@lingui/react/macro" +import { Box, Stack } from "@mantine/core" +import { Constants } from "app/constants" import { - redirectToCategory, - redirectToCategoryDetails, - redirectToFeed, - redirectToFeedDetails, - redirectToTag, - redirectToTagDetails, -} from "app/redirect/thunks"; -import { useAppDispatch, useAppSelector } from "app/store"; -import { collapseTreeCategory } from "app/tree/thunks"; -import type { Category, Subscription, TreeSubscription } from "app/types"; -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"; + redirectToCategory, + redirectToCategoryDetails, + redirectToFeed, + redirectToFeedDetails, + redirectToTag, + redirectToTagDetails, +} from "app/redirect/thunks" +import { useAppDispatch, useAppSelector } from "app/store" +import { collapseTreeCategory } from "app/tree/thunks" +import type { Category, Subscription, TreeSubscription } from "app/types" +import { categoryUnreadCount, flattenCategoryTree } from "app/utils" +import { Loader } from "components/Loader" +import { OnDesktop } from "components/responsive/OnDesktop" +import React from "react" +import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb" +import { TreeNode } from "./TreeNode" +import { TreeSearch } from "./TreeSearch" -const allIcon = ; -const starredIcon = ; -const tagIcon = ; -const expandedIcon = ; -const collapsedIcon = ; +const allIcon = +const starredIcon = +const tagIcon = +const expandedIcon = +const collapsedIcon = -const errorThreshold = 9; +const errorThreshold = 9 export function Tree() { - 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(); + 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() - const isFeedDisplayed = (feed: Subscription) => { - const isCurrentFeed = - source.type === "feed" && source.id === String(feed.id); - return isCurrentFeed || feed.unread > 0 || showRead; - }; - - 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)); + const isFeedDisplayed = (feed: Subscription) => { + const isCurrentFeed = source.type === "feed" && source.id === String(feed.id) + return isCurrentFeed || feed.unread > 0 || showRead } - }; - const categoryClicked = (e: React.MouseEvent, id: string) => { - if (e.detail === 2) { - dispatch(redirectToCategoryDetails(id)); - } else { - dispatch(redirectToCategory(id)); + + 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 categoryIconClicked = (e: React.MouseEvent, category: Category) => { - e.stopPropagation(); - 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)); + const feedClicked = (e: React.MouseEvent, id: string) => { + if (e.detail === 2) { + dispatch(redirectToFeedDetails(id)) + } else { + dispatch(redirectToFeed(id)) + } } - }; + const categoryClicked = (e: React.MouseEvent, id: string) => { + if (e.detail === 2) { + dispatch(redirectToCategoryDetails(id)) + } else { + dispatch(redirectToCategory(id)) + } + } + const categoryIconClicked = (e: React.MouseEvent, category: Category) => { + e.stopPropagation() - console.log(root?.feeds.map(f => f.hasNewEntries)); + 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)) + } + } - const allCategoryNode = () => ( + const allCategoryNode = () => ( + All} + 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 = () => ( + Starred} + icon={starredIcon} + unread={0} + selected={source.type === "category" && source.id === Constants.categories.starred.id} + expanded={false} + level={0} + hasError={false} + onClick={categoryClicked} + /> + ) - All} - 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 = () => ( - Starred} - 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 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)) + return ( + categoryIconClicked(e, category)} + key={category.id} + /> + ) + } - const hasError = - !category.expanded && - flattenCategoryTree(category).some((c) => - c.feeds.some((f) => f.errorCount > errorThreshold) - ); + const feedNode = (feed: TreeSubscription, level = 0) => { + if (!isFeedDisplayed(feed)) return null + + return ( + errorThreshold} + onClick={feedClicked} + key={feed.id} + newMessages={feed.hasNewEntries} + /> + ) + } + + const tagNode = (tag: string) => ( + + ) + + const recursiveCategoryNode = (category: Category, level = 0) => ( + + {categoryNode(category, level)} + {category.expanded && category.children.map(c => recursiveCategoryNode(c, level + 1))} + {category.expanded && category.feeds.map(f => feedNode(f, level + 1))} + + ) + + if (!root) return + const feeds = flattenCategoryTree(root).flatMap(c => c.feeds) return ( - categoryIconClicked(e, category)} - key={category.id} - /> - ); - }; - - const feedNode = (feed: TreeSubscription, level = 0) => { - if (!isFeedDisplayed(feed)) return null; - - return ( - errorThreshold} - onClick={feedClicked} - key={feed.id} - newMessages={feed.hasNewEntries} - /> - ); - }; - - const tagNode = (tag: string) => ( - - ); - - const recursiveCategoryNode = (category: Category, level = 0) => ( - - {categoryNode(category, level)} - {category.expanded && - category.children.map((c) => recursiveCategoryNode(c, level + 1))} - {category.expanded && category.feeds.map((f) => feedNode(f, level + 1))} - - ); - - if (!root) return ; - const feeds = flattenCategoryTree(root).flatMap((c) => c.feeds); - return ( - - - - - - {allCategoryNode()} - {starredCategoryNode()} - {root.children.map((c) => recursiveCategoryNode(c))} - {root.feeds.map((f) => feedNode(f))} - {tags?.map((tag) => tagNode(tag))} - - - ); + + + + + + {allCategoryNode()} + {starredCategoryNode()} + {root.children.map(c => recursiveCategoryNode(c))} + {root.feeds.map(f => feedNode(f))} + {tags?.map(tag => tagNode(tag))} + + + ) } diff --git a/commafeed-client/src/components/sidebar/TreeNode.tsx b/commafeed-client/src/components/sidebar/TreeNode.tsx index 443daf77..56baa1c1 100644 --- a/commafeed-client/src/components/sidebar/TreeNode.tsx +++ b/commafeed-client/src/components/sidebar/TreeNode.tsx @@ -1,107 +1,93 @@ -import { Box, Center, Indicator } from "@mantine/core"; -import type { EntrySourceType } from "app/entries/slice"; -import { FeedFavicon } from "components/content/FeedFavicon"; -import type React from "react"; -import { tss } from "tss"; -import { UnreadCount } from "./UnreadCount"; +import { Box, Center } from "@mantine/core" +import type { EntrySourceType } from "app/entries/slice" +import { FeedFavicon } from "components/content/FeedFavicon" +import type React from "react" +import { tss } from "tss" +import { UnreadCount } from "./UnreadCount" interface TreeNodeProps { - id: string; - type: EntrySourceType; - name: React.ReactNode; - icon: React.ReactNode; - unread: number; - selected: boolean; - expanded?: boolean; - level: number; - hasError: boolean; - newMessages?: boolean - onClick: (e: React.MouseEvent, id: string) => void; - onIconClick?: (e: React.MouseEvent, id: string) => void; + id: string + type: EntrySourceType + name: React.ReactNode + icon: React.ReactNode + unread: number + selected: boolean + expanded?: boolean + level: number + hasError: boolean + newMessages?: boolean + onClick: (e: React.MouseEvent, id: string) => void + onIconClick?: (e: React.MouseEvent, id: string) => void } const useStyles = tss - .withParams<{ - selected: boolean; - hasError: boolean; - hasUnread: boolean; - }>() - .create(({ theme, colorScheme, selected, hasError, hasUnread }) => { - let backgroundColor = "inherit"; - if (selected) - backgroundColor = - colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]; + .withParams<{ + selected: boolean + hasError: boolean + hasUnread: boolean + }>() + .create(({ theme, colorScheme, selected, hasError, hasUnread }) => { + let backgroundColor = "inherit" + if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1] - let color: string; - if (hasError) { - color = theme.colors.red[6]; - } else if (colorScheme === "dark") { - color = hasUnread ? theme.colors.dark[0] : theme.colors.dark[3]; - } else { - color = hasUnread ? theme.black : theme.colors.gray[6]; - } + let color: string + if (hasError) { + color = theme.colors.red[6] + } else if (colorScheme === "dark") { + color = hasUnread ? theme.colors.dark[0] : theme.colors.dark[3] + } else { + color = hasUnread ? theme.black : theme.colors.gray[6] + } - return { - node: { - display: "flex", - alignItems: "center", - cursor: "pointer", - color, - backgroundColor, - "&:hover": { - backgroundColor: - colorScheme === "dark" - ? theme.colors.dark[6] - : theme.colors.gray[0], - }, - }, - nodeText: { - flexGrow: 1, - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }, - }; - }); + return { + node: { + display: "flex", + alignItems: "center", + cursor: "pointer", + color, + backgroundColor, + "&:hover": { + backgroundColor: colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0], + }, + }, + nodeText: { + flexGrow: 1, + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }, + } + }) export function TreeNode(props: TreeNodeProps) { - const { classes } = useStyles({ - selected: props.selected, - hasError: props.hasError, - hasUnread: props.unread > 0, - }); - return ( - props.onClick(e, props.id)} - data-id={props.id} - data-type={props.type} - data-unread-count={props.unread} - > - props.onIconClick?.(e, props.id)} - className="cf-treenode-icon" - > -
- {typeof props.icon === "string" ? ( - - ) : ( - props.icon - )} -
-
- {props.name} - {!props.expanded && ( - - + const { classes } = useStyles({ + selected: props.selected, + hasError: props.hasError, + hasUnread: props.unread > 0, + }) + + return ( + { + props.onClick(e, props.id) + props.type === "feed" && localStorage.setItem(`feed-${props.id}-unread`, props.unread.toString()) + }} + data-id={props.id} + data-type={props.type} + data-unread-count={props.unread} + > + props.onIconClick?.(e, props.id)} className="cf-treenode-icon"> +
{typeof props.icon === "string" ? : props.icon}
+
+ {props.name} + {!props.expanded && ( + + + + )}
- )} -
- ); + ) } diff --git a/commafeed-client/src/components/sidebar/UnreadCount.tsx b/commafeed-client/src/components/sidebar/UnreadCount.tsx index 6607b5fd..a1e6e154 100644 --- a/commafeed-client/src/components/sidebar/UnreadCount.tsx +++ b/commafeed-client/src/components/sidebar/UnreadCount.tsx @@ -9,16 +9,13 @@ const useStyles = tss.create(() => ({ }, })) -export function UnreadCount(props: { unreadCount: number, newMessages: boolean | undefined }) { +export function UnreadCount(props: { unreadCount: number; newMessages: boolean | undefined }) { const { classes } = useStyles() if (props.unreadCount <= 0) return null const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount - console.log(props.newMessages); - - return (