From 0399375288027c7431b88bf284a0f798f6a95507 Mon Sep 17 00:00:00 2001 From: Athou Date: Thu, 22 Oct 2026 09:04:57 +0200 Subject: [PATCH] don't mark as read entries that the user explicitly marked as unread (#2337) --- .../src/app/entries/entries.test.ts | 24 +++++++++++++++++++ commafeed-client/src/app/entries/slice.ts | 8 ++++++- .../src/components/content/FeedEntries.tsx | 3 ++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/commafeed-client/src/app/entries/entries.test.ts b/commafeed-client/src/app/entries/entries.test.ts index 71c3590e..0b3fd987 100644 --- a/commafeed-client/src/app/entries/entries.test.ts +++ b/commafeed-client/src/app/entries/entries.test.ts @@ -116,6 +116,30 @@ describe("entries", () => { expect(client.entry.mark).toHaveBeenCalledWith({ id: "3", read: true }) }) + it("flags an entry marked as unread by the user until entries are reloaded", async () => { + vi.mocked(client.feed.getEntries).mockResolvedValue({ + data: { + entries: [{ id: "3", read: true, markable: true } as Entry], + hasMore: false, + name: "my-feed", + errorCount: 0, + feedLink: "https://mysite.com/feed", + timestamp: 123, + ignoredReadStatus: false, + }, + } as AxiosResponse) + + const store = configureStore({ reducer: reducers }) + await store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true })) + + store.dispatch(markEntry({ entry: store.getState().entries.entries[0], read: false })) + expect(store.getState().entries.entries[0].keepUnread).toBe(true) + + // reloading the entry list resets the flag + await store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: false })) + expect(store.getState().entries.entries[0].keepUnread).toBeUndefined() + }) + it("marks all entries as read", () => { const store = configureStore({ reducer: reducers, diff --git a/commafeed-client/src/app/entries/slice.ts b/commafeed-client/src/app/entries/slice.ts index 620c1445..efb8dbf6 100644 --- a/commafeed-client/src/app/entries/slice.ts +++ b/commafeed-client/src/app/entries/slice.ts @@ -10,7 +10,11 @@ export interface EntrySource { id: string } -export type ExpendableEntry = Entry & { expanded?: boolean } +export type ExpendableEntry = Entry & { + expanded?: boolean + /** set when the user explicitly marks the entry as unread, prevents it from being marked as read again by scrolling in expanded mode until the entry list is reloaded */ + keepUnread?: boolean +} interface EntriesState { /** selected source */ @@ -71,11 +75,13 @@ export const entriesSlice = createSlice({ builder.addCase(markEntry.pending, (state, action) => { for (const e of state.entries.filter(e => e.id === action.meta.arg.entry.id)) { e.read = action.meta.arg.read + if (!action.meta.arg.read) e.keepUnread = true } }) builder.addCase(markMultipleEntries.pending, (state, action) => { for (const e of state.entries.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))) { e.read = action.meta.arg.read + if (!action.meta.arg.read) e.keepUnread = true } }) builder.addCase(markAllEntries.pending, (state, action) => { diff --git a/commafeed-client/src/components/content/FeedEntries.tsx b/commafeed-client/src/components/content/FeedEntries.tsx index 4e3dec38..749d4276 100644 --- a/commafeed-client/src/components/content/FeedEntries.tsx +++ b/commafeed-client/src/components/content/FeedEntries.tsx @@ -116,7 +116,8 @@ export function FeedEntries() { selectEntry({ entry: currentEntry, expand: false, - markAsRead: !!scrollMarks, + // don't mark as read entries that the user explicitly marked as unread + markAsRead: !!scrollMarks && !currentEntry.keepUnread, scrollToEntry: false, }) )