mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
eslint update
This commit is contained in:
41
commafeed-client/.eslintrc.cjs
Normal file
41
commafeed-client/.eslintrc.cjs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true
|
||||||
|
},
|
||||||
|
extends: ["standard-with-typescript", "plugin:react/recommended", "plugin:react-hooks/recommended", "plugin:prettier/recommended"],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
files: [".eslintrc.{js,cjs}"],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "script"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module"
|
||||||
|
},
|
||||||
|
plugins: ["react"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/no-confusing-void-expression": ["error", { ignoreArrowShorthand: true }],
|
||||||
|
"@typescript-eslint/no-floating-promises": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": "off",
|
||||||
|
"@typescript-eslint/prefer-nullish-coalescing": ["error", { ignoreConditionalTests: true }],
|
||||||
|
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||||
|
"@typescript-eslint/unbound-method": "off",
|
||||||
|
"react/no-unescaped-entities": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
3664
commafeed-client/package-lock.json
generated
3664
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lingui/cli": "^4.1.2",
|
"@lingui/cli": "^4.1.2",
|
||||||
"@lingui/vite-plugin": "^4.1.2",
|
"@lingui/vite-plugin": "^4.1.2",
|
||||||
"@types/eslint": "^8.40.0",
|
|
||||||
"@types/mousetrap": "^1.6.11",
|
"@types/mousetrap": "^1.6.11",
|
||||||
"@types/react": "^18.2.6",
|
"@types/react": "^18.2.6",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
@@ -62,20 +61,21 @@
|
|||||||
"@types/swagger-ui-react": "^4.18.0",
|
"@types/swagger-ui-react": "^4.18.0",
|
||||||
"@types/throttle-debounce": "^5.0.0",
|
"@types/throttle-debounce": "^5.0.0",
|
||||||
"@types/tinycon": "^0.6.3",
|
"@types/tinycon": "^0.6.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
"@typescript-eslint/parser": "^5.59.7",
|
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-standard-with-typescript": "^43.0.0",
|
||||||
"eslint-plugin-hooks": "^0.4.3",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-n": "^16.5.0",
|
||||||
"eslint-plugin-prettier": "^5.1.2",
|
"eslint-plugin-prettier": "^5.1.2",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"rollup-plugin-visualizer": "^5.9.0",
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^4.3.9",
|
"vite": "^4.3.9",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-tsconfig-paths": "^4.2.0",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { i18n } from "@lingui/core"
|
import { i18n } from "@lingui/core"
|
||||||
import { I18nProvider } from "@lingui/react"
|
import { I18nProvider } from "@lingui/react"
|
||||||
import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core"
|
import { type ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core"
|
||||||
import { useColorScheme } from "@mantine/hooks"
|
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"
|
||||||
@@ -63,7 +63,7 @@ function Providers(props: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// swagger-ui is very large, load only on-demand
|
// swagger-ui is very large, load only on-demand
|
||||||
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
|
const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage"))
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
|
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import {
|
import {
|
||||||
AddCategoryRequest,
|
type AddCategoryRequest,
|
||||||
Category,
|
type AdminSaveUserRequest,
|
||||||
CategoryModificationRequest,
|
type Category,
|
||||||
CollapseRequest,
|
type CategoryModificationRequest,
|
||||||
Entries,
|
type CollapseRequest,
|
||||||
FeedInfo,
|
type Entries,
|
||||||
FeedInfoRequest,
|
type FeedInfo,
|
||||||
FeedModificationRequest,
|
type FeedInfoRequest,
|
||||||
GetEntriesPaginatedRequest,
|
type FeedModificationRequest,
|
||||||
IDRequest,
|
type GetEntriesPaginatedRequest,
|
||||||
LoginRequest,
|
type IDRequest,
|
||||||
MarkRequest,
|
type LoginRequest,
|
||||||
Metrics,
|
type MarkRequest,
|
||||||
MultipleMarkRequest,
|
type Metrics,
|
||||||
PasswordResetRequest,
|
type MultipleMarkRequest,
|
||||||
ProfileModificationRequest,
|
type PasswordResetRequest,
|
||||||
RegistrationRequest,
|
type ProfileModificationRequest,
|
||||||
ServerInfo,
|
type RegistrationRequest,
|
||||||
Settings,
|
type ServerInfo,
|
||||||
StarRequest,
|
type Settings,
|
||||||
SubscribeRequest,
|
type StarRequest,
|
||||||
Subscription,
|
type SubscribeRequest,
|
||||||
TagRequest,
|
type Subscription,
|
||||||
UserModel,
|
type TagRequest,
|
||||||
|
type UserModel,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
||||||
@@ -42,34 +43,34 @@ axiosInstance.interceptors.response.use(
|
|||||||
|
|
||||||
export const client = {
|
export const client = {
|
||||||
category: {
|
category: {
|
||||||
getRoot: () => axiosInstance.get<Category>("category/get"),
|
getRoot: async () => await axiosInstance.get<Category>("category/get"),
|
||||||
modify: (req: CategoryModificationRequest) => axiosInstance.post("category/modify", req),
|
modify: async (req: CategoryModificationRequest) => await axiosInstance.post("category/modify", req),
|
||||||
collapse: (req: CollapseRequest) => axiosInstance.post("category/collapse", req),
|
collapse: async (req: CollapseRequest) => await axiosInstance.post("category/collapse", req),
|
||||||
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("category/entries", { params: req }),
|
getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("category/entries", { params: req }),
|
||||||
markEntries: (req: MarkRequest) => axiosInstance.post("category/mark", req),
|
markEntries: async (req: MarkRequest) => await axiosInstance.post("category/mark", req),
|
||||||
add: (req: AddCategoryRequest) => axiosInstance.post("category/add", req),
|
add: async (req: AddCategoryRequest) => await axiosInstance.post("category/add", req),
|
||||||
delete: (req: IDRequest) => axiosInstance.post("category/delete", req),
|
delete: async (req: IDRequest) => await axiosInstance.post("category/delete", req),
|
||||||
},
|
},
|
||||||
entry: {
|
entry: {
|
||||||
mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req),
|
mark: async (req: MarkRequest) => await axiosInstance.post("entry/mark", req),
|
||||||
markMultiple: (req: MultipleMarkRequest) => axiosInstance.post("entry/markMultiple", req),
|
markMultiple: async (req: MultipleMarkRequest) => await axiosInstance.post("entry/markMultiple", req),
|
||||||
star: (req: StarRequest) => axiosInstance.post("entry/star", req),
|
star: async (req: StarRequest) => await axiosInstance.post("entry/star", req),
|
||||||
getTags: () => axiosInstance.get<string[]>("entry/tags"),
|
getTags: async () => await axiosInstance.get<string[]>("entry/tags"),
|
||||||
tag: (req: TagRequest) => axiosInstance.post("entry/tag", req),
|
tag: async (req: TagRequest) => await axiosInstance.post("entry/tag", req),
|
||||||
},
|
},
|
||||||
feed: {
|
feed: {
|
||||||
get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`),
|
get: async (id: string) => await axiosInstance.get<Subscription>(`feed/get/${id}`),
|
||||||
modify: (req: FeedModificationRequest) => axiosInstance.post("feed/modify", req),
|
modify: async (req: FeedModificationRequest) => await axiosInstance.post("feed/modify", req),
|
||||||
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("feed/entries", { params: req }),
|
getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("feed/entries", { params: req }),
|
||||||
markEntries: (req: MarkRequest) => axiosInstance.post("feed/mark", req),
|
markEntries: async (req: MarkRequest) => await axiosInstance.post("feed/mark", req),
|
||||||
fetchFeed: (req: FeedInfoRequest) => axiosInstance.post<FeedInfo>("feed/fetch", req),
|
fetchFeed: async (req: FeedInfoRequest) => await axiosInstance.post<FeedInfo>("feed/fetch", req),
|
||||||
refreshAll: () => axiosInstance.get("feed/refreshAll"),
|
refreshAll: async () => await axiosInstance.get("feed/refreshAll"),
|
||||||
subscribe: (req: SubscribeRequest) => axiosInstance.post<number>("feed/subscribe", req),
|
subscribe: async (req: SubscribeRequest) => await axiosInstance.post<number>("feed/subscribe", req),
|
||||||
unsubscribe: (req: IDRequest) => axiosInstance.post("feed/unsubscribe", req),
|
unsubscribe: async (req: IDRequest) => await axiosInstance.post("feed/unsubscribe", req),
|
||||||
importOpml: (req: File) => {
|
importOpml: async (req: File) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append("file", req)
|
formData.append("file", req)
|
||||||
return axiosInstance.post("feed/import", formData, {
|
return await axiosInstance.post("feed/import", formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
@@ -77,23 +78,23 @@ export const client = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
login: (req: LoginRequest) => axiosInstance.post("user/login", req),
|
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
|
||||||
register: (req: RegistrationRequest) => axiosInstance.post("user/register", req),
|
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
|
||||||
passwordReset: (req: PasswordResetRequest) => axiosInstance.post("user/passwordReset", req),
|
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
|
||||||
getSettings: () => axiosInstance.get<Settings>("user/settings"),
|
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
|
||||||
saveSettings: (settings: Settings) => axiosInstance.post("user/settings", settings),
|
saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings),
|
||||||
getProfile: () => axiosInstance.get<UserModel>("user/profile"),
|
getProfile: async () => await axiosInstance.get<UserModel>("user/profile"),
|
||||||
saveProfile: (req: ProfileModificationRequest) => axiosInstance.post("user/profile", req),
|
saveProfile: async (req: ProfileModificationRequest) => await axiosInstance.post("user/profile", req),
|
||||||
deleteProfile: () => axiosInstance.post("user/profile/deleteAccount"),
|
deleteProfile: async () => await axiosInstance.post("user/profile/deleteAccount"),
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
getServerInfos: () => axiosInstance.get<ServerInfo>("server/get"),
|
getServerInfos: async () => await axiosInstance.get<ServerInfo>("server/get"),
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
getAllUsers: () => axiosInstance.get<UserModel[]>("admin/user/getAll"),
|
getAllUsers: async () => await axiosInstance.get<UserModel[]>("admin/user/getAll"),
|
||||||
saveUser: (req: UserModel) => axiosInstance.post("admin/user/save", req),
|
saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req),
|
||||||
deleteUser: (req: IDRequest) => axiosInstance.post("admin/user/delete", req),
|
deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req),
|
||||||
getMetrics: () => axiosInstance.get<Metrics>("admin/metrics"),
|
getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ export const errorToStrings = (err: unknown) => {
|
|||||||
if (err.response) {
|
if (err.response) {
|
||||||
const { data } = err.response
|
const { data } = err.response
|
||||||
if (typeof data === "string") strings.push(data)
|
if (typeof data === "string") strings.push(data)
|
||||||
if (typeof data === "object" && data.message) strings.push(data.message)
|
if (typeof data === "object" && data.message) strings.push(data.message as string)
|
||||||
if (typeof data === "object" && data.errors) strings = [...strings, ...data.errors]
|
if (typeof data === "object" && data.errors) strings = [...strings, ...data.errors]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { t } from "@lingui/macro"
|
||||||
import { DEFAULT_THEME } from "@mantine/core"
|
import { DEFAULT_THEME } from "@mantine/core"
|
||||||
import { IconType } from "react-icons"
|
import { type IconType } from "react-icons"
|
||||||
import { FaAt } from "react-icons/fa"
|
import { FaAt } from "react-icons/fa"
|
||||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
|
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
|
||||||
import { Category, Entry, SharingSettings } from "./types"
|
import { type Category, type Entry, type SharingSettings } from "./types"
|
||||||
|
|
||||||
const categories: { [key: string]: Category } = {
|
const categories: Record<string, Category> = {
|
||||||
all: {
|
all: {
|
||||||
id: "all",
|
id: "all",
|
||||||
name: t`All`,
|
name: t`All`,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable import/first */
|
/* eslint-disable import/first */
|
||||||
import { configureStore } from "@reduxjs/toolkit"
|
import { configureStore } from "@reduxjs/toolkit"
|
||||||
import { client } from "app/client"
|
import { type client } from "app/client"
|
||||||
import { reducers } from "app/store"
|
import { reducers } from "app/store"
|
||||||
import { Entries, Entry } from "app/types"
|
import { type Entries, type Entry } from "app/types"
|
||||||
import { 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"
|
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "./entries"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
|
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { createAppAsyncThunk, RootState } from "app/store"
|
import { createAppAsyncThunk, type RootState } from "app/store"
|
||||||
import { Entry, MarkRequest, TagRequest } from "app/types"
|
import { type Entry, type MarkRequest, type TagRequest } from "app/types"
|
||||||
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
|
// eslint-disable-next-line import/no-cycle
|
||||||
@@ -11,7 +11,10 @@ import { reloadTree } from "./tree"
|
|||||||
import { reloadTags } from "./user"
|
import { reloadTags } from "./user"
|
||||||
|
|
||||||
export type EntrySourceType = "category" | "feed" | "tag"
|
export type EntrySourceType = "category" | "feed" | "tag"
|
||||||
export type EntrySource = { type: EntrySourceType; id: string }
|
export interface EntrySource {
|
||||||
|
type: EntrySourceType
|
||||||
|
id: string
|
||||||
|
}
|
||||||
export type ExpendableEntry = Entry & { expanded?: boolean }
|
export type ExpendableEntry = Entry & { expanded?: boolean }
|
||||||
|
|
||||||
interface EntriesState {
|
interface EntriesState {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
|
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { createAppAsyncThunk } from "app/store"
|
import { createAppAsyncThunk } from "app/store"
|
||||||
|
|
||||||
@@ -22,8 +22,9 @@ export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSo
|
|||||||
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
|
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
|
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
|
||||||
)
|
)
|
||||||
export const redirectToRootCategory = createAppAsyncThunk("redirect/category/root", (_, thunkApi) =>
|
export const redirectToRootCategory = createAppAsyncThunk(
|
||||||
thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
|
"redirect/category/root",
|
||||||
|
async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
|
||||||
)
|
)
|
||||||
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
|
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
|
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { createAppAsyncThunk } from "app/store"
|
import { createAppAsyncThunk } from "app/store"
|
||||||
import { ServerInfo } from "app/types"
|
import { type ServerInfo } from "app/types"
|
||||||
|
|
||||||
interface ServerState {
|
interface ServerState {
|
||||||
serverInfos?: ServerInfo
|
serverInfos?: ServerInfo
|
||||||
@@ -12,7 +12,7 @@ const initialState: ServerState = {
|
|||||||
webSocketConnected: false,
|
webSocketConnected: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reloadServerInfos = createAppAsyncThunk("server/infos", () => client.server.getServerInfos().then(r => r.data))
|
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,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
|
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { createAppAsyncThunk } from "app/store"
|
import { createAppAsyncThunk } from "app/store"
|
||||||
import { Category, CollapseRequest } from "app/types"
|
import { type Category, type CollapseRequest } from "app/types"
|
||||||
import { visitCategoryTree } from "app/utils"
|
import { visitCategoryTree } from "app/utils"
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { markEntry } from "./entries"
|
import { markEntry } from "./entries"
|
||||||
@@ -20,9 +20,10 @@ const initialState: TreeState = {
|
|||||||
sidebarVisible: true,
|
sidebarVisible: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reloadTree = createAppAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
|
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) =>
|
export const collapseTreeCategory = createAppAsyncThunk(
|
||||||
client.category.collapse(req)
|
"tree/category/collapse",
|
||||||
|
async (req: CollapseRequest) => await client.category.collapse(req)
|
||||||
)
|
)
|
||||||
|
|
||||||
export const treeSlice = createSlice({
|
export const treeSlice = createSlice({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { showNotification } from "@mantine/notifications"
|
|||||||
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { createAppAsyncThunk } from "app/store"
|
import { createAppAsyncThunk } from "app/store"
|
||||||
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel } from "app/types"
|
import { type ReadingMode, type ReadingOrder, type Settings, type SharingSettings, type UserModel } from "app/types"
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { reloadEntries } from "./entries"
|
import { reloadEntries } from "./entries"
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ interface UserState {
|
|||||||
|
|
||||||
const initialState: UserState = {}
|
const initialState: UserState = {}
|
||||||
|
|
||||||
export const reloadSettings = createAppAsyncThunk("settings/reload", () => client.user.getSettings().then(r => r.data))
|
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
|
||||||
export const reloadProfile = createAppAsyncThunk("profile/reload", () => client.user.getProfile().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", () => client.entry.getTags().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) => {
|
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { configureStore, createAsyncThunk } from "@reduxjs/toolkit"
|
import { configureStore, createAsyncThunk } from "@reduxjs/toolkit"
|
||||||
import { setupListeners } from "@reduxjs/toolkit/query"
|
import { setupListeners } from "@reduxjs/toolkit/query"
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||||
import entriesReducer from "./slices/entries"
|
import entriesReducer from "./slices/entries"
|
||||||
import redirectReducer from "./slices/redirect"
|
import redirectReducer from "./slices/redirect"
|
||||||
import serverReducer from "./slices/server"
|
import serverReducer from "./slices/server"
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export interface MetricMeter {
|
|||||||
units: string
|
units: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MetricTimer = {
|
export interface MetricTimer {
|
||||||
count: number
|
count: number
|
||||||
max: number
|
max: number
|
||||||
mean: number
|
mean: number
|
||||||
@@ -155,10 +155,10 @@ export type MetricTimer = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Metrics {
|
export interface Metrics {
|
||||||
counters: { [key: string]: MetricCounter }
|
counters: Record<string, MetricCounter>
|
||||||
gauges: { [key: string]: MetricGauge }
|
gauges: Record<string, MetricGauge>
|
||||||
meters: { [key: string]: MetricMeter }
|
meters: Record<string, MetricMeter>
|
||||||
timers: { [key: string]: MetricTimer }
|
timers: Record<string, MetricTimer>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultipleMarkRequest {
|
export interface MultipleMarkRequest {
|
||||||
@@ -273,6 +273,15 @@ export interface UserModel {
|
|||||||
admin: boolean
|
admin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminSaveUserRequest {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
email?: string
|
||||||
|
password?: string
|
||||||
|
enabled: boolean
|
||||||
|
admin: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type ReadingMode = "all" | "unread"
|
export type ReadingMode = "all" | "unread"
|
||||||
|
|
||||||
export type ReadingOrder = "asc" | "desc"
|
export type ReadingOrder = "asc" | "desc"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { throttle } from "throttle-debounce"
|
import { throttle } from "throttle-debounce"
|
||||||
import { Category } from "./types"
|
import { type Category } from "./types"
|
||||||
|
|
||||||
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
|
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
|
||||||
visitor(category)
|
visitor(category)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ActionIcon, Button, Tooltip, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Button, Tooltip, useMantineTheme } from "@mantine/core"
|
||||||
import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
|
import { type ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
|
||||||
import { ButtonProps } from "@mantine/core/lib/Button/Button"
|
import { type ButtonProps } from "@mantine/core/lib/Button/Button"
|
||||||
import { useActionButton } from "hooks/useActionButton"
|
import { useActionButton } from "hooks/useActionButton"
|
||||||
import { forwardRef, MouseEventHandler, ReactNode } from "react"
|
import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
|
||||||
|
|
||||||
interface ActionButtonProps {
|
interface ActionButtonProps {
|
||||||
className?: string
|
className?: string
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/macro"
|
||||||
import { Box, Alert as MantineAlert } from "@mantine/core"
|
import { Alert as MantineAlert, Box } from "@mantine/core"
|
||||||
import { Fragment } from "react"
|
import { Fragment } from "react"
|
||||||
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
|
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
|
||||||
|
|
||||||
type Level = "error" | "warning" | "success"
|
type Level = "error" | "warning" | "success"
|
||||||
|
|
||||||
export interface ErrorsAlertProps {
|
export interface ErrorsAlertProps {
|
||||||
level?: Level
|
level?: Level
|
||||||
messages: string[]
|
messages: string[]
|
||||||
@@ -31,8 +32,6 @@ export function Alert(props: ErrorsAlertProps) {
|
|||||||
color = "green"
|
color = "green"
|
||||||
icon = <TbCircleCheck />
|
icon = <TbCircleCheck />
|
||||||
break
|
break
|
||||||
default:
|
|
||||||
throw Error(`unsupported level: ${level}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ErrorPage } from "pages/ErrorPage"
|
import { ErrorPage } from "pages/ErrorPage"
|
||||||
import React, { ReactNode } from "react"
|
import React, { type ReactNode } from "react"
|
||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
|
|||||||
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
|
import { Box, Button, Checkbox, Group, PasswordInput, 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 { UserModel } from "app/types"
|
import { type AdminSaveUserRequest, type UserModel } 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"
|
||||||
import { TbDeviceFloppy } from "react-icons/tb"
|
import { TbDeviceFloppy } from "react-icons/tb"
|
||||||
@@ -14,8 +14,12 @@ interface UserEditProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UserEdit(props: UserEditProps) {
|
export function UserEdit(props: UserEditProps) {
|
||||||
const form = useForm<UserModel>({
|
const form = useForm<AdminSaveUserRequest>({
|
||||||
initialValues: props.user ?? ({ enabled: true } as UserModel),
|
initialValues: props.user ?? {
|
||||||
|
name: "",
|
||||||
|
enabled: true,
|
||||||
|
admin: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave })
|
const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave })
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Input, Textarea } from "@mantine/core"
|
import { Input, Textarea } from "@mantine/core"
|
||||||
import RichCodeEditor from "components/code/RichCodeEditor"
|
import RichCodeEditor from "components/code/RichCodeEditor"
|
||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
import { ReactNode } from "react"
|
import { type ReactNode } from "react"
|
||||||
|
|
||||||
interface CodeEditorProps {
|
interface CodeEditorProps {
|
||||||
description?: ReactNode
|
description?: ReactNode
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Constants } from "app/constants"
|
|||||||
import { calculatePlaceholderSize } from "app/utils"
|
import { calculatePlaceholderSize } from "app/utils"
|
||||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||||
import escapeStringRegexp from "escape-string-regexp"
|
import escapeStringRegexp from "escape-string-regexp"
|
||||||
import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave"
|
import { type ChildrenNode, Interweave, Matcher, type MatchResponse, type Node, type TransformCallback } from "interweave"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export interface ContentProps {
|
export interface ContentProps {
|
||||||
@@ -61,7 +61,7 @@ const transform: TransformCallback = node => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class HighlightMatcher extends Matcher {
|
class HighlightMatcher extends Matcher {
|
||||||
private search: string
|
private readonly search: string
|
||||||
|
|
||||||
constructor(search: string) {
|
constructor(search: string) {
|
||||||
super("highlight")
|
super("highlight")
|
||||||
@@ -73,7 +73,6 @@ class HighlightMatcher extends Matcher {
|
|||||||
return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
|
return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
|
|
||||||
replaceWith(children: ChildrenNode, props: unknown): Node {
|
replaceWith(children: ChildrenNode, props: unknown): Node {
|
||||||
return <Mark>{children}</Mark>
|
return <Mark>{children}</Mark>
|
||||||
}
|
}
|
||||||
@@ -97,5 +96,6 @@ const Content = React.memo((props: ContentProps) => {
|
|||||||
</TypographyStylesProvider>
|
</TypographyStylesProvider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
Content.displayName = "Content"
|
||||||
|
|
||||||
export { Content }
|
export { Content }
|
||||||
|
|||||||
@@ -9,13 +9,11 @@ export function Enclosure(props: { enclosureType: string; enclosureUrl: string }
|
|||||||
return (
|
return (
|
||||||
<TypographyStylesProvider>
|
<TypographyStylesProvider>
|
||||||
{hasVideo && (
|
{hasVideo && (
|
||||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
|
||||||
<video controls>
|
<video controls>
|
||||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||||
</video>
|
</video>
|
||||||
)}
|
)}
|
||||||
{hasAudio && (
|
{hasAudio && (
|
||||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
|
||||||
<audio controls>
|
<audio controls>
|
||||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||||
</audio>
|
</audio>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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 {
|
import {
|
||||||
ExpendableEntry,
|
type ExpendableEntry,
|
||||||
loadMoreEntries,
|
loadMoreEntries,
|
||||||
markAllEntries,
|
markAllEntries,
|
||||||
markEntry,
|
markEntry,
|
||||||
@@ -91,7 +91,7 @@ export function FeedEntries() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const swipedRight = (entry: ExpendableEntry) => dispatch(markEntry({ entry, read: !entry.read }))
|
const swipedRight = async (entry: ExpendableEntry) => await dispatch(markEntry({ entry, read: !entry.read }))
|
||||||
|
|
||||||
// close context menu on scroll
|
// close context menu on scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -128,42 +128,50 @@ export function FeedEntries() {
|
|||||||
return () => window.removeEventListener("scroll", listener)
|
return () => window.removeEventListener("scroll", listener)
|
||||||
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
|
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
|
||||||
|
|
||||||
useMousetrap("r", () => dispatch(reloadEntries()))
|
useMousetrap("r", async () => await dispatch(reloadEntries()))
|
||||||
useMousetrap("j", () =>
|
useMousetrap(
|
||||||
dispatch(
|
"j",
|
||||||
selectNextEntry({
|
async () =>
|
||||||
expand: true,
|
await dispatch(
|
||||||
markAsRead: true,
|
selectNextEntry({
|
||||||
scrollToEntry: true,
|
expand: true,
|
||||||
})
|
markAsRead: true,
|
||||||
)
|
scrollToEntry: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
useMousetrap("n", () =>
|
useMousetrap(
|
||||||
dispatch(
|
"n",
|
||||||
selectNextEntry({
|
async () =>
|
||||||
expand: false,
|
await dispatch(
|
||||||
markAsRead: false,
|
selectNextEntry({
|
||||||
scrollToEntry: true,
|
expand: false,
|
||||||
})
|
markAsRead: false,
|
||||||
)
|
scrollToEntry: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
useMousetrap("k", () =>
|
useMousetrap(
|
||||||
dispatch(
|
"k",
|
||||||
selectPreviousEntry({
|
async () =>
|
||||||
expand: true,
|
await dispatch(
|
||||||
markAsRead: true,
|
selectPreviousEntry({
|
||||||
scrollToEntry: true,
|
expand: true,
|
||||||
})
|
markAsRead: true,
|
||||||
)
|
scrollToEntry: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
useMousetrap("p", () =>
|
useMousetrap(
|
||||||
dispatch(
|
"p",
|
||||||
selectPreviousEntry({
|
async () =>
|
||||||
expand: false,
|
await dispatch(
|
||||||
markAsRead: false,
|
selectPreviousEntry({
|
||||||
scrollToEntry: true,
|
expand: false,
|
||||||
})
|
markAsRead: false,
|
||||||
)
|
scrollToEntry: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
useMousetrap("space", () => {
|
useMousetrap("space", () => {
|
||||||
if (selectedEntry) {
|
if (selectedEntry) {
|
||||||
@@ -276,7 +284,7 @@ export function FeedEntries() {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
useMousetrap("g a", () => dispatch(redirectToRootCategory()))
|
useMousetrap("g a", async () => await dispatch(redirectToRootCategory()))
|
||||||
useMousetrap("f", () => dispatch(toggleSidebar()))
|
useMousetrap("f", () => dispatch(toggleSidebar()))
|
||||||
useMousetrap("?", () =>
|
useMousetrap("?", () =>
|
||||||
openModal({
|
openModal({
|
||||||
@@ -291,7 +299,7 @@ export function FeedEntries() {
|
|||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
id="entries"
|
id="entries"
|
||||||
initialLoad={false}
|
initialLoad={false}
|
||||||
loadMore={() => !loading && dispatch(loadMoreEntries())}
|
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
loader={<Box key={0}>{loading && <Loader />}</Box>}
|
loader={<Box key={0}>{loading && <Loader />}</Box>}
|
||||||
>
|
>
|
||||||
@@ -311,7 +319,7 @@ export function FeedEntries() {
|
|||||||
onHeaderClick={event => headerClicked(entry, event)}
|
onHeaderClick={event => headerClicked(entry, event)}
|
||||||
onHeaderRightClick={event => headerRightClicked(entry, event)}
|
onHeaderRightClick={event => headerRightClicked(entry, event)}
|
||||||
onBodyClick={() => bodyClicked(entry)}
|
onBodyClick={() => bodyClicked(entry)}
|
||||||
onSwipedRight={() => swipedRight(entry)}
|
onSwipedRight={async () => await swipedRight(entry)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Box, createStyles, Divider, Paper } from "@mantine/core"
|
import { Box, createStyles, Divider, Paper } from "@mantine/core"
|
||||||
import { MantineNumberSize } from "@mantine/styles"
|
import { type MantineNumberSize } from "@mantine/styles"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { Entry, ViewMode } from "app/types"
|
import { type Entry, type ViewMode } from "app/types"
|
||||||
import { useViewMode } from "hooks/useViewMode"
|
import { useViewMode } from "hooks/useViewMode"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useSwipeable } from "react-swipeable"
|
import { useSwipeable } from "react-swipeable"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
import { Content } from "./Content"
|
import { Content } from "./Content"
|
||||||
import { Enclosure } from "./Enclosure"
|
import { Enclosure } from "./Enclosure"
|
||||||
import { Media } from "./Media"
|
import { Media } from "./Media"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box, createStyles, Text } from "@mantine/core"
|
import { Box, createStyles, Text } from "@mantine/core"
|
||||||
import { Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Constants } from "app/constants"
|
|||||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
|
import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
|
||||||
import { redirectToFeed } from "app/slices/redirect"
|
import { redirectToFeed } from "app/slices/redirect"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
import { truncate } from "app/utils"
|
import { truncate } from "app/utils"
|
||||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||||
import { Item, Menu, Separator } from "react-contexify"
|
import { Item, Menu, Separator } from "react-contexify"
|
||||||
@@ -60,19 +60,19 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<Item onClick={() => dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}>
|
<Item onClick={async () => await dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}>
|
||||||
<Group>
|
<Group>
|
||||||
{props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />}
|
{props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />}
|
||||||
{props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
|
{props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
|
||||||
</Group>
|
</Group>
|
||||||
</Item>
|
</Item>
|
||||||
<Item onClick={() => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
|
<Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
|
||||||
<Group>
|
<Group>
|
||||||
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
|
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
|
||||||
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||||
</Group>
|
</Group>
|
||||||
</Item>
|
</Item>
|
||||||
<Item onClick={() => dispatch(markEntriesUpToEntry(props.entry))}>
|
<Item onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}>
|
||||||
<Group>
|
<Group>
|
||||||
<TbArrowBarToDown size={iconSize} />
|
<TbArrowBarToDown size={iconSize} />
|
||||||
<Trans>Mark as read up to here</Trans>
|
<Trans>Mark as read up to here</Trans>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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/slices/entries"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
import { ActionButton } from "components/ActionButton"
|
import { ActionButton } from "components/ActionButton"
|
||||||
import { useActionButton } from "hooks/useActionButton"
|
import { useActionButton } from "hooks/useActionButton"
|
||||||
import { useMobile } from "hooks/useMobile"
|
import { useMobile } from "hooks/useMobile"
|
||||||
@@ -22,9 +22,9 @@ 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 = () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))
|
const readStatusButtonClicked = async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))
|
||||||
const onTagsChange = (values: string[]) =>
|
const onTagsChange = async (values: string[]) =>
|
||||||
dispatch(
|
await dispatch(
|
||||||
tagEntry({
|
tagEntry({
|
||||||
entryId: +props.entry.id,
|
entryId: +props.entry.id,
|
||||||
tags: values,
|
tags: values,
|
||||||
@@ -44,7 +44,7 @@ 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={() => dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}
|
onClick={async () => await dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showSharingButtons && (
|
{showSharingButtons && (
|
||||||
@@ -88,7 +88,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbArrowBarToDown size={18} />}
|
icon={<TbArrowBarToDown size={18} />}
|
||||||
label={<Trans>Mark as read up to here</Trans>}
|
label={<Trans>Mark as read up to here</Trans>}
|
||||||
onClick={() => dispatch(markEntriesUpToEntry(props.entry))}
|
onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box, createStyles, Space, Text } from "@mantine/core"
|
import { Box, createStyles, Space, Text } from "@mantine/core"
|
||||||
import { Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||||
import { FeedFavicon } from "./FeedFavicon"
|
import { FeedFavicon } from "./FeedFavicon"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Highlight } from "@mantine/core"
|
import { Highlight } from "@mantine/core"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
|
|
||||||
export interface FeedEntryTitleProps {
|
export interface FeedEntryTitleProps {
|
||||||
entry: Entry
|
entry: Entry
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ActionIcon, Box, createStyles, SimpleGrid } from "@mantine/core"
|
import { ActionIcon, Box, createStyles, SimpleGrid } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { SharingSettings } from "app/types"
|
import { type SharingSettings } from "app/types"
|
||||||
import { IconType } from "react-icons"
|
import { type IconType } from "react-icons"
|
||||||
|
|
||||||
type Color = `#${string}`
|
type Color = `#${string}`
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { client, errorToStrings } from "app/client"
|
|||||||
import { redirectToSelectedSource } from "app/slices/redirect"
|
import { redirectToSelectedSource } from "app/slices/redirect"
|
||||||
import { reloadTree } from "app/slices/tree"
|
import { reloadTree } from "app/slices/tree"
|
||||||
import { useAppDispatch } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { 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"
|
||||||
import { TbFolderPlus } from "react-icons/tb"
|
import { TbFolderPlus } from "react-icons/tb"
|
||||||
@@ -36,7 +36,7 @@ export function AddCategory() {
|
|||||||
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
|
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
|
||||||
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
|
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
|
||||||
<Group position="center">
|
<Group position="center">
|
||||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" leftIcon={<TbFolderPlus size={16} />} loading={addCategory.loading}>
|
<Button type="submit" leftIcon={<TbFolderPlus size={16} />} loading={addCategory.loading}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { t } from "@lingui/macro"
|
||||||
import { Select, SelectItem, SelectProps } from "@mantine/core"
|
import { Select, type SelectItem, type SelectProps } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { flattenCategoryTree } from "app/utils"
|
import { flattenCategoryTree } from "app/utils"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function ImportOpml() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(v => importOpml.execute(v.file))}>
|
<form onSubmit={form.onSubmit(async v => await importOpml.execute(v.file))}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FileInput
|
<FileInput
|
||||||
label={<Trans>OPML file</Trans>}
|
label={<Trans>OPML file</Trans>}
|
||||||
@@ -49,7 +49,7 @@ export function ImportOpml() {
|
|||||||
accept="application/xml"
|
accept="application/xml"
|
||||||
/>
|
/>
|
||||||
<Group position="center">
|
<Group position="center">
|
||||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" leftIcon={<TbFileImport size={16} />} loading={importOpml.loading}>
|
<Button type="submit" leftIcon={<TbFileImport size={16} />} loading={importOpml.loading}>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Constants } from "app/constants"
|
|||||||
import { redirectToFeed, redirectToSelectedSource } from "app/slices/redirect"
|
import { redirectToFeed, redirectToSelectedSource } from "app/slices/redirect"
|
||||||
import { reloadTree } from "app/slices/tree"
|
import { reloadTree } from "app/slices/tree"
|
||||||
import { useAppDispatch } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { FeedInfoRequest, 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"
|
||||||
import { useAsyncCallback } from "react-async-hook"
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ export function Header() {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbArrowUp size={iconSize} />}
|
icon={<TbArrowUp size={iconSize} />}
|
||||||
label={<Trans>Previous</Trans>}
|
label={<Trans>Previous</Trans>}
|
||||||
onClick={() =>
|
onClick={async () =>
|
||||||
dispatch(
|
await dispatch(
|
||||||
selectPreviousEntry({
|
selectPreviousEntry({
|
||||||
expand: true,
|
expand: true,
|
||||||
markAsRead: true,
|
markAsRead: true,
|
||||||
@@ -92,8 +92,8 @@ export function Header() {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbArrowDown size={iconSize} />}
|
icon={<TbArrowDown size={iconSize} />}
|
||||||
label={<Trans>Next</Trans>}
|
label={<Trans>Next</Trans>}
|
||||||
onClick={() =>
|
onClick={async () =>
|
||||||
dispatch(
|
await dispatch(
|
||||||
selectNextEntry({
|
selectNextEntry({
|
||||||
expand: true,
|
expand: true,
|
||||||
markAsRead: true,
|
markAsRead: true,
|
||||||
@@ -108,7 +108,7 @@ export function Header() {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<TbRefresh size={iconSize} />}
|
icon={<TbRefresh size={iconSize} />}
|
||||||
label={<Trans>Refresh</Trans>}
|
label={<Trans>Refresh</Trans>}
|
||||||
onClick={() => dispatch(reloadEntries())}
|
onClick={async () => await dispatch(reloadEntries())}
|
||||||
/>
|
/>
|
||||||
<MarkAllAsReadButton iconSize={iconSize} />
|
<MarkAllAsReadButton iconSize={iconSize} />
|
||||||
|
|
||||||
@@ -117,12 +117,12 @@ export function Header() {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
|
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
|
||||||
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>}
|
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>}
|
||||||
onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
|
onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
|
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
|
||||||
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
|
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
|
||||||
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Popover>
|
<Popover>
|
||||||
@@ -132,13 +132,13 @@ export function Header() {
|
|||||||
</Indicator>
|
</Indicator>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<form onSubmit={searchForm.onSubmit(values => dispatch(search(values.search)))}>
|
<form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t`Search`}
|
placeholder={t`Search`}
|
||||||
{...searchForm.getInputProps("search")}
|
{...searchForm.getInputProps("search")}
|
||||||
icon={<TbSearch size={iconSize} />}
|
icon={<TbSearch size={iconSize} />}
|
||||||
rightSection={
|
rightSection={
|
||||||
<ActionIcon onClick={() => searchFromStore && dispatch(search(""))}>
|
<ActionIcon onClick={async () => await (searchFromStore && dispatch(search("")))}>
|
||||||
<TbX />
|
<TbX />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/macro"
|
||||||
import { Box, Divider, Group, Menu, SegmentedControl, 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/slices/redirect"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { ViewMode } from "app/types"
|
import { type ViewMode } from "app/types"
|
||||||
import { useViewMode } from "hooks/useViewMode"
|
import { useViewMode } from "hooks/useViewMode"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
import {
|
||||||
@@ -109,8 +109,8 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<TbWorldDownload size={iconSize} />}
|
icon={<TbWorldDownload size={iconSize} />}
|
||||||
onClick={() =>
|
onClick={async () =>
|
||||||
client.feed.refreshAll().then(() => {
|
await client.feed.refreshAll().then(() => {
|
||||||
showNotification({
|
showNotification({
|
||||||
message: <Trans>Your feeds have been queued for refresh.</Trans>,
|
message: <Trans>Your feeds have been queued for refresh.</Trans>,
|
||||||
color: "green",
|
color: "green",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MetricGauge } from "app/types"
|
import { type MetricGauge } from "app/types"
|
||||||
|
|
||||||
interface MeterProps {
|
interface MeterProps {
|
||||||
gauge: MetricGauge
|
gauge: MetricGauge
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { MetricMeter } from "app/types"
|
import { type MetricMeter } from "app/types"
|
||||||
|
|
||||||
interface MeterProps {
|
interface MeterProps {
|
||||||
meter: MetricMeter
|
meter: MetricMeter
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box } from "@mantine/core"
|
import { Box } from "@mantine/core"
|
||||||
import { MetricTimer } from "app/types"
|
import { type MetricTimer } from "app/types"
|
||||||
|
|
||||||
interface MetricTimerProps {
|
interface MetricTimerProps {
|
||||||
timer: MetricTimer
|
timer: MetricTimer
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function CustomCodeSettings() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
|
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
changeShowRead,
|
changeShowRead,
|
||||||
} from "app/slices/user"
|
} from "app/slices/user"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { SharingSettings } from "app/types"
|
import { type SharingSettings } from "app/types"
|
||||||
import { locales } from "i18n"
|
import { locales } from "i18n"
|
||||||
|
|
||||||
export function DisplaySettings() {
|
export function DisplaySettings() {
|
||||||
@@ -35,43 +35,43 @@ export function DisplaySettings() {
|
|||||||
value: l.key,
|
value: l.key,
|
||||||
label: l.label,
|
label: l.label,
|
||||||
}))}
|
}))}
|
||||||
onChange={s => s && dispatch(changeLanguage(s))}
|
onChange={async s => await (s && dispatch(changeLanguage(s)))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
|
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
|
||||||
checked={scrollSpeed ? scrollSpeed > 0 : false}
|
checked={scrollSpeed ? scrollSpeed > 0 : false}
|
||||||
onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))}
|
onChange={async e => await dispatch(changeScrollSpeed(e.currentTarget.checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
|
label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
|
||||||
checked={alwaysScrollToEntry}
|
checked={alwaysScrollToEntry}
|
||||||
onChange={e => dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
|
onChange={async e => await dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={<Trans>Show feeds and categories with no unread entries</Trans>}
|
label={<Trans>Show feeds and categories with no unread entries</Trans>}
|
||||||
checked={showRead}
|
checked={showRead}
|
||||||
onChange={e => dispatch(changeShowRead(e.currentTarget.checked))}
|
onChange={async e => await dispatch(changeShowRead(e.currentTarget.checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
|
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
|
||||||
checked={scrollMarks}
|
checked={scrollMarks}
|
||||||
onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))}
|
onChange={async e => await dispatch(changeScrollMarks(e.currentTarget.checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={<Trans>Show confirmation when marking all entries as read</Trans>}
|
label={<Trans>Show confirmation when marking all entries as read</Trans>}
|
||||||
checked={markAllAsReadConfirmation}
|
checked={markAllAsReadConfirmation}
|
||||||
onChange={e => dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
|
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
|
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
|
||||||
checked={customContextMenu}
|
checked={customContextMenu}
|
||||||
onChange={e => dispatch(changeCustomContextMenu(e.currentTarget.checked))}
|
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
|
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
|
||||||
@@ -82,7 +82,7 @@ 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={e => dispatch(changeSharingSetting({ site, value: e.currentTarget.checked }))}
|
onChange={async e => await dispatch(changeSharingSetting({ site, value: e.currentTarget.checked }))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { client, errorToStrings } from "app/client"
|
|||||||
import { redirectToLogin, redirectToSelectedSource } from "app/slices/redirect"
|
import { redirectToLogin, redirectToSelectedSource } from "app/slices/redirect"
|
||||||
import { reloadProfile } from "app/slices/user"
|
import { reloadProfile } from "app/slices/user"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { ProfileModificationRequest } from "app/types"
|
import { type ProfileModificationRequest } from "app/types"
|
||||||
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"
|
||||||
@@ -49,7 +49,7 @@ export function ProfileSettings() {
|
|||||||
),
|
),
|
||||||
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
||||||
confirmProps: { color: "red" },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => deleteProfile.execute(),
|
onConfirm: async () => await deleteProfile.execute(),
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -129,7 +129,7 @@ export function ProfileSettings() {
|
|||||||
<Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} />
|
<Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} />
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}>
|
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "app/slices/redirect"
|
} from "app/slices/redirect"
|
||||||
import { collapseTreeCategory } from "app/slices/tree"
|
import { collapseTreeCategory } from "app/slices/tree"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { Category, 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"
|
||||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, Center, createStyles } from "@mantine/core"
|
import { Box, Center, createStyles } from "@mantine/core"
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||||
import React, { ReactNode } from "react"
|
import React, { type ReactNode } from "react"
|
||||||
import { UnreadCount } from "./UnreadCount"
|
import { UnreadCount } from "./UnreadCount"
|
||||||
|
|
||||||
interface TreeNodeProps {
|
interface TreeNodeProps {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
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, SpotlightAction, SpotlightProvider } from "@mantine/spotlight"
|
import { openSpotlight, type SpotlightAction, SpotlightProvider } from "@mantine/spotlight"
|
||||||
import { redirectToFeed } from "app/slices/redirect"
|
import { redirectToFeed } from "app/slices/redirect"
|
||||||
import { useAppDispatch } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { Subscription } from "app/types"
|
import { type Subscription } from "app/types"
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||||
import { useMousetrap } from "hooks/useMousetrap"
|
import { useMousetrap } from "hooks/useMousetrap"
|
||||||
import { TbSearch } from "react-icons/tb"
|
import { TbSearch } from "react-icons/tb"
|
||||||
@@ -20,7 +20,7 @@ export function TreeSearch(props: TreeSearchProps) {
|
|||||||
.map(f => ({
|
.map(f => ({
|
||||||
title: f.name,
|
title: f.name,
|
||||||
icon: <FeedFavicon url={f.iconUrl} />,
|
icon: <FeedFavicon url={f.iconUrl} />,
|
||||||
onTrigger: () => dispatch(redirectToFeed(f.id)),
|
onTrigger: async () => await dispatch(redirectToFeed(f.id)),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const searchIcon = <TbSearch size={18} />
|
const searchIcon = <TbSearch size={18} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import mousetrap, { ExtendedKeyboardEvent } from "mousetrap"
|
import mousetrap, { type ExtendedKeyboardEvent } from "mousetrap"
|
||||||
import { useEffect, useRef } from "react"
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
type Callback = (e: ExtendedKeyboardEvent, combo: string) => void
|
type Callback = (e: ExtendedKeyboardEvent, combo: string) => void
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ViewMode } from "app/types"
|
import { type ViewMode } from "app/types"
|
||||||
import useLocalStorage from "use-local-storage"
|
import useLocalStorage from "use-local-storage"
|
||||||
|
|
||||||
export function useViewMode() {
|
export function useViewMode() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { i18n } from "@lingui/core"
|
import { i18n, type Messages } from "@lingui/core"
|
||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
@@ -12,40 +12,40 @@ interface Locale {
|
|||||||
// add an object to the array to add a new locale
|
// add an object to the array to add a new locale
|
||||||
// don't forget to also add it to the 'locales' array in .linguirc
|
// don't forget to also add it to the 'locales' array in .linguirc
|
||||||
export const locales: Locale[] = [
|
export const locales: Locale[] = [
|
||||||
{ key: "ar", label: "العربية", daysjsImportFn: () => import("dayjs/locale/ar") },
|
{ key: "ar", label: "العربية", daysjsImportFn: async () => await import("dayjs/locale/ar") },
|
||||||
{ key: "ca", label: "Català", daysjsImportFn: () => import("dayjs/locale/ca") },
|
{ key: "ca", label: "Català", daysjsImportFn: async () => await import("dayjs/locale/ca") },
|
||||||
{ key: "cs", label: "Čeština", daysjsImportFn: () => import("dayjs/locale/cs") },
|
{ key: "cs", label: "Čeština", daysjsImportFn: async () => await import("dayjs/locale/cs") },
|
||||||
{ key: "cy", label: "Cymraeg", daysjsImportFn: () => import("dayjs/locale/cy") },
|
{ key: "cy", label: "Cymraeg", daysjsImportFn: async () => await import("dayjs/locale/cy") },
|
||||||
{ key: "da", label: "Danish", daysjsImportFn: () => import("dayjs/locale/da") },
|
{ key: "da", label: "Danish", daysjsImportFn: async () => await import("dayjs/locale/da") },
|
||||||
{ key: "de", label: "Deutsch", daysjsImportFn: () => import("dayjs/locale/de") },
|
{ key: "de", label: "Deutsch", daysjsImportFn: async () => await import("dayjs/locale/de") },
|
||||||
{ key: "en", label: "English", daysjsImportFn: () => import("dayjs/locale/en") },
|
{ key: "en", label: "English", daysjsImportFn: async () => await import("dayjs/locale/en") },
|
||||||
{ key: "es", label: "Español", daysjsImportFn: () => import("dayjs/locale/es") },
|
{ key: "es", label: "Español", daysjsImportFn: async () => await import("dayjs/locale/es") },
|
||||||
{ key: "fa", label: "فارسی", daysjsImportFn: () => import("dayjs/locale/fa") },
|
{ key: "fa", label: "فارسی", daysjsImportFn: async () => await import("dayjs/locale/fa") },
|
||||||
{ key: "fi", label: "Suomi", daysjsImportFn: () => import("dayjs/locale/fi") },
|
{ key: "fi", label: "Suomi", daysjsImportFn: async () => await import("dayjs/locale/fi") },
|
||||||
{ key: "fr", label: "Français", daysjsImportFn: () => import("dayjs/locale/fr") },
|
{ key: "fr", label: "Français", daysjsImportFn: async () => await import("dayjs/locale/fr") },
|
||||||
{ key: "gl", label: "Galician", daysjsImportFn: () => import("dayjs/locale/gl") },
|
{ key: "gl", label: "Galician", daysjsImportFn: async () => await import("dayjs/locale/gl") },
|
||||||
{ key: "hu", label: "Magyar", daysjsImportFn: () => import("dayjs/locale/hu") },
|
{ key: "hu", label: "Magyar", daysjsImportFn: async () => await import("dayjs/locale/hu") },
|
||||||
{ key: "id", label: "Indonesian", daysjsImportFn: () => import("dayjs/locale/id") },
|
{ key: "id", label: "Indonesian", daysjsImportFn: async () => await import("dayjs/locale/id") },
|
||||||
{ key: "it", label: "Italiano", daysjsImportFn: () => import("dayjs/locale/it") },
|
{ key: "it", label: "Italiano", daysjsImportFn: async () => await import("dayjs/locale/it") },
|
||||||
{ key: "ja", label: "日本語", daysjsImportFn: () => import("dayjs/locale/ja") },
|
{ key: "ja", label: "日本語", daysjsImportFn: async () => await import("dayjs/locale/ja") },
|
||||||
{ key: "ko", label: "한국어", daysjsImportFn: () => import("dayjs/locale/ko") },
|
{ key: "ko", label: "한국어", daysjsImportFn: async () => await import("dayjs/locale/ko") },
|
||||||
{ key: "ms", label: "Bahasa Malaysian", daysjsImportFn: () => import("dayjs/locale/ms") },
|
{ key: "ms", label: "Bahasa Malaysian", daysjsImportFn: async () => await import("dayjs/locale/ms") },
|
||||||
{ key: "nb", label: "Norsk (bokmål)", daysjsImportFn: () => import("dayjs/locale/nb") },
|
{ key: "nb", label: "Norsk (bokmål)", daysjsImportFn: async () => await import("dayjs/locale/nb") },
|
||||||
{ key: "nl", label: "Nederlands", daysjsImportFn: () => import("dayjs/locale/nl") },
|
{ key: "nl", label: "Nederlands", daysjsImportFn: async () => await import("dayjs/locale/nl") },
|
||||||
{ key: "nn", label: "Norsk (nynorsk)", daysjsImportFn: () => import("dayjs/locale/nn") },
|
{ key: "nn", label: "Norsk (nynorsk)", daysjsImportFn: async () => await import("dayjs/locale/nn") },
|
||||||
{ key: "pl", label: "Polski", daysjsImportFn: () => import("dayjs/locale/pl") },
|
{ key: "pl", label: "Polski", daysjsImportFn: async () => await import("dayjs/locale/pl") },
|
||||||
{ key: "pt", label: "Português", daysjsImportFn: () => import("dayjs/locale/pt") },
|
{ key: "pt", label: "Português", daysjsImportFn: async () => await import("dayjs/locale/pt") },
|
||||||
{ key: "ru", label: "Русский", daysjsImportFn: () => import("dayjs/locale/ru") },
|
{ key: "ru", label: "Русский", daysjsImportFn: async () => await import("dayjs/locale/ru") },
|
||||||
{ key: "sk", label: "Slovenčina", daysjsImportFn: () => import("dayjs/locale/sk") },
|
{ key: "sk", label: "Slovenčina", daysjsImportFn: async () => await import("dayjs/locale/sk") },
|
||||||
{ key: "sv", label: "Svenska", daysjsImportFn: () => import("dayjs/locale/sv") },
|
{ key: "sv", label: "Svenska", daysjsImportFn: async () => await import("dayjs/locale/sv") },
|
||||||
{ key: "tr", label: "Türkçe", daysjsImportFn: () => import("dayjs/locale/tr") },
|
{ key: "tr", label: "Türkçe", daysjsImportFn: async () => await import("dayjs/locale/tr") },
|
||||||
{ key: "zh", label: "简体中文", daysjsImportFn: () => import("dayjs/locale/zh") },
|
{ key: "zh", label: "简体中文", daysjsImportFn: async () => await import("dayjs/locale/zh") },
|
||||||
]
|
]
|
||||||
|
|
||||||
function activateLocale(locale: string) {
|
function activateLocale(locale: string) {
|
||||||
// lingui
|
// lingui
|
||||||
import(`./locales/${locale}/messages.po`).then(data => {
|
import(`./locales/${locale}/messages.po`).then(data => {
|
||||||
i18n.load(locale, data.messages)
|
i18n.load(locale, data.messages as Messages)
|
||||||
i18n.activate(locale)
|
i18n.activate(locale)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import { Provider } from "react-redux"
|
|||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
const root = document.getElementById("root")
|
||||||
<Provider store={store}>
|
root &&
|
||||||
<App />
|
ReactDOM.createRoot(root).render(
|
||||||
</Provider>
|
<Provider store={store}>
|
||||||
)
|
<App />
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function WelcomePage() {
|
|||||||
label={<Trans>Try the demo!</Trans>}
|
label={<Trans>Try the demo!</Trans>}
|
||||||
icon={<TbClock size={iconSize} />}
|
icon={<TbClock size={iconSize} />}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => login.execute({ name: "demo", password: "demo" })}
|
onClick={async () => await login.execute({ name: "demo", password: "demo" })}
|
||||||
showLabelOnMobile
|
showLabelOnMobile
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
@@ -93,7 +93,7 @@ function Buttons() {
|
|||||||
label={<Trans>Log in</Trans>}
|
label={<Trans>Log in</Trans>}
|
||||||
icon={<TbKey size={iconSize} />}
|
icon={<TbKey size={iconSize} />}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => dispatch(redirectToLogin())}
|
onClick={async () => await dispatch(redirectToLogin())}
|
||||||
showLabelOnMobile
|
showLabelOnMobile
|
||||||
/>
|
/>
|
||||||
{serverInfos?.allowRegistrations && (
|
{serverInfos?.allowRegistrations && (
|
||||||
@@ -101,7 +101,7 @@ function Buttons() {
|
|||||||
label={<Trans>Sign up</Trans>}
|
label={<Trans>Sign up</Trans>}
|
||||||
icon={<TbUserPlus size={iconSize} />}
|
icon={<TbUserPlus size={iconSize} />}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onClick={() => dispatch(redirectToRegistration())}
|
onClick={async () => await dispatch(redirectToRegistration())}
|
||||||
showLabelOnMobile
|
showLabelOnMobile
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -139,7 +139,7 @@ function Footer() {
|
|||||||
</Anchor>
|
</Anchor>
|
||||||
</Group>
|
</Group>
|
||||||
<Box>
|
<Box>
|
||||||
<Anchor variant="text" onClick={() => dispatch(redirectToApiDocumentation())}>
|
<Anchor variant="text" onClick={async () => await dispatch(redirectToApiDocumentation())}>
|
||||||
API documentation
|
API documentation
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { Trans } from "@lingui/macro"
|
|||||||
import { ActionIcon, Box, Code, Container, Group, Table, Text, Title, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Box, Code, Container, Group, Table, Text, Title, useMantineTheme } from "@mantine/core"
|
||||||
import { closeAllModals, openConfirmModal, openModal } from "@mantine/modals"
|
import { closeAllModals, openConfirmModal, openModal } from "@mantine/modals"
|
||||||
import { client, errorToStrings } from "app/client"
|
import { client, errorToStrings } from "app/client"
|
||||||
import { UserModel } from "app/types"
|
import { type UserModel } from "app/types"
|
||||||
import { UserEdit } from "components/admin/UserEdit"
|
import { UserEdit } from "components/admin/UserEdit"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
import { ReactNode } from "react"
|
import { type ReactNode } from "react"
|
||||||
import { useAsync, useAsyncCallback } from "react-async-hook"
|
import { useAsync, useAsyncCallback } from "react-async-hook"
|
||||||
import { TbCheck, TbPencil, TbPlus, TbTrash, TbX } from "react-icons/tb"
|
import { TbCheck, TbPencil, TbPlus, TbTrash, TbX } from "react-icons/tb"
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ function BooleanIcon({ value }: { value: boolean }) {
|
|||||||
|
|
||||||
export function AdminUsersPage() {
|
export function AdminUsersPage() {
|
||||||
const theme = useMantineTheme()
|
const theme = useMantineTheme()
|
||||||
const query = useAsync(() => client.admin.getAllUsers(), [])
|
const query = useAsync(async () => await client.admin.getAllUsers(), [])
|
||||||
const users = query.result?.data.sort((a, b) => a.id - b.id)
|
const users = query.result?.data.sort((a, b) => a.id - b.id)
|
||||||
|
|
||||||
const deleteUser = useAsyncCallback(client.admin.deleteUser, {
|
const deleteUser = useAsyncCallback(client.admin.deleteUser, {
|
||||||
@@ -56,7 +56,7 @@ export function AdminUsersPage() {
|
|||||||
),
|
),
|
||||||
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
||||||
confirmProps: { color: "red" },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => deleteUser.execute({ id: user.id }),
|
onConfirm: async () => await deleteUser.execute({ id: user.id }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Timer } from "components/metrics/Timer"
|
|||||||
import { useAsync } from "react-async-hook"
|
import { useAsync } from "react-async-hook"
|
||||||
import { TbChartAreaLine, TbClock } from "react-icons/tb"
|
import { TbChartAreaLine, TbClock } from "react-icons/tb"
|
||||||
|
|
||||||
const shownMeters: { [key: string]: string } = {
|
const shownMeters: Record<string, string> = {
|
||||||
"com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate",
|
"com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate",
|
||||||
"com.commafeed.backend.feed.FeedRefreshWorker.feedFetched": "Feed fetching rate",
|
"com.commafeed.backend.feed.FeedRefreshWorker.feedFetched": "Feed fetching rate",
|
||||||
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
|
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
|
||||||
@@ -17,7 +17,7 @@ const shownMeters: { [key: string]: string } = {
|
|||||||
"com.commafeed.backend.service.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
"com.commafeed.backend.service.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
||||||
}
|
}
|
||||||
|
|
||||||
const shownGauges: { [key: string]: string } = {
|
const shownGauges: Record<string, string> = {
|
||||||
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
|
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
|
||||||
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active",
|
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active",
|
||||||
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active",
|
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active",
|
||||||
@@ -26,7 +26,7 @@ const shownGauges: { [key: string]: string } = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MetricsPage() {
|
export function MetricsPage() {
|
||||||
const query = useAsync(() => client.admin.getMetrics(), [])
|
const query = useAsync(async () => await client.admin.getMetrics(), [])
|
||||||
|
|
||||||
if (!query.result) return <Loader />
|
if (!query.result) return <Loader />
|
||||||
const { meters, gauges, timers } = query.result.data
|
const { meters, gauges, timers } = query.result.data
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function AboutPage() {
|
|||||||
<KeyboardShortcutsHelp />
|
<KeyboardShortcutsHelp />
|
||||||
</Section>
|
</Section>
|
||||||
<Section title={<Trans>REST API</Trans>} icon={<TbRocket size={24} />}>
|
<Section title={<Trans>REST API</Trans>} icon={<TbRocket size={24} />}>
|
||||||
<Anchor onClick={() => dispatch(redirectToApiDocumentation())}>
|
<Anchor onClick={async () => await dispatch(redirectToApiDocumentation())}>
|
||||||
<Trans>Go to the API documentation.</Trans>
|
<Trans>Go to the API documentation.</Trans>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Constants } from "app/constants"
|
|||||||
import { redirectToRootCategory, redirectToSelectedSource } from "app/slices/redirect"
|
import { redirectToRootCategory, redirectToSelectedSource } from "app/slices/redirect"
|
||||||
import { reloadTree } from "app/slices/tree"
|
import { reloadTree } from "app/slices/tree"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { 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"
|
||||||
import { CategorySelect } from "components/content/add/CategorySelect"
|
import { CategorySelect } from "components/content/add/CategorySelect"
|
||||||
@@ -23,7 +23,7 @@ export function CategoryDetailsPage() {
|
|||||||
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
|
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const query = useAsync(() => client.category.getRoot(), [])
|
const query = useAsync(async () => await client.category.getRoot(), [])
|
||||||
const category =
|
const category =
|
||||||
id === Constants.categories.starred.id
|
id === Constants.categories.starred.id
|
||||||
? Constants.categories.starred
|
? Constants.categories.starred
|
||||||
@@ -47,7 +47,7 @@ export function CategoryDetailsPage() {
|
|||||||
|
|
||||||
const openDeleteCategoryModal = () => {
|
const openDeleteCategoryModal = () => {
|
||||||
const categoryName = category?.name
|
const categoryName = category?.name
|
||||||
return openConfirmModal({
|
openConfirmModal({
|
||||||
title: <Trans>Delete Category</Trans>,
|
title: <Trans>Delete Category</Trans>,
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
@@ -58,7 +58,7 @@ export function CategoryDetailsPage() {
|
|||||||
),
|
),
|
||||||
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
||||||
confirmProps: { color: "red" },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => deleteCategory.execute({ id: +id }),
|
onConfirm: async () => await deleteCategory.execute({ id: +id }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ export function CategoryDetailsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
{editable && (
|
{editable && (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { client, errorToStrings } from "app/client"
|
|||||||
import { redirectToRootCategory, redirectToSelectedSource } from "app/slices/redirect"
|
import { redirectToRootCategory, redirectToSelectedSource } from "app/slices/redirect"
|
||||||
import { reloadTree } from "app/slices/tree"
|
import { reloadTree } from "app/slices/tree"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { 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"
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader"
|
||||||
@@ -54,7 +54,7 @@ export function FeedDetailsPage() {
|
|||||||
|
|
||||||
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
|
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const query = useAsync(() => client.feed.get(id), [id])
|
const query = useAsync(async () => await client.feed.get(id), [id])
|
||||||
const feed = query.result?.data
|
const feed = query.result?.data
|
||||||
|
|
||||||
const form = useForm<FeedModificationRequest>()
|
const form = useForm<FeedModificationRequest>()
|
||||||
@@ -75,7 +75,7 @@ export function FeedDetailsPage() {
|
|||||||
|
|
||||||
const openUnsubscribeModal = () => {
|
const openUnsubscribeModal = () => {
|
||||||
const feedName = feed?.name
|
const feedName = feed?.name
|
||||||
return openConfirmModal({
|
openConfirmModal({
|
||||||
title: <Trans>Unsubscribe</Trans>,
|
title: <Trans>Unsubscribe</Trans>,
|
||||||
children: (
|
children: (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
@@ -86,7 +86,7 @@ export function FeedDetailsPage() {
|
|||||||
),
|
),
|
||||||
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
||||||
confirmProps: { color: "red" },
|
confirmProps: { color: "red" },
|
||||||
onConfirm: () => unsubscribe.execute({ id: +id }),
|
onConfirm: async () => await unsubscribe.execute({ id: +id }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ export function FeedDetailsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={modifyFeed.loading}>
|
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={modifyFeed.loading}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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 { EntrySourceType, loadEntries } from "app/slices/entries"
|
import { type EntrySourceType, loadEntries } from "app/slices/entries"
|
||||||
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "app/slices/redirect"
|
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "app/slices/redirect"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { flattenCategoryTree } from "app/utils"
|
import { flattenCategoryTree } from "app/utils"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { useMobile } from "hooks/useMobile"
|
|||||||
import { useWebSocket } from "hooks/useWebSocket"
|
import { useWebSocket } from "hooks/useWebSocket"
|
||||||
import { LoadingPage } from "pages/LoadingPage"
|
import { LoadingPage } from "pages/LoadingPage"
|
||||||
import { Resizable } from "re-resizable"
|
import { Resizable } from "re-resizable"
|
||||||
import { ReactNode, Suspense, useEffect } from "react"
|
import { type ReactNode, Suspense, useEffect } from "react"
|
||||||
import { TbPlus } from "react-icons/tb"
|
import { TbPlus } from "react-icons/tb"
|
||||||
import { Outlet } from "react-router-dom"
|
import { Outlet } from "react-router-dom"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ const useStyles = createStyles((theme, props: LayoutProps) => ({
|
|||||||
function LogoAndTitle() {
|
function LogoAndTitle() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
return (
|
return (
|
||||||
<Center inline onClick={() => dispatch(redirectToRootCategory())} style={{ cursor: "pointer" }}>
|
<Center inline onClick={async () => await dispatch(redirectToRootCategory())} style={{ cursor: "pointer" }}>
|
||||||
<Logo size={24} />
|
<Logo size={24} />
|
||||||
<Title order={3} pl="md">
|
<Title order={3} pl="md">
|
||||||
CommaFeed
|
CommaFeed
|
||||||
@@ -115,7 +115,7 @@ export default function Layout(props: LayoutProps) {
|
|||||||
|
|
||||||
if (!webSocketConnected && treeReloadInterval) {
|
if (!webSocketConnected && treeReloadInterval) {
|
||||||
// reload tree periodically if not receiving websocket events
|
// reload tree periodically if not receiving websocket events
|
||||||
timer = window.setInterval(() => dispatch(reloadTree()), treeReloadInterval)
|
timer = window.setInterval(async () => await dispatch(reloadTree()), treeReloadInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
@@ -133,7 +133,7 @@ export default function Layout(props: LayoutProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const addButton = (
|
const addButton = (
|
||||||
<ActionIcon color={theme.primaryColor} onClick={() => dispatch(redirectToAdd())} aria-label="Subscribe">
|
<ActionIcon color={theme.primaryColor} onClick={async () => await dispatch(redirectToAdd())} aria-label="Subscribe">
|
||||||
<TbPlus size={18} />
|
<TbPlus size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function TagDetailsPage() {
|
|||||||
</Input.Wrapper>
|
</Input.Wrapper>
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||||
<Trans>Cancel</Trans>
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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/slices/redirect"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { LoginRequest } from "app/types"
|
import { type LoginRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { PageTitle } from "pages/PageTitle"
|
import { PageTitle } from "pages/PageTitle"
|
||||||
import { useAsyncCallback } from "react-async-hook"
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { t, Trans } from "@lingui/macro"
|
|||||||
import { Anchor, Box, Button, Center, Container, Group, Paper, Stack, TextInput, Title } from "@mantine/core"
|
import { Anchor, Box, Button, Center, Container, Group, Paper, 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 { PasswordResetRequest } from "app/types"
|
import { type PasswordResetRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { PageTitle } from "pages/PageTitle"
|
import { PageTitle } from "pages/PageTitle"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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/slices/redirect"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { RegistrationRequest } from "app/types"
|
import { type RegistrationRequest } from "app/types"
|
||||||
import { Alert } from "components/Alert"
|
import { Alert } from "components/Alert"
|
||||||
import { PageTitle } from "pages/PageTitle"
|
import { PageTitle } from "pages/PageTitle"
|
||||||
import { useAsyncCallback } from "react-async-hook"
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
|
|||||||
2
commafeed-client/src/vite-env.d.ts
vendored
2
commafeed-client/src/vite-env.d.ts
vendored
@@ -1 +1 @@
|
|||||||
/// <reference types="vite/client" />
|
import "vite/client"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.commafeed.frontend.model.request;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
@Schema(description = "Save User information")
|
||||||
|
@Data
|
||||||
|
public class AdminSaveUserRequest implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "user id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "user name", requiredMode = RequiredMode.REQUIRED)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "user email, if any")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "user password")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Schema(description = "account status", requiredMode = RequiredMode.REQUIRED)
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Schema(description = "user is admin", requiredMode = RequiredMode.REQUIRED)
|
||||||
|
private boolean admin;
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import com.commafeed.backend.service.PasswordEncryptionService;
|
|||||||
import com.commafeed.backend.service.UserService;
|
import com.commafeed.backend.service.UserService;
|
||||||
import com.commafeed.frontend.auth.SecurityCheck;
|
import com.commafeed.frontend.auth.SecurityCheck;
|
||||||
import com.commafeed.frontend.model.UserModel;
|
import com.commafeed.frontend.model.UserModel;
|
||||||
|
import com.commafeed.frontend.model.request.AdminSaveUserRequest;
|
||||||
import com.commafeed.frontend.model.request.IDRequest;
|
import com.commafeed.frontend.model.request.IDRequest;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
@@ -66,41 +67,41 @@ public class AdminREST {
|
|||||||
description = "Save or update a user. If the id is not specified, a new user will be created")
|
description = "Save or update a user. If the id is not specified, a new user will be created")
|
||||||
@Timed
|
@Timed
|
||||||
public Response adminSaveUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
|
public Response adminSaveUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
|
||||||
@Parameter(required = true) UserModel userModel) {
|
@Parameter(required = true) AdminSaveUserRequest req) {
|
||||||
Preconditions.checkNotNull(userModel);
|
Preconditions.checkNotNull(req);
|
||||||
Preconditions.checkNotNull(userModel.getName());
|
Preconditions.checkNotNull(req.getName());
|
||||||
|
|
||||||
Long id = userModel.getId();
|
Long id = req.getId();
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
Preconditions.checkNotNull(userModel.getPassword());
|
Preconditions.checkNotNull(req.getPassword());
|
||||||
|
|
||||||
Set<Role> roles = Sets.newHashSet(Role.USER);
|
Set<Role> roles = Sets.newHashSet(Role.USER);
|
||||||
if (userModel.isAdmin()) {
|
if (req.isAdmin()) {
|
||||||
roles.add(Role.ADMIN);
|
roles.add(Role.ADMIN);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
userService.register(userModel.getName(), userModel.getPassword(), userModel.getEmail(), roles, true);
|
userService.register(req.getName(), req.getPassword(), req.getEmail(), roles, true);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
|
return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (userModel.getId().equals(user.getId()) && !userModel.isEnabled()) {
|
if (req.getId().equals(user.getId()) && !req.isEnabled()) {
|
||||||
return Response.status(Status.FORBIDDEN).entity("You cannot disable your own account.").build();
|
return Response.status(Status.FORBIDDEN).entity("You cannot disable your own account.").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
User u = userDAO.findById(id);
|
User u = userDAO.findById(id);
|
||||||
u.setName(userModel.getName());
|
u.setName(req.getName());
|
||||||
if (StringUtils.isNotBlank(userModel.getPassword())) {
|
if (StringUtils.isNotBlank(req.getPassword())) {
|
||||||
u.setPassword(encryptionService.getEncryptedPassword(userModel.getPassword(), u.getSalt()));
|
u.setPassword(encryptionService.getEncryptedPassword(req.getPassword(), u.getSalt()));
|
||||||
}
|
}
|
||||||
u.setEmail(userModel.getEmail());
|
u.setEmail(req.getEmail());
|
||||||
u.setDisabled(!userModel.isEnabled());
|
u.setDisabled(!req.isEnabled());
|
||||||
userDAO.saveOrUpdate(u);
|
userDAO.saveOrUpdate(u);
|
||||||
|
|
||||||
Set<Role> roles = userRoleDAO.findRoles(u);
|
Set<Role> roles = userRoleDAO.findRoles(u);
|
||||||
if (userModel.isAdmin() && !roles.contains(Role.ADMIN)) {
|
if (req.isAdmin() && !roles.contains(Role.ADMIN)) {
|
||||||
userRoleDAO.saveOrUpdate(new UserRole(u, Role.ADMIN));
|
userRoleDAO.saveOrUpdate(new UserRole(u, Role.ADMIN));
|
||||||
} else if (!userModel.isAdmin() && roles.contains(Role.ADMIN)) {
|
} else if (!req.isAdmin() && roles.contains(Role.ADMIN)) {
|
||||||
if (CommaFeedApplication.USERNAME_ADMIN.equals(u.getName())) {
|
if (CommaFeedApplication.USERNAME_ADMIN.equals(u.getName())) {
|
||||||
return Response.status(Status.FORBIDDEN).entity("You cannot remove the admin role from the admin user.").build();
|
return Response.status(Status.FORBIDDEN).entity("You cannot remove the admin role from the admin user.").build();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user