split thunks from slices to avoid circular dependencies

This commit is contained in:
Athou
2023-12-28 22:11:03 +01:00
parent bb4bb0c7d7
commit 0ea0db48db
41 changed files with 460 additions and 413 deletions

View File

@@ -5,8 +5,8 @@ import { useColorScheme } from "@mantine/hooks"
import { ModalsProvider } from "@mantine/modals" import { ModalsProvider } from "@mantine/modals"
import { Notifications } from "@mantine/notifications" import { Notifications } from "@mantine/notifications"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectTo } from "app/slices/redirect" import { redirectTo } from "app/redirect/slice"
import { reloadServerInfos } from "app/slices/server" import { reloadServerInfos } from "app/server/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { categoryUnreadCount } from "app/utils" import { categoryUnreadCount } from "app/utils"
import { ErrorBoundary } from "components/ErrorBoundary" import { ErrorBoundary } from "components/ErrorBoundary"

View File

@@ -1,12 +1,12 @@
/* eslint-disable import/first */ /* eslint-disable import/first */
import { configureStore } from "@reduxjs/toolkit" import { configureStore } from "@reduxjs/toolkit"
import { type client } from "app/client" import { type client } from "app/client"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
import { reducers } from "app/store" import { reducers } from "app/store"
import { type Entries, type Entry } from "app/types" import { type Entries, type Entry } from "app/types"
import { type AxiosResponse } from "axios" import { type AxiosResponse } from "axios"
import { beforeEach, describe, expect, it, vi } from "vitest" import { beforeEach, describe, expect, it, vi } from "vitest"
import { mockReset } from "vitest-mock-extended" import { mockReset } from "vitest-mock-extended"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "./entries"
const mockClient = await vi.hoisted(async () => { const mockClient = await vi.hoisted(async () => {
const mockModule = await import("vitest-mock-extended") const mockModule = await import("vitest-mock-extended")

View File

@@ -0,0 +1,134 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { Constants } from "app/constants"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks"
import { type Entry } from "app/types"
export type EntrySourceType = "category" | "feed" | "tag"
export interface 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
loading: boolean
search?: string
scrollingToEntry: boolean
}
const initialState: EntriesState = {
source: {
type: "category",
id: Constants.categories.all.id,
},
sourceLabel: "",
sourceWebsiteUrl: "",
entries: [],
hasMore: true,
loading: false,
scrollingToEntry: false,
}
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
})
},
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
state.scrollingToEntry = action.payload
},
setSearch: (state, action: PayloadAction<string>) => {
state.search = action.payload
},
},
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(markMultipleEntries.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))
.forEach(e => {
e.read = action.meta.arg.read
})
})
builder.addCase(markAllEntries.pending, (state, action) => {
state.entries
.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))
.forEach(e => {
e.read = true
})
})
builder.addCase(starEntry.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)
.forEach(e => {
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) => {
state.entries
.filter(e => +e.id === action.meta.arg.entryId)
.forEach(e => {
e.tags = action.meta.arg.tags
})
})
},
})
export const { setSearch } = entriesSlice.actions

View File

