mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Merge pull request #1780 from Eshwar1212-maker/clean-red-dot
feat: red dot indicator for new unread articles
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user