forked from Archives/Athou_commafeed
replace old client with new client from commafeed-ui repository
This commit is contained in:
146
commafeed-client/src/app/slices/entries.test.ts
Normal file
146
commafeed-client/src/app/slices/entries.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/* eslint-disable import/first */
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { DeepMockProxy, mockDeep, mockReset } from "vitest-mock-extended"
|
||||
|
||||
vi.doMock("app/client", () => ({ client: mockDeep() }))
|
||||
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { reducers } from "app/store"
|
||||
import { Entries, Entry } from "app/types"
|
||||
import { AxiosResponse } from "axios"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "./entries"
|
||||
|
||||
describe("entries", () => {
|
||||
const mockClient = client as DeepMockProxy<typeof client>
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(mockClient)
|
||||
})
|
||||
|
||||
it("loads entries", async () => {
|
||||
mockClient.feed.getEntries.mockResolvedValue({
|
||||
data: {
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: false,
|
||||
name: "my-feed",
|
||||
errorCount: 3,
|
||||
feedLink: "https://mysite.com/feed",
|
||||
timestamp: 123,
|
||||
ignoredReadStatus: false,
|
||||
},
|
||||
} as AxiosResponse<Entries>)
|
||||
|
||||
const store = configureStore({ reducer: reducers })
|
||||
const promise = store.dispatch(
|
||||
loadEntries({
|
||||
sourceType: "feed",
|
||||
req: {
|
||||
id: "feed-id",
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.getState().entries.source.type).toBe("feed")
|
||||
expect(store.getState().entries.source.id).toBe("feed-id")
|
||||
expect(store.getState().entries.entries).toStrictEqual([])
|
||||
expect(store.getState().entries.hasMore).toBe(true)
|
||||
expect(store.getState().entries.sourceLabel).toBe("")
|
||||
expect(store.getState().entries.sourceWebsiteUrl).toBe("")
|
||||
expect(store.getState().entries.timestamp).toBeUndefined()
|
||||
|
||||
await promise
|
||||
expect(store.getState().entries.source.type).toBe("feed")
|
||||
expect(store.getState().entries.source.id).toBe("feed-id")
|
||||
expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }])
|
||||
expect(store.getState().entries.hasMore).toBe(false)
|
||||
expect(store.getState().entries.sourceLabel).toBe("my-feed")
|
||||
expect(store.getState().entries.sourceWebsiteUrl).toBe("https://mysite.com/feed")
|
||||
expect(store.getState().entries.timestamp).toBe(123)
|
||||
})
|
||||
|
||||
it("loads more entries", async () => {
|
||||
mockClient.category.getEntries.mockResolvedValue({
|
||||
data: {
|
||||
entries: [{ id: "4" } as Entry],
|
||||
hasMore: false,
|
||||
name: "my-feed",
|
||||
errorCount: 3,
|
||||
feedLink: "https://mysite.com/feed",
|
||||
timestamp: 123,
|
||||
ignoredReadStatus: false,
|
||||
},
|
||||
} as AxiosResponse<Entries>)
|
||||
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
entries: {
|
||||
source: {
|
||||
type: "category",
|
||||
id: "category-id",
|
||||
},
|
||||
sourceLabel: "",
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
const promise = store.dispatch(loadMoreEntries())
|
||||
|
||||
await promise
|
||||
expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }, { id: "4" }])
|
||||
expect(store.getState().entries.hasMore).toBe(false)
|
||||
})
|
||||
|
||||
it("marks an entry as read", async () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
entries: {
|
||||
source: {
|
||||
type: "category",
|
||||
id: "category-id",
|
||||
},
|
||||
sourceLabel: "",
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
|
||||
hasMore: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
store.dispatch(markEntry({ entry: { id: "3" } as Entry, read: true }))
|
||||
expect(store.getState().entries.entries).toStrictEqual([
|
||||
{ id: "3", read: true },
|
||||
{ id: "4", read: false },
|
||||
])
|
||||
expect(mockClient.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
|
||||
})
|
||||
|
||||
it("marks all entries as read", async () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
entries: {
|
||||
source: {
|
||||
type: "category",
|
||||
id: "category-id",
|
||||
},
|
||||
sourceLabel: "",
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
|
||||
hasMore: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } }))
|
||||
expect(store.getState().entries.entries).toStrictEqual([
|
||||
{ id: "3", read: true },
|
||||
{ id: "4", read: true },
|
||||
])
|
||||
expect(mockClient.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true })
|
||||
})
|
||||
})
|
||||
205
commafeed-client/src/app/slices/entries.ts
Normal file
205
commafeed-client/src/app/slices/entries.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { RootState } from "app/store"
|
||||
import { Entries, Entry, GetEntriesRequest, MarkRequest } from "app/types"
|
||||
|
||||
export type EntrySourceType = "category" | "feed"
|
||||
export type EntrySource = { type: EntrySourceType; id: string }
|
||||
export type ExpendableEntry = Entry & { expanded?: boolean }
|
||||
|
||||
interface EntriesState {
|
||||
/** selected source */
|
||||
source: EntrySource
|
||||
sourceLabel: string
|
||||
sourceWebsiteUrl: string
|
||||
entries: ExpendableEntry[]
|
||||
/** stores when the first batch of entries were retrieved
|
||||
*
|
||||
* 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
|
||||
}
|
||||
|
||||
const initialState: EntriesState = {
|
||||
source: {
|
||||
type: "category",
|
||||
id: Constants.categoryIds.all,
|
||||
},
|
||||
sourceLabel: "",
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [],
|
||||
hasMore: true,
|
||||
}
|
||||
|
||||
const getEndpoint = (sourceType: EntrySourceType) => (sourceType === "category" ? client.category.getEntries : client.feed.getEntries)
|
||||
export const loadEntries = createAsyncThunk<Entries, { req: GetEntriesRequest; sourceType: EntrySourceType }, { state: RootState }>(
|
||||
"entries/load",
|
||||
async arg => {
|
||||
const endpoint = getEndpoint(arg.sourceType)
|
||||
const result = await endpoint({
|
||||
...arg.req,
|
||||
offset: 0,
|
||||
limit: 50,
|
||||
})
|
||||
return result.data
|
||||
}
|
||||
)
|
||||
export const loadMoreEntries = createAsyncThunk<Entries, void, { state: RootState }>("entries/loadMore", async (_, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const offset =
|
||||
state.user.settings?.readingMode === "all" ? state.entries.entries.length : state.entries.entries.filter(e => !e.read).length
|
||||
const endpoint = getEndpoint(state.entries.source.type)
|
||||
const result = await endpoint({
|
||||
id: state.entries.source.id,
|
||||
readType: state.user.settings?.readingMode,
|
||||
order: state.user.settings?.readingOrder,
|
||||
offset,
|
||||
limit: 50,
|
||||
})
|
||||
return result.data
|
||||
})
|
||||
export const reloadEntries = createAsyncThunk<Entries, void, { state: RootState }>("entries/reload", async (_, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const endpoint = getEndpoint(state.entries.source.type)
|
||||
const result = await endpoint({
|
||||
id: state.entries.source.id,
|
||||
readType: state.user.settings?.readingMode,
|
||||
order: state.user.settings?.readingOrder,
|
||||
offset: 0,
|
||||
limit: 50,
|
||||
})
|
||||
return result.data
|
||||
})
|
||||
export const markEntry = createAsyncThunk(
|
||||
"entries/entry/mark",
|
||||
(arg: { entry: Entry; read: boolean }) => {
|
||||
client.entry.mark({
|
||||
id: arg.entry.id,
|
||||
read: arg.read,
|
||||
})
|
||||
},
|
||||
{
|
||||
condition: arg => arg.entry.read !== arg.read,
|
||||
}
|
||||
)
|
||||
export const markAllEntries = createAsyncThunk("entries/entry/markAll", (arg: { sourceType: EntrySourceType; req: MarkRequest }) => {
|
||||
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries
|
||||
endpoint(arg.req)
|
||||
})
|
||||
export const selectEntry = createAsyncThunk<void, Entry, { state: RootState }>("entries/entry/select", (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const entry = state.entries.entries.find(e => e.id === arg.id)
|
||||
if (!entry) return
|
||||
|
||||
// only mark entry as read if we're expanding
|
||||
if (!entry.expanded) {
|
||||
thunkApi.dispatch(markEntry({ entry, read: true }))
|
||||
}
|
||||
|
||||
// set entry as selected
|
||||
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
|
||||
|
||||
// collapse or expand entry if needed
|
||||
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
|
||||
if (entry === previouslySelectedEntry) {
|
||||
// selecting an entry already selected toggles expanded status
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: !entry.expanded }))
|
||||
} else {
|
||||
if (previouslySelectedEntry) {
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry: previouslySelectedEntry, expanded: false }))
|
||||
}
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: true }))
|
||||
}
|
||||
})
|
||||
export const selectPreviousEntry = createAsyncThunk<void, void, { state: RootState }>("entries/entry/selectPrevious", (_, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
|
||||
if (previousIndex >= 0) {
|
||||
thunkApi.dispatch(selectEntry(entries[previousIndex]))
|
||||
}
|
||||
})
|
||||
export const selectNextEntry = createAsyncThunk<void, void, { state: RootState }>("entries/entry/selectNext", (_, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
|
||||
if (nextIndex < entries.length) {
|
||||
thunkApi.dispatch(selectEntry(entries[nextIndex]))
|
||||
}
|
||||
})
|
||||
|
||||
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 }>) => {
|
||||
state.entries
|
||||
.filter(e => e.id === action.payload.entry.id)
|
||||
.forEach(e => {
|
||||
e.expanded = action.payload.expanded
|
||||
})
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(markEntry.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => e.id === action.meta.arg.entry.id)
|
||||
.forEach(e => {
|
||||
e.read = action.meta.arg.read
|
||||
})
|
||||
})
|
||||
builder.addCase(markAllEntries.pending, state => {
|
||||
state.entries.forEach(e => {
|
||||
e.read = true
|
||||
})
|
||||
})
|
||||
builder.addCase(loadEntries.pending, (state, action) => {
|
||||
state.source = {
|
||||
type: action.meta.arg.sourceType,
|
||||
id: action.meta.arg.req.id,
|
||||
}
|
||||
state.entries = []
|
||||
state.timestamp = undefined
|
||||
state.sourceLabel = ""
|
||||
state.sourceWebsiteUrl = ""
|
||||
state.hasMore = true
|
||||
state.selectedEntryId = undefined
|
||||
})
|
||||
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
|
||||
})
|
||||
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
|
||||
})
|
||||
builder.addCase(reloadEntries.pending, state => {
|
||||
state.entries = []
|
||||
state.timestamp = undefined
|
||||
state.sourceLabel = ""
|
||||
state.sourceWebsiteUrl = ""
|
||||
state.hasMore = true
|
||||
state.selectedEntryId = undefined
|
||||
})
|
||||
builder.addCase(reloadEntries.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
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export default entriesSlice.reducer
|
||||
10
commafeed-client/src/app/slices/redirect.test.ts
Normal file
10
commafeed-client/src/app/slices/redirect.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { store } from "app/store"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { redirectToCategory } from "./redirect"
|
||||
|
||||
describe("redirects", () => {
|
||||
it("redirects to category", async () => {
|
||||
await store.dispatch(redirectToCategory("1"))
|
||||
expect(store.getState().redirect.to).toBe("/app/category/1")
|
||||
})
|
||||
})
|
||||
52
commafeed-client/src/app/slices/redirect.ts
Normal file
52
commafeed-client/src/app/slices/redirect.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { Constants } from "app/constants"
|
||||
import { RootState } from "app/store"
|
||||
|
||||
interface RedirectState {
|
||||
to?: string
|
||||
}
|
||||
|
||||
const initialState: RedirectState = {}
|
||||
|
||||
export const redirectToLogin = createAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
|
||||
export const redirectToRegistration = createAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
|
||||
export const redirectToPasswordRecovery = createAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/passwordRecovery"))
|
||||
)
|
||||
export const redirectToSelectedSource = createAsyncThunk<void, void, { state: RootState }>("redirect/selectedSource", (_, thunkApi) => {
|
||||
const { source } = thunkApi.getState().entries
|
||||
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
|
||||
})
|
||||
export const redirectToCategory = createAsyncThunk("redirect/category", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
|
||||
)
|
||||
export const redirectToRootCategory = createAsyncThunk("redirect/category/root", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectToCategory(Constants.categoryIds.all))
|
||||
)
|
||||
export const redirectToCategoryDetails = createAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
|
||||
)
|
||||
export const redirectToFeed = createAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
|
||||
)
|
||||
export const redirectToFeedDetails = createAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
|
||||
)
|
||||
export const redirectToAdd = createAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
|
||||
export const redirectToSettings = createAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
|
||||
export const redirectToAdminUsers = createAsyncThunk("redirect/admin/users", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/app/admin/users"))
|
||||
)
|
||||
|
||||
export const redirectSlice = createSlice({
|
||||
name: "redirect",
|
||||
initialState,
|
||||
reducers: {
|
||||
redirectTo: (state, action: PayloadAction<string | undefined>) => {
|
||||
state.to = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { redirectTo } = redirectSlice.actions
|
||||
export default redirectSlice.reducer
|
||||
23
commafeed-client/src/app/slices/server.ts
Normal file
23
commafeed-client/src/app/slices/server.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { ServerInfo } from "app/types"
|
||||
|
||||
interface ServerState {
|
||||
serverInfos?: ServerInfo
|
||||
}
|
||||
|
||||
const initialState: ServerState = {}
|
||||
|
||||
export const reloadServerInfos = createAsyncThunk("server/infos", () => client.server.getServerInfos().then(r => r.data))
|
||||
export const serverSlice = createSlice({
|
||||
name: "server",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadServerInfos.fulfilled, (state, action) => {
|
||||
state.serverInfos = action.payload
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export default serverSlice.reducer
|
||||
77
commafeed-client/src/app/slices/tree.ts
Normal file
77
commafeed-client/src/app/slices/tree.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { Category, CollapseRequest } from "app/types"
|
||||
import { visitCategoryTree } from "app/utils"
|
||||
import { markAllEntries, markEntry } from "./entries"
|
||||
import { redirectTo } from "./redirect"
|
||||
|
||||
interface TreeState {
|
||||
rootCategory?: Category
|
||||
mobileMenuOpen: boolean
|
||||
}
|
||||
|
||||
const initialState: TreeState = {
|
||||
mobileMenuOpen: false,
|
||||
}
|
||||
|
||||
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
|
||||
export const collapseTreeCategory = createAsyncThunk("tree/category/collapse", async (req: CollapseRequest) =>
|
||||
client.category.collapse(req)
|
||||
)
|
||||
|
||||
export const treeSlice = createSlice({
|
||||
name: "tree",
|
||||
initialState,
|
||||
reducers: {
|
||||
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.mobileMenuOpen = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadTree.fulfilled, (state, action) => {
|
||||
state.rootCategory = action.payload
|
||||
})
|
||||
builder.addCase(collapseTreeCategory.pending, (state, action) => {
|
||||
if (!state.rootCategory) return
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
if (+c.id === action.meta.arg.id) c.expanded = !action.meta.arg.collapse
|
||||
})
|
||||
})
|
||||
builder.addCase(markEntry.pending, (state, action) => {
|
||||
if (!state.rootCategory) return
|
||||
visitCategoryTree(state.rootCategory, c =>
|
||||
c.feeds
|
||||
.filter(f => f.id === +action.meta.arg.entry.feedId)
|
||||
.forEach(f => {
|
||||
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1
|
||||
})
|
||||
)
|
||||
})
|
||||
builder.addCase(markAllEntries.pending, (state, action) => {
|
||||
if (!state.rootCategory) return
|
||||
const { sourceType } = action.meta.arg
|
||||
const sourceId = action.meta.arg.req.id
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
if (sourceType === "category" && c.id === sourceId) {
|
||||
visitCategoryTree(c, c2 =>
|
||||
c2.feeds.forEach(f => {
|
||||
f.unread = 0
|
||||
})
|
||||
)
|
||||
} else if (sourceType === "feed") {
|
||||
c.feeds
|
||||
.filter(f => f.id === +sourceId)
|
||||
.forEach(f => {
|
||||
f.unread = 0
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
builder.addCase(redirectTo, state => {
|
||||
state.mobileMenuOpen = false
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { setMobileMenuOpen } = treeSlice.actions
|
||||
export default treeSlice.reducer
|
||||
80
commafeed-client/src/app/slices/user.ts
Normal file
80
commafeed-client/src/app/slices/user.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { showNotification } from "@mantine/notifications"
|
||||
import { createAsyncThunk, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { RootState } from "app/store"
|
||||
import { ReadingMode, ReadingOrder, Settings, UserModel } from "app/types"
|
||||
|
||||
interface UserState {
|
||||
settings?: Settings
|
||||
profile?: UserModel
|
||||
}
|
||||
|
||||
const initialState: UserState = {}
|
||||
|
||||
export const reloadSettings = createAsyncThunk("settings/reload", () => client.user.getSettings().then(r => r.data))
|
||||
export const reloadProfile = createAsyncThunk("profile/reload", () => client.user.getProfile().then(r => r.data))
|
||||
export const changeReadingMode = createAsyncThunk<void, ReadingMode, { state: RootState }>(
|
||||
"settings/readingMode",
|
||||
(readingMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, readingMode })
|
||||
}
|
||||
)
|
||||
export const changeReadingOrder = createAsyncThunk<void, ReadingOrder, { state: RootState }>(
|
||||
"settings/readingOrder",
|
||||
(readingOrder, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, readingOrder })
|
||||
}
|
||||
)
|
||||
export const changeLanguage = createAsyncThunk<void, string, { state: RootState }>("settings/language", (language, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, language })
|
||||
})
|
||||
export const changeScrollSpeed = createAsyncThunk<void, boolean, { state: RootState }>("settings/scrollSpeed", (speed, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
|
||||
})
|
||||
|
||||
export const userSlice = createSlice({
|
||||
name: "user",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadSettings.fulfilled, (state, action) => {
|
||||
state.settings = action.payload
|
||||
})
|
||||
builder.addCase(reloadProfile.fulfilled, (state, action) => {
|
||||
state.profile = action.payload
|
||||
})
|
||||
builder.addCase(changeReadingMode.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.readingMode = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeReadingOrder.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.readingOrder = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeLanguage.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.language = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeScrollSpeed.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.scrollSpeed = action.meta.arg ? 400 : 0
|
||||
})
|
||||
builder.addMatcher(isAnyOf(changeLanguage.fulfilled, changeScrollSpeed.fulfilled), () => {
|
||||
showNotification({
|
||||
message: t`Settings saved.`,
|
||||
color: "green",
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export default userSlice.reducer
|
||||
Reference in New Issue
Block a user