@@ -1,55 +1,13 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit" import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client" import { client } from "app/client"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { type RootState } from "app/store" import { entriesSlice, type EntrySource, type EntrySourceType, setSearch } from "app/entries/slice"
import { createAppAsyncThunk } from "app/thunk" import type { RootState } from "app/store"
import { type Entry, type MarkRequest, type TagRequest } from "app/types" import { reloadTree } from "app/tree/thunks"
import type { Entry, MarkRequest, TagRequest } from "app/types"
import { reloadTags } from "app/user/thunks"
import { scrollToWithCallback } from "app/utils" import { scrollToWithCallback } from "app/utils"
import { flushSync } from "react-dom" import { flushSync } from "react-dom"
// eslint-disable-next-line import/no-cycle
import { reloadTree } from "./tree"
// eslint-disable-next-line import/no-cycle
import { reloadTags } from "./user"
export type EntrySourceType = "category" | "feed" | "tag"
export interface 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
loading: boolean
search?: string
scrollingToEntry: boolean
}
const initialState: EntriesState = {
source: {
type: "category",
id: Constants.categories.all.id,
},
sourceLabel: "",
sourceWebsiteUrl: "",
entries: [],
hasMore: true,
loading: false,
scrollingToEntry: false,
}
const getEndpoint = (sourceType: EntrySourceType) => const getEndpoint = (sourceType: EntrySourceType) =>
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
@@ -280,94 +238,3 @@ export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: Tag
await client.entry.tag(arg) await client.entry.tag(arg)
thunkApi.dispatch(reloadTags()) thunkApi.dispatch(reloadTags())
}) })
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
})
},
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
state.scrollingToEntry = action.payload
},
setSearch: (state, action: PayloadAction<string>) => {
state.search = action.payload
},
},
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(markMultipleEntries.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))
.forEach(e => {
e.read = action.meta.arg.read
})
})
builder.addCase(markAllEntries.pending, (state, action) => {
state.entries
.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))
.forEach(e => {
e.read = true
})
})
builder.addCase(starEntry.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)
.forEach(e => {
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) => {
state.entries
.filter(e => +e.id === action.meta.arg.entryId)
.forEach(e => {
e.tags = action.meta.arg.tags
})
})
},
})
export const { setSearch } = entriesSlice.actions
export default entriesSlice.reducer

View File

