2025-06-17 18:31:30 +02:00
import { createSlice , type PayloadAction } from "@reduxjs/toolkit"
2025-06-27 16:29:31 +02:00
import { Constants } from "@/app/constants"
import { loadEntries , loadMoreEntries , markAllEntries , markEntry , markMultipleEntries , starEntry , tagEntry } from "@/app/entries/thunks"
import type { Entry } from "@/app/types"
2024-06-13 21:54:14 +02:00
export type EntrySourceType = "category" | "feed" | "tag"
export interface EntrySource {
type : EntrySourceType
id : string
}
2026-10-22 09:04:57 +02:00
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
}
2024-06-13 21:54:14 +02:00
interface EntriesState {
/** selected source */
source : EntrySource
sourceLabel : string
sourceWebsiteUrl : string
entries : ExpendableEntry [ ]
/ * * s t o r e s w h e n t h e f i r s t b a t c h o f e n t r i e s w e r e r e t r i e v e d
*
* this is used when marking all entries of a feed / category to only mark entries up to that timestamp as newer entries were potentially never shown
* /
timestamp? : number
selectedEntryId? : string
hasMore : boolean
loading : boolean
search? : string
scrollingToEntry : boolean
2025-04-09 20:12:29 +02:00
markAllAsReadConfirmationDialogOpen : boolean
2024-06-13 21:54:14 +02:00
}
const initialState : EntriesState = {
source : {
type : "category" ,
id : Constants.categories.all.id ,
} ,
sourceLabel : "" ,
sourceWebsiteUrl : "" ,
entries : [ ] ,
hasMore : true ,
loading : false ,
scrollingToEntry : false ,
2025-04-09 20:12:29 +02:00
markAllAsReadConfirmationDialogOpen : false ,
2024-06-13 21:54:14 +02:00
}
export const entriesSlice = createSlice ( {
name : "entries" ,
initialState ,
reducers : {
setSelectedEntry : ( state , action : PayloadAction < Entry > ) = > {
state . selectedEntryId = action . payload . id
} ,
setEntryExpanded : ( state , action : PayloadAction < { entry : Entry ; expanded : boolean } > ) = > {
for ( const e of state . entries . filter ( e = > e . id === action . payload . entry . id ) ) {
e . expanded = action . payload . expanded
}
} ,
setScrollingToEntry : ( state , action : PayloadAction < boolean > ) = > {
state . scrollingToEntry = action . payload
} ,
setSearch : ( state , action : PayloadAction < string > ) = > {
state . search = action . payload
} ,
2025-04-09 20:12:29 +02:00
setMarkAllAsReadConfirmationDialogOpen : ( state , action : PayloadAction < boolean > ) = > {
state . markAllAsReadConfirmationDialogOpen = action . payload
} ,
2024-06-13 21:54:14 +02:00
} ,
extraReducers : builder = > {
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
2026-10-22 09:04:57 +02:00
if ( ! action . meta . arg . read ) e . keepUnread = true
2024-06-13 21:54:14 +02:00
}
} )
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
2026-10-22 09:04:57 +02:00
if ( ! action . meta . arg . read ) e . keepUnread = true
2024-06-13 21:54:14 +02:00
}
} )
builder . addCase ( markAllEntries . pending , ( state , action ) = > {
for ( const e of state . entries . filter ( e = > ( action . meta . arg . req . olderThan ? e . date < action.meta.arg.req.olderThan : true ) ) ) {
e . read = true
}
} )
builder . addCase ( starEntry . pending , ( state , action ) = > {
for ( const e of state . entries . filter ( e = > action . meta . arg . entry . id === e . id && action . meta . arg . entry . feedId === e . feedId ) ) {
e . starred = action . meta . arg . starred
}
} )
builder . addCase ( loadEntries . pending , ( state , action ) = > {
state . source = action . meta . arg . source
state . entries = [ ]
state . timestamp = undefined
state . sourceLabel = ""
state . sourceWebsiteUrl = ""
state . hasMore = true
state . selectedEntryId = undefined
state . loading = true
} )
builder . addCase ( loadMoreEntries . pending , state = > {
state . loading = true
} )
builder . addCase ( loadEntries . fulfilled , ( state , action ) = > {
state . entries = action . payload . entries
state . timestamp = action . payload . timestamp
state . sourceLabel = action . payload . name
state . sourceWebsiteUrl = action . payload . feedLink
state . hasMore = action . payload . hasMore
state . loading = false
} )
builder . addCase ( loadMoreEntries . fulfilled , ( state , action ) = > {
// remove already existing entries
const entriesToAdd = action . payload . entries . filter ( e = > ! state . entries . some ( e2 = > e . id === e2 . id ) )
state . entries = [ . . . state . entries , . . . entriesToAdd ]
state . hasMore = action . payload . hasMore
state . loading = false
} )
builder . addCase ( tagEntry . pending , ( state , action ) = > {
for ( const e of state . entries . filter ( e = > + e . id === action . meta . arg . entryId ) ) {
e . tags = action . meta . arg . tags
}
} )
} ,
} )
2025-04-09 20:12:29 +02:00
export const { setSearch , setMarkAllAsReadConfirmationDialogOpen } = entriesSlice . actions