forked from Archives/Athou_commafeed
feat: red dot indicator for new unread articles
This commit is contained in:
@@ -1,194 +1,225 @@
|
|||||||
import { Trans } from "@lingui/react/macro"
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { Box, Stack } from "@mantine/core"
|
import { Box, Stack } from "@mantine/core";
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants";
|
||||||
import {
|
import {
|
||||||
redirectToCategory,
|
redirectToCategory,
|
||||||
redirectToCategoryDetails,
|
redirectToCategoryDetails,
|
||||||
redirectToFeed,
|
redirectToFeed,
|
||||||
redirectToFeedDetails,
|
redirectToFeedDetails,
|
||||||
redirectToTag,
|
redirectToTag,
|
||||||
redirectToTagDetails,
|
redirectToTagDetails,
|
||||||
} from "app/redirect/thunks"
|
} from "app/redirect/thunks";
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store";
|
||||||
import { collapseTreeCategory } from "app/tree/thunks"
|
import { collapseTreeCategory } from "app/tree/thunks";
|
||||||
import type { Category, Subscription } from "app/types"
|
import type { Category, Subscription } from "app/types";
|
||||||
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
import { categoryUnreadCount, flattenCategoryTree } from "app/utils";
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader";
|
||||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
import { OnDesktop } from "components/responsive/OnDesktop";
|
||||||
import React from "react"
|
import React, { useEffect, useState } from "react";
|
||||||
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
import {
|
||||||
import { TreeNode } from "./TreeNode"
|
TbChevronDown,
|
||||||
import { TreeSearch } from "./TreeSearch"
|
TbChevronRight,
|
||||||
|
TbInbox,
|
||||||
|
TbStar,
|
||||||
|
TbTag,
|
||||||
|
} from "react-icons/tb";
|
||||||
|
import { TreeNode } from "./TreeNode";
|
||||||
|
import { TreeSearch } from "./TreeSearch";
|
||||||
|
|
||||||
const allIcon = <TbInbox size={16} />
|
const allIcon = <TbInbox size={16} />;
|
||||||
const starredIcon = <TbStar size={16} />
|
const starredIcon = <TbStar size={16} />;
|
||||||
const tagIcon = <TbTag size={16} />
|
const tagIcon = <TbTag size={16} />;
|
||||||
const expandedIcon = <TbChevronDown size={16} />
|
const expandedIcon = <TbChevronDown size={16} />;
|
||||||
const collapsedIcon = <TbChevronRight size={16} />
|
const collapsedIcon = <TbChevronRight size={16} />;
|
||||||
|
|
||||||
const errorThreshold = 9
|
const errorThreshold = 9;
|
||||||
|
|
||||||
export function Tree() {
|
export function Tree() {
|
||||||
const root = useAppSelector(state => state.tree.rootCategory)
|
const [hasNewMessages, setHasNewMessages] = useState(false);
|
||||||
const source = useAppSelector(state => state.entries.source)
|
const root = useAppSelector((state) => state.tree.rootCategory);
|
||||||
const tags = useAppSelector(state => state.user.tags)
|
const source = useAppSelector((state) => state.entries.source);
|
||||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
const tags = useAppSelector((state) => state.user.tags);
|
||||||
const dispatch = useAppDispatch()
|
const showRead = useAppSelector((state) => state.user.settings?.showRead);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const isFeedDisplayed = (feed: Subscription) => {
|
const isFeedDisplayed = (feed: Subscription) => {
|
||||||
const isCurrentFeed = source.type === "feed" && source.id === String(feed.id)
|
const isCurrentFeed =
|
||||||
return isCurrentFeed || feed.unread > 0 || showRead
|
source.type === "feed" && source.id === String(feed.id);
|
||||||
}
|
return isCurrentFeed || feed.unread > 0 || showRead;
|
||||||
|
};
|
||||||
|
|
||||||
const isCategoryDisplayed = (category: Category): boolean => {
|
const isCategoryDisplayed = (category: Category): boolean => {
|
||||||
const isCurrentCategory = source.type === "category" && source.id === category.id
|
const isCurrentCategory =
|
||||||
return (
|
source.type === "category" && source.id === category.id;
|
||||||
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 = () => (
|
|
||||||
<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))
|
|
||||||
return (
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const feedNode = (feed: Subscription, level = 0) => {
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Stack>
|
isCurrentCategory ||
|
||||||
<OnDesktop>
|
showRead ||
|
||||||
<TreeSearch feeds={feeds} />
|
category.children.some((c) => isCategoryDisplayed(c)) ||
|
||||||
</OnDesktop>
|
category.feeds.some((f) => isFeedDisplayed(f))
|
||||||
<Box className="cf-tree">
|
);
|
||||||
{allCategoryNode()}
|
};
|
||||||
{starredCategoryNode()}
|
|
||||||
{root.children.map(c => recursiveCategoryNode(c))}
|
const feedClicked = (e: React.MouseEvent, id: string) => {
|
||||||
{root.feeds.map(f => feedNode(f))}
|
if (e.detail === 2) {
|
||||||
{tags?.map(tag => tagNode(tag))}
|
dispatch(redirectToFeedDetails(id));
|
||||||
</Box>
|
} else {
|
||||||
</Stack>
|
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 = () => (
|
||||||
|
|
||||||
|
<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}
|
||||||
|
newMessages={hasNewMessages}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const feedNode = (feed: Subscription, level = 0) => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +1,107 @@
|
|||||||
import { Box, Center } from "@mantine/core"
|
import { Box, Center, Indicator } from "@mantine/core";
|
||||||
import type { EntrySourceType } from "app/entries/slice"
|
import type { EntrySourceType } from "app/entries/slice";
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon";
|
||||||
import type React from "react"
|
import type React from "react";
|
||||||
import { tss } from "tss"
|
import { tss } from "tss";
|
||||||
import { UnreadCount } from "./UnreadCount"
|
import { UnreadCount } from "./UnreadCount";
|
||||||
|
|
||||||
interface TreeNodeProps {
|
interface TreeNodeProps {
|
||||||
id: string
|
id: string;
|
||||||
type: EntrySourceType
|
type: EntrySourceType;
|
||||||
name: React.ReactNode
|
name: React.ReactNode;
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode;
|
||||||
unread: number
|
unread: number;
|
||||||
selected: boolean
|
selected: boolean;
|
||||||
expanded?: boolean
|
expanded?: boolean;
|
||||||
level: number
|
level: number;
|
||||||
hasError: boolean
|
hasError: boolean;
|
||||||
onClick: (e: React.MouseEvent, id: string) => void
|
newMessages?: boolean
|
||||||
onIconClick?: (e: React.MouseEvent, id: string) => void
|
onClick: (e: React.MouseEvent, id: string) => void;
|
||||||
|
onIconClick?: (e: React.MouseEvent, id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = tss
|
const useStyles = tss
|
||||||
.withParams<{
|
.withParams<{
|
||||||
selected: boolean
|
selected: boolean;
|
||||||
hasError: boolean
|
hasError: boolean;
|
||||||
hasUnread: boolean
|
hasUnread: boolean;
|
||||||
}>()
|
}>()
|
||||||
.create(({ theme, colorScheme, selected, hasError, hasUnread }) => {
|
.create(({ theme, colorScheme, selected, hasError, hasUnread }) => {
|
||||||
let backgroundColor = "inherit"
|
let backgroundColor = "inherit";
|
||||||
if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]
|
if (selected)
|
||||||
|
backgroundColor =
|
||||||
|
colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1];
|
||||||
|
|
||||||
let color: string
|
let color: string;
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
color = theme.colors.red[6]
|
color = theme.colors.red[6];
|
||||||
} else if (colorScheme === "dark") {
|
} else if (colorScheme === "dark") {
|
||||||
color = hasUnread ? theme.colors.dark[0] : theme.colors.dark[3]
|
color = hasUnread ? theme.colors.dark[0] : theme.colors.dark[3];
|
||||||
} else {
|
} else {
|
||||||
color = hasUnread ? theme.black : theme.colors.gray[6]
|
color = hasUnread ? theme.black : theme.colors.gray[6];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: {
|
node: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color,
|
color,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0],
|
backgroundColor:
|
||||||
},
|
colorScheme === "dark"
|
||||||
},
|
? theme.colors.dark[6]
|
||||||
nodeText: {
|
: theme.colors.gray[0],
|
||||||
flexGrow: 1,
|
},
|
||||||
whiteSpace: "nowrap",
|
},
|
||||||
overflow: "hidden",
|
nodeText: {
|
||||||
textOverflow: "ellipsis",
|
flexGrow: 1,
|
||||||
},
|
whiteSpace: "nowrap",
|
||||||
}
|
overflow: "hidden",
|
||||||
})
|
textOverflow: "ellipsis",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export function TreeNode(props: TreeNodeProps) {
|
export function TreeNode(props: TreeNodeProps) {
|
||||||
const { classes } = useStyles({
|
const { classes } = useStyles({
|
||||||
selected: props.selected,
|
selected: props.selected,
|
||||||
hasError: props.hasError,
|
hasError: props.hasError,
|
||||||
hasUnread: props.unread > 0,
|
hasUnread: props.unread > 0,
|
||||||
})
|
});
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
py={1}
|
py={1}
|
||||||
pl={props.level * 20}
|
pl={props.level * 20}
|
||||||
className={`${classes.node} cf-treenode cf-treenode-${props.type}`}
|
className={`${classes.node} cf-treenode cf-treenode-${props.type}`}
|
||||||
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
|
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
|
||||||
data-id={props.id}
|
data-id={props.id}
|
||||||
data-type={props.type}
|
data-type={props.type}
|
||||||
data-unread-count={props.unread}
|
data-unread-count={props.unread}
|
||||||
>
|
>
|
||||||
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)} className="cf-treenode-icon">
|
<Box
|
||||||
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
|
mr={6}
|
||||||
</Box>
|
onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}
|
||||||
<Box className={classes.nodeText}>{props.name}</Box>
|
className="cf-treenode-icon"
|
||||||
{!props.expanded && (
|
>
|
||||||
<Box className="cf-treenode-unread-count">
|
<Center>
|
||||||
<UnreadCount unreadCount={props.unread} />
|
{typeof props.icon === "string" ? (
|
||||||
</Box>
|
<FeedFavicon url={props.icon} />
|
||||||
)}
|
) : (
|
||||||
|
props.icon
|
||||||
|
)}
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
<Box className={classes.nodeText}>{props.name}</Box>
|
||||||
|
{!props.expanded && (
|
||||||
|
<Box className="cf-treenode-unread-count">
|
||||||
|
<UnreadCount
|
||||||
|
unreadCount={props.unread}
|
||||||
|
newMessages={props.id === "all" ? true: false}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,60 @@
|
|||||||
import { Badge, Tooltip } from "@mantine/core"
|
import { Badge, Box, Flex, Tooltip } from "@mantine/core";
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants";
|
||||||
import { tss } from "tss"
|
import { tss } from "tss";
|
||||||
|
|
||||||
const useStyles = tss.create(() => ({
|
const useStyles = tss.create(() => ({
|
||||||
badge: {
|
badge: {
|
||||||
width: "3.2rem",
|
width: "3.2rem",
|
||||||
// for some reason, mantine Badge has "cursor: 'default'"
|
cursor: "pointer",
|
||||||
cursor: "pointer",
|
display: "flex",
|
||||||
},
|
justifyContent: "flex-start",
|
||||||
}))
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export function UnreadCount(props: { unreadCount: number }) {
|
export function UnreadCount(props: {
|
||||||
const { classes } = useStyles()
|
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
|
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount;
|
||||||
return (
|
|
||||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
return (
|
||||||
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
|
<Tooltip
|
||||||
{count}
|
label={props.unreadCount}
|
||||||
</Badge>
|
disabled={props.unreadCount === count}
|
||||||
</Tooltip>
|
openDelay={Constants.tooltip.delay}
|
||||||
)
|
>
|
||||||
|
<Badge className={`${classes.badge} cf-badge`} variant="light">
|
||||||
|
<Flex align="center" justify="center" style={{ width: "100%" }}>
|
||||||
|
{/* Dot wrapper: always renders, but conditionally visible */}
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginRight: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.newMessages && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 5,
|
||||||
|
height: 5,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "orange",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box style={{ minWidth: "1.3rem", textAlign: "center" }}>{count}</Box>
|
||||||
|
</Flex>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user