From 1bd504cbfbe784ddccdf42d26f4ef061dc20c9c6 Mon Sep 17 00:00:00 2001 From: Eshwar Tangirala Date: Sat, 10 May 2025 14:16:34 -0400 Subject: [PATCH 1/7] feat: red dot indicator for new unread articles --- .../src/components/sidebar/Tree.tsx | 401 ++++++++++-------- .../src/components/sidebar/TreeNode.tsx | 173 ++++---- .../src/components/sidebar/UnreadCount.tsx | 74 +++- 3 files changed, 366 insertions(+), 282 deletions(-) diff --git a/commafeed-client/src/components/sidebar/Tree.tsx b/commafeed-client/src/components/sidebar/Tree.tsx index ccfa8dca..2e775c42 100644 --- a/commafeed-client/src/components/sidebar/Tree.tsx +++ b/commafeed-client/src/components/sidebar/Tree.tsx @@ -1,194 +1,225 @@ -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 } 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" + 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 } 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"; -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 [hasNewMessages, setHasNewMessages] = useState(false); + 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 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 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() - - 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 = () => ( - 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 hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold)) - return ( - categoryIconClicked(e, category)} - key={category.id} - /> - ) - } - - const feedNode = (feed: Subscription, level = 0) => { - if (!isFeedDisplayed(feed)) return null - - return ( - errorThreshold} - onClick={feedClicked} - key={feed.id} - /> - ) - } - - 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) + const isCategoryDisplayed = (category: Category): boolean => { + const isCurrentCategory = + source.type === "category" && source.id === category.id; return ( - - - - - - {allCategoryNode()} - {starredCategoryNode()} - {root.children.map(c => recursiveCategoryNode(c))} - {root.feeds.map(f => feedNode(f))} - {tags?.map(tag => tagNode(tag))} - - - ) + 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 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(); + + 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)); + } + }; + + useEffect(() => { + const prevCount = JSON.parse(localStorage.getItem("FeedCount") || "0"); + const currentCount = categoryUnreadCount(root); + + if (currentCount > prevCount) { + setHasNewMessages(true); + } + localStorage.setItem("FeedCount", JSON.stringify(currentCount)); + }, [root?.feeds.length]); + + 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} + newMessages={hasNewMessages} + /> + ); + 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 hasError = + !category.expanded && + flattenCategoryTree(category).some((c) => + c.feeds.some((f) => f.errorCount > errorThreshold) + ); + return ( + categoryIconClicked(e, category)} + key={category.id} + /> + ); + }; + + const feedNode = (feed: Subscription, level = 0) => { + if (!isFeedDisplayed(feed)) return null; + + return ( + errorThreshold} + onClick={feedClicked} + key={feed.id} + /> + ); + }; + + 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))} + + + ); } diff --git a/commafeed-client/src/components/sidebar/TreeNode.tsx b/commafeed-client/src/components/sidebar/TreeNode.tsx index 26d55777..f52895c8 100644 --- a/commafeed-client/src/components/sidebar/TreeNode.tsx +++ b/commafeed-client/src/components/sidebar/TreeNode.tsx @@ -1,88 +1,107 @@ -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" +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"; interface TreeNodeProps { - id: string - type: EntrySourceType - name: React.ReactNode - icon: React.ReactNode - unread: number - selected: boolean - expanded?: boolean - level: number - hasError: 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)} + 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 63e39235..83e0f047 100644 --- a/commafeed-client/src/components/sidebar/UnreadCount.tsx +++ b/commafeed-client/src/components/sidebar/UnreadCount.tsx @@ -1,26 +1,60 @@ -import { Badge, Tooltip } from "@mantine/core" -import { Constants } from "app/constants" -import { tss } from "tss" +import { Badge, Box, Flex, Tooltip } from "@mantine/core"; +import { Constants } from "app/constants"; +import { tss } from "tss"; const useStyles = tss.create(() => ({ - badge: { - width: "3.2rem", - // for some reason, mantine Badge has "cursor: 'default'" - cursor: "pointer", - }, -})) + badge: { + width: "3.2rem", + cursor: "pointer", + display: "flex", + justifyContent: "flex-start", + alignItems: "center", + }, +})); -export function UnreadCount(props: { unreadCount: number }) { - const { classes } = useStyles() +export function UnreadCount(props: { + unreadCount: number; + newMessages?: boolean; +}) { + const { classes } = useStyles(); - if (props.unreadCount <= 0) return null + if (props.unreadCount <= 0) return null; - const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount - return ( - - - {count} - - - ) + const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount; + + return ( + + + + {/* Dot wrapper: always renders, but conditionally visible */} + + {props.newMessages && ( +
+ )} + + + {count} + + + + ); } From afc56c60537d3ab7dd5c0b848e0a47bcbb770eb8 Mon Sep 17 00:00:00 2001 From: Eshwar Tangirala Date: Sat, 10 May 2025 14:32:20 -0400 Subject: [PATCH 2/7] feat: red dot indicator for new unread articles --- commafeed-client/src/components/sidebar/TreeNode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commafeed-client/src/components/sidebar/TreeNode.tsx b/commafeed-client/src/components/sidebar/TreeNode.tsx index f52895c8..443daf77 100644 --- a/commafeed-client/src/components/sidebar/TreeNode.tsx +++ b/commafeed-client/src/components/sidebar/TreeNode.tsx @@ -98,7 +98,7 @@ export function TreeNode(props: TreeNodeProps) { )} From d6910aa1e82191f3d69203accdf1820d54b2f49d Mon Sep 17 00:00:00 2001 From: Eshwar Tangirala Date: Mon, 12 May 2025 16:30:06 -0400 Subject: [PATCH 3/7] Cleaned up UI for Indicator --- .../src/components/sidebar/UnreadCount.tsx | 74 ++++++------------- 1 file changed, 21 insertions(+), 53 deletions(-) diff --git a/commafeed-client/src/components/sidebar/UnreadCount.tsx b/commafeed-client/src/components/sidebar/UnreadCount.tsx index 83e0f047..bcc94c55 100644 --- a/commafeed-client/src/components/sidebar/UnreadCount.tsx +++ b/commafeed-client/src/components/sidebar/UnreadCount.tsx @@ -1,60 +1,28 @@ -import { Badge, Box, Flex, Tooltip } from "@mantine/core"; -import { Constants } from "app/constants"; -import { tss } from "tss"; +import { Badge, Indicator, Tooltip } from "@mantine/core" +import { Constants } from "app/constants" +import { tss } from "tss" const useStyles = tss.create(() => ({ - badge: { - width: "3.2rem", - cursor: "pointer", - display: "flex", - justifyContent: "flex-start", - alignItems: "center", - }, -})); + badge: { + width: "3.2rem", + cursor: "pointer", + }, +})) -export function UnreadCount(props: { - unreadCount: number; - newMessages?: boolean; -}) { - const { classes } = useStyles(); +export function UnreadCount(props: { unreadCount: number, newMessages: boolean | undefined }) { + const { classes } = useStyles() - if (props.unreadCount <= 0) return null; + if (props.unreadCount <= 0) return null - const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount; + const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount - return ( - - - - {/* Dot wrapper: always renders, but conditionally visible */} - - {props.newMessages && ( -
- )} - - - {count} - - - - ); + return ( + + + + {count} + + + + ) } From 3f2f6e83faa6fefa2d9ac60f8ea0b3b61dd85181 Mon Sep 17 00:00:00 2001 From: Eshwar Tangirala Date: Thu, 15 May 2025 20:19:05 -0400 Subject: [PATCH 4/7] Adding red dot indicator feature, got main components done --- commafeed-client/src/app/tree/slice.ts | 14 ++++++++++++++ commafeed-client/src/app/types.ts | 6 +++++- .../src/components/sidebar/Tree.tsx | 17 ++++------------- .../src/components/sidebar/UnreadCount.tsx | 3 +++ 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/commafeed-client/src/app/tree/slice.ts b/commafeed-client/src/app/tree/slice.ts index fd863350..323c0195 100644 --- a/commafeed-client/src/app/tree/slice.ts +++ b/commafeed-client/src/app/tree/slice.ts @@ -43,6 +43,20 @@ 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 + + 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/app/types.ts b/commafeed-client/src/app/types.ts index 68a2218d..db57202b 100644 --- a/commafeed-client/src/app/types.ts +++ b/commafeed-client/src/app/types.ts @@ -30,13 +30,17 @@ export interface Subscription { filter?: string } +export interface TreeSubscription extends Subscription { + hasNewEntries?: boolean +} + export interface Category { id: string parentId?: string parentName?: string name: string children: Category[] - feeds: Subscription[] + feeds: TreeSubscription[] expanded: boolean position: number } diff --git a/commafeed-client/src/components/sidebar/Tree.tsx b/commafeed-client/src/components/sidebar/Tree.tsx index 2e775c42..a965c2ed 100644 --- a/commafeed-client/src/components/sidebar/Tree.tsx +++ b/commafeed-client/src/components/sidebar/Tree.tsx @@ -11,7 +11,7 @@ import { } from "app/redirect/thunks"; import { useAppDispatch, useAppSelector } from "app/store"; import { collapseTreeCategory } from "app/tree/thunks"; -import type { Category, Subscription } from "app/types"; +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"; @@ -35,7 +35,6 @@ const collapsedIcon = ; const errorThreshold = 9; export function Tree() { - const [hasNewMessages, setHasNewMessages] = useState(false); const root = useAppSelector((state) => state.tree.rootCategory); const source = useAppSelector((state) => state.entries.source); const tags = useAppSelector((state) => state.user.tags); @@ -91,15 +90,7 @@ export function Tree() { } }; - useEffect(() => { - const prevCount = JSON.parse(localStorage.getItem("FeedCount") || "0"); - const currentCount = categoryUnreadCount(root); - - if (currentCount > prevCount) { - setHasNewMessages(true); - } - localStorage.setItem("FeedCount", JSON.stringify(currentCount)); - }, [root?.feeds.length]); + console.log(root?.feeds.map(f => f.hasNewEntries)); const allCategoryNode = () => ( @@ -116,7 +107,6 @@ export function Tree() { level={0} hasError={false} onClick={categoryClicked} - newMessages={hasNewMessages} /> ); const starredCategoryNode = () => ( @@ -163,7 +153,7 @@ export function Tree() { ); }; - const feedNode = (feed: Subscription, level = 0) => { + const feedNode = (feed: TreeSubscription, level = 0) => { if (!isFeedDisplayed(feed)) return null; return ( @@ -178,6 +168,7 @@ export function Tree() { hasError={feed.errorCount > errorThreshold} onClick={feedClicked} key={feed.id} + newMessages={feed.hasNewEntries} /> ); }; diff --git a/commafeed-client/src/components/sidebar/UnreadCount.tsx b/commafeed-client/src/components/sidebar/UnreadCount.tsx index bcc94c55..6607b5fd 100644 --- a/commafeed-client/src/components/sidebar/UnreadCount.tsx +++ b/commafeed-client/src/components/sidebar/UnreadCount.tsx @@ -16,6 +16,9 @@ export function UnreadCount(props: { unreadCount: number, newMessages: boolean | const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount + console.log(props.newMessages); + + return ( From 0199a362389b994a254374cb710d516200c4e1eb Mon Sep 17 00:00:00 2001 From: Eshwar Tangirala Date: Sun, 18 May 2025 00:03:28 -0400 Subject: [PATCH 5/7] 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 ( From 7b33717333c5db292ff60a4f11c55b9c2edf6603 Mon Sep 17 00:00:00 2001 From: Eshwar Tangirala Date: Thu, 22 May 2025 20:10:52 -0400 Subject: [PATCH 6/7] Readjusted code to not use localstorage, and just used redux for indicator --- commafeed-client/src/app/entries/thunks.ts | 6 ++++ commafeed-client/src/app/tree/slice.ts | 31 ++++++------------- commafeed-client/src/app/tree/thunks.ts | 4 +-- .../src/components/sidebar/TreeNode.tsx | 4 --- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/commafeed-client/src/app/entries/thunks.ts b/commafeed-client/src/app/entries/thunks.ts index 88c5abd1..e7849f93 100644 --- a/commafeed-client/src/app/entries/thunks.ts +++ b/commafeed-client/src/app/entries/thunks.ts @@ -3,6 +3,7 @@ import { client } from "app/client" import { Constants } from "app/constants" import { type EntrySource, type EntrySourceType, entriesSlice, setMarkAllAsReadConfirmationDialogOpen, setSearch } from "app/entries/slice" import type { RootState } from "app/store" +import { setHasNewEntries } from "app/tree/slice" import { reloadTree } from "app/tree/thunks" import type { Entry, MarkRequest, TagRequest } from "app/types" import { reloadTags } from "app/user/thunks" @@ -26,6 +27,11 @@ export const loadEntries = createAppAsyncThunk( const state = thunkApi.getState() const endpoint = getEndpoint(arg.source.type) const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0)) + console.log(arg.source.id) + + if (arg.source.type === "feed") { + thunkApi.dispatch(setHasNewEntries({ feedId: +arg.source.id, value: false })) + } return result.data } ) diff --git a/commafeed-client/src/app/tree/slice.ts b/commafeed-client/src/app/tree/slice.ts index c9a0d5e8..4cf942ef 100644 --- a/commafeed-client/src/app/tree/slice.ts +++ b/commafeed-client/src/app/tree/slice.ts @@ -40,29 +40,18 @@ export const treeSlice = createSlice({ } }) }, + setHasNewEntries: (state, action: PayloadAction<{ feedId: number; value: boolean }>) => { + if (!state.rootCategory) return + + visitCategoryTree(state.rootCategory, category => { + category.feeds = category.feeds.map(feed => + feed.id === action.payload.feedId ? { ...feed, hasNewEntries: action.payload.value } : feed + ) + }) + }, }, 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 existing = localStorage.getItem(storageKey) - const prevUnread = Number.parseInt(existing || "0", 10) - const isNewFeed = existing === null - - const hasNewEntries = isNewFeed ? true : feed.unread > prevUnread - - if (!isNewFeed) { - localStorage.setItem(storageKey, feed.unread.toString()) - } - - return { - ...feed, - hasNewEntries, - } - }) - }) - state.rootCategory = action.payload }) builder.addCase(collapseTreeCategory.pending, (state, action) => { @@ -85,4 +74,4 @@ export const treeSlice = createSlice({ }, }) -export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount } = treeSlice.actions +export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount, setHasNewEntries } = treeSlice.actions diff --git a/commafeed-client/src/app/tree/thunks.ts b/commafeed-client/src/app/tree/thunks.ts index ac0d7a33..22ef4b8d 100644 --- a/commafeed-client/src/app/tree/thunks.ts +++ b/commafeed-client/src/app/tree/thunks.ts @@ -1,7 +1,7 @@ import { createAppAsyncThunk } from "app/async-thunk" import { client } from "app/client" import { redirectToCategory, redirectToFeed } from "app/redirect/thunks" -import { incrementUnreadCount } from "app/tree/slice" +import { incrementUnreadCount, setHasNewEntries } from "app/tree/slice" import type { CollapseRequest, Subscription } from "app/types" import { flattenCategoryTree, visitCategoryTree } from "app/utils" @@ -11,7 +11,6 @@ export const collapseTreeCategory = createAppAsyncThunk( "tree/category/collapse", async (req: CollapseRequest) => await client.category.collapse(req).then(r => r.data) ) - export const selectNextUnreadTreeItem = createAppAsyncThunk( "tree/selectNextUnreadItem", ( @@ -75,6 +74,7 @@ export const newFeedEntriesDiscovered = createAppAsyncThunk( amount, }) ) + thunkApi.dispatch(setHasNewEntries({ feedId, value: true })) } } ) diff --git a/commafeed-client/src/components/sidebar/TreeNode.tsx b/commafeed-client/src/components/sidebar/TreeNode.tsx index 56baa1c1..f61b6eb1 100644 --- a/commafeed-client/src/components/sidebar/TreeNode.tsx +++ b/commafeed-client/src/components/sidebar/TreeNode.tsx @@ -71,10 +71,6 @@ export function TreeNode(props: TreeNodeProps) { py={1} pl={props.level * 20} className={`${classes.node} cf-treenode cf-treenode-${props.type}`} - onClick={e => { - 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} From 0546f25d554574cb5088cb4f5fd01c63d4306546 Mon Sep 17 00:00:00 2001 From: Eshwar Tangirala Date: Thu, 22 May 2025 20:13:16 -0400 Subject: [PATCH 7/7] Removed console.log --- commafeed-client/src/app/entries/thunks.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/commafeed-client/src/app/entries/thunks.ts b/commafeed-client/src/app/entries/thunks.ts index e7849f93..3a85f601 100644 --- a/commafeed-client/src/app/entries/thunks.ts +++ b/commafeed-client/src/app/entries/thunks.ts @@ -27,8 +27,6 @@ export const loadEntries = createAppAsyncThunk( const state = thunkApi.getState() const endpoint = getEndpoint(arg.source.type) const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0)) - console.log(arg.source.id) - if (arg.source.type === "feed") { thunkApi.dispatch(setHasNewEntries({ feedId: +arg.source.id, value: false })) }