@@ -1,6 +1,6 @@
import { redirectToCategory } from "app/redirect/thunks"
import { store } from "app/store" import { store } from "app/store"
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import { redirectToCategory } from "./redirect"
describe("redirects", () => { describe("redirects", () => {
it("redirects to category", async () => { it("redirects to category", async () => {

View File

@@ -0,0 +1,19 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
interface RedirectState {
to?: string
}
const initialState: RedirectState = {}
export const redirectSlice = createSlice({
name: "redirect",
initialState,
reducers: {
redirectTo: (state, action: PayloadAction<string | undefined>) => {
state.to = action.payload
},
},
})
export const { redirectTo } = redirectSlice.actions

View File

@@ -1,12 +1,6 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit" import { createAppAsyncThunk } from "app/async-thunk"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { createAppAsyncThunk } from "app/thunk" import { redirectTo } from "app/redirect/slice"
interface RedirectState {
to?: string
}
const initialState: RedirectState = {}
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login"))) export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register"))) export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
@@ -49,16 +43,3 @@ export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (
) )
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate"))) export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about"))) export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
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

View File

@@ -1,6 +1,5 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit" import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { client } from "app/client" import { reloadServerInfos } from "app/server/thunks"
import { createAppAsyncThunk } from "app/thunk"
import { type ServerInfo } from "app/types" import { type ServerInfo } from "app/types"
interface ServerState { interface ServerState {
@@ -12,7 +11,6 @@ const initialState: ServerState = {
webSocketConnected: false, webSocketConnected: false,
} }
export const reloadServerInfos = createAppAsyncThunk("server/infos", async () => await client.server.getServerInfos().then(r => r.data))
export const serverSlice = createSlice({ export const serverSlice = createSlice({
name: "server", name: "server",
initialState, initialState,
@@ -29,4 +27,3 @@ export const serverSlice = createSlice({
}) })
export const { setWebSocketConnected } = serverSlice.actions export const { setWebSocketConnected } = serverSlice.actions
export default serverSlice.reducer

View File

@@ -0,0 +1,4 @@
import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client"
export const reloadServerInfos = createAppAsyncThunk("server/infos", async () => await client.server.getServerInfos().then(r => r.data))

View File

@@ -1,167 +0,0 @@
import { t } from "@lingui/macro"
import { showNotification } from "@mantine/notifications"
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
import { client } from "app/client"
import { createAppAsyncThunk } from "app/thunk"
import { type ReadingMode, type ReadingOrder, type Settings, type SharingSettings, type UserModel } from "app/types"
// eslint-disable-next-line import/no-cycle
import { reloadEntries } from "./entries"
interface UserState {
settings?: Settings
profile?: UserModel
tags?: string[]
}
const initialState: UserState = {}
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingMode })
thunkApi.dispatch(reloadEntries())
})
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingOrder })
thunkApi.dispatch(reloadEntries())
})
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, language })
})
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
})
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, showRead })
})
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollMarks })
})
export const changeAlwaysScrollToEntry = createAppAsyncThunk("settings/alwaysScrollToEntry", (alwaysScrollToEntry: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, alwaysScrollToEntry })
})
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
"settings/markAllAsReadConfirmation",
(markAllAsReadConfirmation: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
}
)
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, customContextMenu })
})
export const changeSharingSetting = createAppAsyncThunk(
"settings/sharingSetting",
(
sharingSetting: {
site: keyof SharingSettings
value: boolean
},
thunkApi
) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
sharingSettings: {
...settings.sharingSettings,
[sharingSetting.site]: sharingSetting.value,
},
})
}
)
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(reloadTags.fulfilled, (state, action) => {
state.tags = 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.addCase(changeShowRead.pending, (state, action) => {
if (!state.settings) return
state.settings.showRead = action.meta.arg
})
builder.addCase(changeScrollMarks.pending, (state, action) => {
if (!state.settings) return
state.settings.scrollMarks = action.meta.arg
})
builder.addCase(changeAlwaysScrollToEntry.pending, (state, action) => {
if (!state.settings) return
state.settings.alwaysScrollToEntry = action.meta.arg
})
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
if (!state.settings) return
state.settings.markAllAsReadConfirmation = action.meta.arg
})
builder.addCase(changeCustomContextMenu.pending, (state, action) => {
if (!state.settings) return
state.settings.customContextMenu = action.meta.arg
})
builder.addCase(changeSharingSetting.pending, (state, action) => {
if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
})
builder.addMatcher(
isAnyOf(
changeLanguage.fulfilled,
changeScrollSpeed.fulfilled,
changeShowRead.fulfilled,
changeScrollMarks.fulfilled,
changeAlwaysScrollToEntry.fulfilled,
changeMarkAllAsReadConfirmation.fulfilled,
changeCustomContextMenu.fulfilled,
changeSharingSetting.fulfilled
),
() => {
showNotification({
message: t`Settings saved.`,
color: "green",
})
}
)
},
})
export default userSlice.reducer

View File

@@ -1,18 +1,18 @@
import { configureStore } from "@reduxjs/toolkit" import { configureStore } from "@reduxjs/toolkit"
import { setupListeners } from "@reduxjs/toolkit/query" import { setupListeners } from "@reduxjs/toolkit/query"
import { entriesSlice } from "app/entries/slice"
import { redirectSlice } from "app/redirect/slice"
import { serverSlice } from "app/server/slice"
import { treeSlice } from "app/tree/slice"
import { userSlice } from "app/user/slice"
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux" import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
import entriesReducer from "./slices/entries"
import redirectReducer from "./slices/redirect"
import serverReducer from "./slices/server"
import treeReducer from "./slices/tree"
import userReducer from "./slices/user"
export const reducers = { export const reducers = {
entries: entriesReducer, entries: entriesSlice.reducer,
redirect: redirectReducer, redirect: redirectSlice.reducer,
tree: treeReducer, tree: treeSlice.reducer,
server: serverReducer, server: serverSlice.reducer,
user: userReducer, user: userSlice.reducer,
} }
export const store = configureStore({ reducer: reducers }) export const store = configureStore({ reducer: reducers })

View File

