counter on feed select
This commit is contained in:
@@ -6,7 +6,7 @@ import Modal from './modal/AddUrl.vue'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feedFilter, feedTitles, setFeedFilter, unreadCount, lastProgrammaticScroll } = useFeeds()
|
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feedFilter, feedTitles, feedUnreadCounts, setFeedFilter, unreadCount, lastProgrammaticScroll } = useFeeds()
|
||||||
|
|
||||||
const headerRef = ref(null)
|
const headerRef = ref(null)
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ function handleToggleLayout() {
|
|||||||
@change="setFeedFilter($event.target.value || null)"
|
@change="setFeedFilter($event.target.value || null)"
|
||||||
>
|
>
|
||||||
<option value="">All feeds</option>
|
<option value="">All feeds</option>
|
||||||
<option v-for="title in feedTitles" :key="title" :value="title">{{ title }}</option>
|
<option v-for="title in feedTitles" :key="title" :value="title">{{ title }} ({{ feedUnreadCounts[title] ?? 0 }})</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
class="app-nav__hamburger"
|
class="app-nav__hamburger"
|
||||||
|
|||||||
@@ -241,8 +241,9 @@ describe('AppNav', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const options = wrapper.find('.app-nav__filter').findAll('option').map(o => o.text())
|
const options = wrapper.find('.app-nav__filter').findAll('option').map(o => o.text())
|
||||||
// "All feeds" first, then distinct titles sorted alphabetically.
|
// "All feeds" first, then distinct titles sorted alphabetically, each with
|
||||||
expect(options).toEqual(['All feeds', 'Feed A', 'Feed B'])
|
// its unread count.
|
||||||
|
expect(options).toEqual(['All feeds', 'Feed A (2)', 'Feed B (1)'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not render the feed filter when there are no feeds', async () => {
|
it('does not render the feed filter when there are no feeds', async () => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class FakeIntersectionObserver {
|
|||||||
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
|
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
|
||||||
|
|
||||||
describe('useFeeds', () => {
|
describe('useFeeds', () => {
|
||||||
const { feeds, allItems, feedFilter, feedTitles, unreadCount, setFeedFilter, showMessage, message, showModal, fetchData, sync, getReadable, setInitialLoad, handleIntersection, markAllRead, setupIntersectionObserver } = useFeeds()
|
const { feeds, allItems, feedFilter, feedTitles, feedUnreadCounts, unreadCount, setFeedFilter, showMessage, message, showModal, fetchData, sync, getReadable, setInitialLoad, handleIntersection, markAllRead, setupIntersectionObserver } = useFeeds()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.setItem('user-token', 'test-token')
|
localStorage.setItem('user-token', 'test-token')
|
||||||
@@ -252,6 +252,18 @@ describe('useFeeds', () => {
|
|||||||
await setFeedFilter(null)
|
await setFeedFilter(null)
|
||||||
expect(unreadCount.value).toBe(3)
|
expect(unreadCount.value).toBe(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('exposes per-feed unread counts, excluding items marked read in place', async () => {
|
||||||
|
axios.get.mockResolvedValueOnce(twoFeedsResponse)
|
||||||
|
await fetchData()
|
||||||
|
|
||||||
|
expect(feedUnreadCounts.value).toEqual({ 'Feed A': 2, 'Feed B': 1 })
|
||||||
|
|
||||||
|
// Article view marks items read in place (feed.read = true) without
|
||||||
|
// removing them from the master — the counts must not include those.
|
||||||
|
allItems.value.find(i => i.id === 3).read = true
|
||||||
|
expect(feedUnreadCounts.value).toEqual({ 'Feed A': 1, 'Feed B': 1 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('strips leftover embedded-video placeholder headings', async () => {
|
it('strips leftover embedded-video placeholder headings', async () => {
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ const feedTitles = computed(() => {
|
|||||||
if (feedFilter.value) titles.add(feedFilter.value)
|
if (feedFilter.value) titles.add(feedFilter.value)
|
||||||
return [...titles].sort((a, b) => a.localeCompare(b))
|
return [...titles].sort((a, b) => a.localeCompare(b))
|
||||||
})
|
})
|
||||||
|
// Unread count per feed title, shown next to each option in the filter
|
||||||
|
// <select>. Derived from the master (allItems) so article view's
|
||||||
|
// read-but-still-shown items are excluded. A title kept in feedTitles with no
|
||||||
|
// unread items left has no key here — render it as 0.
|
||||||
|
const feedUnreadCounts = computed(() => {
|
||||||
|
const counts = {}
|
||||||
|
for (const item of allItems.value) {
|
||||||
|
if (!item.read) counts[item.feedTitle] = (counts[item.feedTitle] ?? 0) + 1
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
})
|
||||||
// The header badge count: unread items in the selected feed when a filter is
|
// The header badge count: unread items in the selected feed when a filter is
|
||||||
// active, otherwise the global unread total. Derived from the master (allItems)
|
// active, otherwise the global unread total. Derived from the master (allItems)
|
||||||
// rather than the displayed `feeds` so article view's read-but-still-shown
|
// rather than the displayed `feeds` so article view's read-but-still-shown
|
||||||
@@ -466,6 +477,7 @@ export function useFeeds() {
|
|||||||
allItems,
|
allItems,
|
||||||
feedFilter,
|
feedFilter,
|
||||||
feedTitles,
|
feedTitles,
|
||||||
|
feedUnreadCounts,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
setFeedFilter,
|
setFeedFilter,
|
||||||
showMessage,
|
showMessage,
|
||||||
|
|||||||
Reference in New Issue
Block a user