eslint update

This commit is contained in:
Athou
2023-12-28 19:54:51 +01:00
parent f4e48383cc
commit 97781d5551
65 changed files with 1258 additions and 3103 deletions

View 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"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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)

View File

@@ -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]
} }
} }

View File

@@ -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`,

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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`))

View File

@@ -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,

View File

@@ -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({

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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)

View File

@@ -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

View File

@@ -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 (

View File

@@ -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

View File

@@ -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 })

View File

@@ -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

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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>
))} ))}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>
) )

View File

@@ -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"

View File

@@ -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

View File

@@ -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}`

View File

@@ -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}>

View File

@@ -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"

View File

@@ -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}>

View File

@@ -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"

View File

@@ -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>
} }

View File

@@ -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",

View File

@@ -1,4 +1,4 @@
import { MetricGauge } from "app/types" import { type MetricGauge } from "app/types"
interface MeterProps { interface MeterProps {
gauge: MetricGauge gauge: MetricGauge

View File

@@ -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

View File

@@ -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

View File

@@ -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}>

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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} />

View File

@@ -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

View File

@@ -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() {

View File

@@ -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)
}) })

View File

@@ -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>
)

View File

@@ -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>

View File

@@ -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 }),
}) })
} }

View File

@@ -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

View File

@@ -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>

View File

@@ -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 && (

View File

@@ -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}>

View File

@@ -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"

View File

@@ -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>
) )

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -1 +1 @@
/// <reference types="vite/client" /> import "vite/client"

View File

@@ -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;
}

View File

@@ -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();
} }