@@ -1,11 +1,9 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit" import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { client } from "app/client" import { markEntry } from "app/entries/thunks"
import { createAppAsyncThunk } from "app/thunk" import { redirectTo } from "app/redirect/slice"
import { type Category, type CollapseRequest } from "app/types" import { collapseTreeCategory, reloadTree } from "app/tree/thunks"
import { type Category } from "app/types"
import { visitCategoryTree } from "app/utils" import { visitCategoryTree } from "app/utils"
// eslint-disable-next-line import/no-cycle
import { markEntry } from "./entries"
import { redirectTo } from "./redirect"
interface TreeState { interface TreeState {
rootCategory?: Category rootCategory?: Category
@@ -20,12 +18,6 @@ const initialState: TreeState = {
sidebarVisible: true, sidebarVisible: true,
} }
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
export const collapseTreeCategory = createAppAsyncThunk(
"tree/category/collapse",
async (req: CollapseRequest) => await client.category.collapse(req)
)
export const treeSlice = createSlice({ export const treeSlice = createSlice({
name: "tree", name: "tree",
initialState, initialState,
@@ -67,4 +59,3 @@ export const treeSlice = createSlice({
}) })
export const { setMobileMenuOpen, setSidebarWidth, toggleSidebar } = treeSlice.actions export const { setMobileMenuOpen, setSidebarWidth, toggleSidebar } = treeSlice.actions
export default treeSlice.reducer

View File

@@ -0,0 +1,9 @@
import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client"
import type { CollapseRequest } from "app/types"
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
export const collapseTreeCategory = createAppAsyncThunk(
"tree/category/collapse",
async (req: CollapseRequest) => await client.category.collapse(req)
)

View File

@@ -0,0 +1,102 @@
import { t } from "@lingui/macro"
import { showNotification } from "@mantine/notifications"
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
import { type Settings, type UserModel } from "app/types"
import {
changeAlwaysScrollToEntry,
changeCustomContextMenu,
changeLanguage,
changeMarkAllAsReadConfirmation,
changeReadingMode,
changeReadingOrder,
changeScrollMarks,
changeScrollSpeed,
changeSharingSetting,
changeShowRead,
reloadProfile,
reloadSettings,
reloadTags,
} from "./thunks"
interface UserState {
settings?: Settings
profile?: UserModel
tags?: string[]
}
const initialState: UserState = {}
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(reloadTags.fulfilled, (state, action) => {
state.tags = 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.addCase(changeShowRead.pending, (state, action) => {
if (!state.settings) return
state.settings.showRead = action.meta.arg
})
builder.addCase(changeScrollMarks.pending, (state, action) => {
if (!state.settings) return
state.settings.scrollMarks = action.meta.arg
})
builder.addCase(changeAlwaysScrollToEntry.pending, (state, action) => {
if (!state.settings) return
state.settings.alwaysScrollToEntry = action.meta.arg
})
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
if (!state.settings) return
state.settings.markAllAsReadConfirmation = action.meta.arg
})
builder.addCase(changeCustomContextMenu.pending, (state, action) => {
if (!state.settings) return
state.settings.customContextMenu = action.meta.arg
})
builder.addCase(changeSharingSetting.pending, (state, action) => {
if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
})
builder.addMatcher(
isAnyOf(
changeLanguage.fulfilled,
changeScrollSpeed.fulfilled,
changeShowRead.fulfilled,
changeScrollMarks.fulfilled,
changeAlwaysScrollToEntry.fulfilled,
changeMarkAllAsReadConfirmation.fulfilled,
changeCustomContextMenu.fulfilled,
changeSharingSetting.fulfilled
),
() => {
showNotification({
message: t`Settings saved.`,
color: "green",
})
}
)
},
})

View File

