Merge pull request #1780 from Eshwar1212-maker/clean-red-dot

feat: red dot indicator for new unread articles
This commit is contained in:
Jérémie Panzer
2025-05-23 15:54:22 +02:00
committed by GitHub
7 changed files with 35 additions and 14 deletions

View File

@@ -3,6 +3,7 @@ import { client } from "app/client"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { type EntrySource, type EntrySourceType, entriesSlice, setMarkAllAsReadConfirmationDialogOpen, setSearch } from "app/entries/slice" import { type EntrySource, type EntrySourceType, entriesSlice, setMarkAllAsReadConfirmationDialogOpen, setSearch } from "app/entries/slice"
import type { RootState } from "app/store" import type { RootState } from "app/store"
import { setHasNewEntries } from "app/tree/slice"
import { reloadTree } from "app/tree/thunks" import { reloadTree } from "app/tree/thunks"
import type { Entry, MarkRequest, TagRequest } from "app/types" import type { Entry, MarkRequest, TagRequest } from "app/types"
import { reloadTags } from "app/user/thunks" import { reloadTags } from "app/user/thunks"
@@ -26,6 +27,9 @@ export const loadEntries = createAppAsyncThunk(
const state = thunkApi.getState() const state = thunkApi.getState()
const endpoint = getEndpoint(arg.source.type) const endpoint = getEndpoint(arg.source.type)
const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0)) const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0))
if (arg.source.type === "feed") {
thunkApi.dispatch(setHasNewEntries({ feedId: +arg.source.id, value: false }))
}
return result.data return result.data
} }
) )

View File

@@ -40,6 +40,15 @@ 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 => { extraReducers: builder => {
builder.addCase(reloadTree.fulfilled, (state, action) => { builder.addCase(reloadTree.fulfilled, (state, action) => {
@@ -65,4 +74,4 @@ export const treeSlice = createSlice({
}, },
}) })
export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount } = treeSlice.actions export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount, setHasNewEntries } = treeSlice.actions

View File

@@ -1,7 +1,7 @@
import { createAppAsyncThunk } from "app/async-thunk" import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client" import { client } from "app/client"
import { redirectToCategory, redirectToFeed } from "app/redirect/thunks" 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 type { CollapseRequest, Subscription } from "app/types"
import { flattenCategoryTree, visitCategoryTree } from "app/utils" import { flattenCategoryTree, visitCategoryTree } from "app/utils"
@@ -11,7 +11,6 @@ export const collapseTreeCategory = createAppAsyncThunk(
"tree/category/collapse", "tree/category/collapse",
async (req: CollapseRequest) => await client.category.collapse(req).then(r => r.data) async (req: CollapseRequest) => await client.category.collapse(req).then(r => r.data)
) )
export const selectNextUnreadTreeItem = createAppAsyncThunk( export const selectNextUnreadTreeItem = createAppAsyncThunk(
"tree/selectNextUnreadItem", "tree/selectNextUnreadItem",
( (
@@ -75,6 +74,7 @@ export const newFeedEntriesDiscovered = createAppAsyncThunk(
amount, amount,
}) })
) )
thunkApi.dispatch(setHasNewEntries({ feedId, value: true }))
} }
} }
) )

View File

@@ -30,13 +30,17 @@ export interface Subscription {
filter?: string filter?: string
} }
export interface TreeSubscription extends Subscription {
hasNewEntries?: boolean
}
export interface Category { export interface Category {
id: string id: string
parentId?: string parentId?: string
parentName?: string parentName?: string
name: string name: string
children: Category[] children: Category[]
feeds: Subscription[] feeds: TreeSubscription[]
expanded: boolean expanded: boolean
position: number position: number
} }

View File

@@ -11,7 +11,7 @@ import {
} 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, TreeSubscription } 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"
@@ -133,7 +133,7 @@ export function Tree() {
) )
} }
const feedNode = (feed: Subscription, level = 0) => { const feedNode = (feed: TreeSubscription, level = 0) => {
if (!isFeedDisplayed(feed)) return null if (!isFeedDisplayed(feed)) return null
return ( return (
@@ -148,6 +148,7 @@ export function Tree() {
hasError={feed.errorCount > errorThreshold} hasError={feed.errorCount > errorThreshold}
onClick={feedClicked} onClick={feedClicked}
key={feed.id} key={feed.id}
newMessages={feed.hasNewEntries}
/> />
) )
} }

View File

@@ -15,6 +15,7 @@ interface TreeNodeProps {
expanded?: boolean expanded?: boolean
level: number level: number
hasError: boolean hasError: boolean
newMessages?: boolean
onClick: (e: React.MouseEvent, id: string) => void onClick: (e: React.MouseEvent, id: string) => void
onIconClick?: (e: React.MouseEvent, id: string) => void onIconClick?: (e: React.MouseEvent, id: string) => void
} }
@@ -64,12 +65,12 @@ export function TreeNode(props: TreeNodeProps) {
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)}
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}
@@ -80,7 +81,7 @@ export function TreeNode(props: TreeNodeProps) {
<Box className={classes.nodeText}>{props.name}</Box> <Box className={classes.nodeText}>{props.name}</Box>
{!props.expanded && ( {!props.expanded && (
<Box className="cf-treenode-unread-count"> <Box className="cf-treenode-unread-count">
<UnreadCount unreadCount={props.unread} /> <UnreadCount unreadCount={props.unread} newMessages={props.id !== "all" ? props.newMessages : false} />
</Box> </Box>
)} )}
</Box> </Box>

View File

@@ -1,26 +1,28 @@
import { Badge, Tooltip } from "@mantine/core" import { Badge, Indicator, 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",
}, },
})) }))
export function UnreadCount(props: { unreadCount: number }) { export function UnreadCount(props: { unreadCount: number; newMessages: boolean | undefined }) {
const { classes } = useStyles() 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 ( return (
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}> <Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
<Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth> <Indicator disabled={!props.newMessages} size={4} offset={10} position="top-start" color="orange" withBorder={false} zIndex={5}>
{count} <Badge className={`${classes.badge} cf-badge`} variant="light" fullWidth>
</Badge> {count}
</Badge>
</Indicator>
</Tooltip> </Tooltip>
) )
} }