@@ -0,0 +1,78 @@
import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client"
import { reloadEntries } from "app/entries/thunks"
import type { ReadingMode, ReadingOrder, SharingSettings } from "app/types"
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingMode })
thunkApi.dispatch(reloadEntries())
})
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingOrder })
thunkApi.dispatch(reloadEntries())
})
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, language })
})
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
})
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, showRead })
})
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollMarks })
})
export const changeAlwaysScrollToEntry = createAppAsyncThunk("settings/alwaysScrollToEntry", (alwaysScrollToEntry: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, alwaysScrollToEntry })
})
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
"settings/markAllAsReadConfirmation",
(markAllAsReadConfirmation: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
}
)
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, customContextMenu })
})
export const changeSharingSetting = createAppAsyncThunk(
"settings/sharingSetting",
(
sharingSetting: {
site: keyof SharingSettings
value: boolean
},
thunkApi
) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
sharingSettings: {
...settings.sharingSettings,
[sharingSetting.site]: sharingSetting.value,
},
})
}
)

View File

@@ -77,7 +77,6 @@ class HighlightMatcher extends Matcher {
return <Mark>{children}</Mark> return <Mark>{children}</Mark>
} }
// eslint-disable-next-line class-methods-use-this
asTag(): string { asTag(): string {
return "span" return "span"
} }

View File

@@ -2,8 +2,8 @@ import { Trans } from "@lingui/macro"
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { openModal } from "@mantine/modals" import { openModal } from "@mantine/modals"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { type ExpendableEntry } from "app/entries/slice"
import { import {
type ExpendableEntry,
loadMoreEntries, loadMoreEntries,
markAllEntries, markAllEntries,
markEntry, markEntry,
@@ -12,10 +12,10 @@ import {
selectNextEntry, selectNextEntry,
selectPreviousEntry, selectPreviousEntry,
starEntry, starEntry,
} from "app/slices/entries" } from "app/entries/thunks"
import { redirectToRootCategory } from "app/slices/redirect" import { redirectToRootCategory } from "app/redirect/thunks"
import { toggleSidebar } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { toggleSidebar } from "app/tree/slice"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"

View File

@@ -1,8 +1,8 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { createStyles, Group } from "@mantine/core" import { createStyles, Group } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries" import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
import { redirectToFeed } from "app/slices/redirect" import { redirectToFeed } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { type Entry } from "app/types" import { type Entry } from "app/types"
import { truncate } from "app/utils" import { truncate } from "app/utils"

View File

@@ -1,6 +1,6 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { Group, Indicator, MultiSelect, Popover } from "@mantine/core" import { Group, Indicator, MultiSelect, Popover } from "@mantine/core"
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries" import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { type Entry } from "app/types" import { type Entry } from "app/types"
import { ActionButton } from "components/ActionButton" import { ActionButton } from "components/ActionButton"
@@ -22,7 +22,13 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v) const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v)
const readStatusButtonClicked = async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read })) const readStatusButtonClicked = async () =>
await dispatch(
markEntry({
entry: props.entry,
read: !props.entry.read,
})
)
const onTagsChange = async (values: string[]) => const onTagsChange = async (values: string[]) =>
await dispatch( await dispatch(
tagEntry({ tagEntry({
@@ -44,7 +50,14 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<ActionButton <ActionButton
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />} icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
onClick={async () => await dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))} onClick={async () =>
await dispatch(
starEntry({
entry: props.entry,
starred: !props.entry.starred,
})
)
}
/> />
{showSharingButtons && ( {showSharingButtons && (

View File

@@ -2,9 +2,9 @@ import { t, Trans } from "@lingui/macro"
import { Box, Button, Group, Stack, TextInput } from "@mantine/core" import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/redirect/thunks"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { reloadTree } from "app/tree/thunks"
import { type AddCategoryRequest } from "app/types" import { type AddCategoryRequest } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"

View File

@@ -2,9 +2,9 @@ import { t, Trans } from "@lingui/macro"
import { Box, Button, FileInput, Group, Stack } from "@mantine/core" import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/redirect/thunks"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { reloadTree } from "app/tree/thunks"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbFileImport } from "react-icons/tb" import { TbFileImport } from "react-icons/tb"

View File

@@ -3,9 +3,9 @@ import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToFeed, redirectToSelectedSource } from "app/slices/redirect" import { redirectToFeed, redirectToSelectedSource } from "app/redirect/thunks"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { reloadTree } from "app/tree/thunks"
import { type FeedInfoRequest, type SubscribeRequest } from "app/types" import { type FeedInfoRequest, type SubscribeRequest } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useState } from "react" import { useState } from "react"
@@ -46,8 +46,11 @@ export function Subscribe() {
}) })
const previousStep = () => { const previousStep = () => {
if (activeStep === 0) dispatch(redirectToSelectedSource()) if (activeStep === 0) {
else setActiveStep(activeStep - 1) dispatch(redirectToSelectedSource())
} else {
setActiveStep(activeStep - 1)
}
} }
const nextStep = (e: React.FormEvent<HTMLFormElement>) => { const nextStep = (e: React.FormEvent<HTMLFormElement>) => {
if (activeStep === 0) { if (activeStep === 0) {

View File

@@ -1,9 +1,9 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { ActionIcon, Box, Center, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core" import { ActionIcon, Box, Center, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/slices/entries" import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { changeReadingMode, changeReadingOrder } from "app/user/thunks"
import { ActionButton } from "components/ActionButton" import { ActionButton } from "components/ActionButton"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useActionButton } from "hooks/useActionButton" import { useActionButton } from "hooks/useActionButton"

View File

@@ -1,7 +1,7 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core" import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/slices/entries" import { markAllEntries } from "app/entries/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButton" import { ActionButton } from "components/ActionButton"
import { useState } from "react" import { useState } from "react"

View File

@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
import { Box, Divider, Group, Menu, SegmentedControl, type SegmentedControlItem, useMantineColorScheme } from "@mantine/core" import { Box, Divider, Group, Menu, SegmentedControl, type SegmentedControlItem, useMantineColorScheme } from "@mantine/core"
import { showNotification } from "@mantine/notifications" import { showNotification } from "@mantine/notifications"
import { client } from "app/client" import { client } from "app/client"
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/slices/redirect" import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { type ViewMode } from "app/types" import { type ViewMode } from "app/types"
import { useViewMode } from "hooks/useViewMode" import { useViewMode } from "hooks/useViewMode"

View File

@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
import { Box, Button, Group, Stack } from "@mantine/core" import { Box, Button, Group, Stack } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { CodeEditor } from "components/code/CodeEditor" import { CodeEditor } from "components/code/CodeEditor"

View File

@@ -1,6 +1,8 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core" import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { useAppDispatch, useAppSelector } from "app/store"
import { type SharingSettings } from "app/types"
import { import {
changeAlwaysScrollToEntry, changeAlwaysScrollToEntry,
changeCustomContextMenu, changeCustomContextMenu,
@@ -10,9 +12,7 @@ import {
changeScrollSpeed, changeScrollSpeed,
changeSharingSetting, changeSharingSetting,
changeShowRead, changeShowRead,
} from "app/slices/user" } from "app/user/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { type SharingSettings } from "app/types"
import { locales } from "i18n" import { locales } from "i18n"
export function DisplaySettings() { export function DisplaySettings() {
@@ -82,7 +82,14 @@ export function DisplaySettings() {
key={site} key={site}
label={Constants.sharing[site].label} label={Constants.sharing[site].label}
checked={sharingSettings && sharingSettings[site]} checked={sharingSettings && sharingSettings[site]}
onChange={async e => await dispatch(changeSharingSetting({ site, value: e.currentTarget.checked }))} onChange={async e =>
await dispatch(
changeSharingSetting({
site,
value: e.currentTarget.checked,
})
)
}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>

View File

@@ -3,10 +3,10 @@ import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, St
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals" import { openConfirmModal } from "@mantine/modals"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToLogin, redirectToSelectedSource } from "app/slices/redirect" import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks"
import { reloadProfile } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { type ProfileModificationRequest } from "app/types" import { type ProfileModificationRequest } from "app/types"
import { reloadProfile } from "app/user/thunks"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useEffect } from "react" import { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"

View File

@@ -8,9 +8,9 @@ import {
redirectToFeedDetails, redirectToFeedDetails,
redirectToTag, redirectToTag,
redirectToTagDetails, redirectToTagDetails,
} from "app/slices/redirect" } from "app/redirect/thunks"
import { collapseTreeCategory } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { collapseTreeCategory } from "app/tree/thunks"
import { type Category, type Subscription } from "app/types" import { type Category, type Subscription } 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"
@@ -36,8 +36,11 @@ export function Tree() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const feedClicked = (e: React.MouseEvent, id: string) => { const feedClicked = (e: React.MouseEvent, id: string) => {
if (e.detail === 2) dispatch(redirectToFeedDetails(id)) if (e.detail === 2) {
else dispatch(redirectToFeed(id)) dispatch(redirectToFeedDetails(id))
} else {
dispatch(redirectToFeed(id))
}
} }
const categoryClicked = (e: React.MouseEvent, id: string) => { const categoryClicked = (e: React.MouseEvent, id: string) => {
if (e.detail === 2) { if (e.detail === 2) {
@@ -57,8 +60,11 @@ export function Tree() {
) )
} }
const tagClicked = (e: React.MouseEvent, id: string) => { const tagClicked = (e: React.MouseEvent, id: string) => {
if (e.detail === 2) dispatch(redirectToTagDetails(id)) if (e.detail === 2) {
else dispatch(redirectToTag(id)) dispatch(redirectToTagDetails(id))
} else {
dispatch(redirectToTag(id))
}
} }
const allCategoryNode = () => ( const allCategoryNode = () => (

View File

@@ -1,7 +1,7 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { Box, Center, Kbd, TextInput } from "@mantine/core" import { Box, Center, Kbd, TextInput } from "@mantine/core"
import { openSpotlight, type SpotlightAction, SpotlightProvider } from "@mantine/spotlight" import { openSpotlight, type SpotlightAction, SpotlightProvider } from "@mantine/spotlight"
import { redirectToFeed } from "app/slices/redirect" import { redirectToFeed } from "app/redirect/thunks"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { type Subscription } from "app/types" import { type Subscription } from "app/types"
import { FeedFavicon } from "components/content/FeedFavicon" import { FeedFavicon } from "components/content/FeedFavicon"

View File

@@ -1,6 +1,6 @@
import { setWebSocketConnected } from "app/slices/server" import { setWebSocketConnected } from "app/server/slice"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { reloadTree } from "app/tree/thunks"
import { useEffect } from "react" import { useEffect } from "react"
import WebsocketHeartbeatJs from "websocket-heartbeat-js" import WebsocketHeartbeatJs from "websocket-heartbeat-js"

View File

@@ -1,7 +1,7 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Anchor, Box, Center, Container, Divider, Group, Image, Title, useMantineColorScheme } from "@mantine/core" import { Anchor, Box, Center, Container, Divider, Group, Image, Title, useMantineColorScheme } from "@mantine/core"
import { client } from "app/client" import { client } from "app/client"
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/slices/redirect" import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import welcome_page_dark from "assets/welcome_page_dark.png" import welcome_page_dark from "assets/welcome_page_dark.png"
import welcome_page_light from "assets/welcome_page_light.png" import welcome_page_light from "assets/welcome_page_light.png"

View File

@@ -1,7 +1,7 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { Anchor, Box, Container, createStyles, List, NativeSelect, SimpleGrid, Title } from "@mantine/core" import { Anchor, Box, Container, createStyles, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToApiDocumentation } from "app/slices/redirect" import { redirectToApiDocumentation } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { CategorySelect } from "components/content/add/CategorySelect" import { CategorySelect } from "components/content/add/CategorySelect"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"

View File

@@ -4,9 +4,9 @@ import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals" import { openConfirmModal } from "@mantine/modals"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToRootCategory, redirectToSelectedSource } from "app/slices/redirect" import { redirectToRootCategory, redirectToSelectedSource } from "app/redirect/thunks"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { reloadTree } from "app/tree/thunks"
import { type CategoryModificationRequest } from "app/types" import { type CategoryModificationRequest } from "app/types"
import { flattenCategoryTree } from "app/utils" import { flattenCategoryTree } from "app/utils"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"

View File

@@ -3,9 +3,9 @@ import { Anchor, Box, Button, Code, Container, Divider, Group, Input, NumberInpu
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals" import { openConfirmModal } from "@mantine/modals"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToRootCategory, redirectToSelectedSource } from "app/slices/redirect" import { redirectToRootCategory, redirectToSelectedSource } from "app/redirect/thunks"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { reloadTree } from "app/tree/thunks"
import { type FeedModificationRequest } from "app/types" import { type FeedModificationRequest } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { CategorySelect } from "components/content/add/CategorySelect" import { CategorySelect } from "components/content/add/CategorySelect"

View File

@@ -2,8 +2,9 @@ import { Trans } from "@lingui/macro"
import { ActionIcon, Box, Center, createStyles, Divider, Group, Title, useMantineTheme } from "@mantine/core" import { ActionIcon, Box, Center, createStyles, Divider, Group, Title, useMantineTheme } from "@mantine/core"
import { useViewportSize } from "@mantine/hooks" import { useViewportSize } from "@mantine/hooks"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { type EntrySourceType, loadEntries } from "app/slices/entries" import { type EntrySourceType } from "app/entries/slice"
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "app/slices/redirect" import { loadEntries } from "app/entries/thunks"
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { flattenCategoryTree } from "app/utils" import { flattenCategoryTree } from "app/utils"
import { FeedEntries } from "components/content/FeedEntries" import { FeedEntries } from "components/content/FeedEntries"
@@ -47,9 +48,11 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const titleClicked = () => { const titleClicked = () => {
if (props.sourceType === "category") dispatch(redirectToCategoryDetails(id)) if (props.sourceType === "category") {
else if (props.sourceType === "feed") dispatch(redirectToFeedDetails(id)) dispatch(redirectToCategoryDetails(id))
else if (props.sourceType === "tag") dispatch(redirectToTagDetails(id)) } else if (props.sourceType === "feed") {
dispatch(redirectToFeedDetails(id))
} else if (props.sourceType === "tag") dispatch(redirectToTagDetails(id))
} }
useEffect(() => { useEffect(() => {

View File

@@ -14,10 +14,11 @@ import {
useMantineTheme, useMantineTheme,
} from "@mantine/core" } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect" import { redirectToAdd, redirectToRootCategory } from "app/redirect/thunks"
import { reloadTree, setMobileMenuOpen, setSidebarWidth } from "app/slices/tree"
import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { setMobileMenuOpen, setSidebarWidth } from "app/tree/slice"
import { reloadTree } from "app/tree/thunks"
import { reloadProfile, reloadSettings, reloadTags } from "app/user/thunks"
import { AnnouncementDialog } from "components/AnnouncementDialog" import { AnnouncementDialog } from "components/AnnouncementDialog"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { Logo } from "components/Logo" import { Logo } from "components/Logo"

View File

@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
import { Anchor, Box, Button, Container, Group, Input, Stack, Title } from "@mantine/core" import { Anchor, Box, Button, Container, Group, Input, Stack, Title } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"

View File

@@ -2,7 +2,7 @@ import { t, Trans } from "@lingui/macro"
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core" import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToRootCategory } from "app/slices/redirect" import { redirectToRootCategory } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { type LoginRequest } from "app/types" import { type LoginRequest } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"

View File

@@ -2,7 +2,7 @@ import { t, Trans } from "@lingui/macro"
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core" import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToRootCategory } from "app/slices/redirect" import { redirectToRootCategory } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { type RegistrationRequest } from "app/types" import { type RegistrationRequest } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"