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": {
"@lingui/cli": "^4.1.2",
"@lingui/vite-plugin": "^4.1.2",
"@types/eslint": "^8.40.0",
"@types/mousetrap": "^1.6.11",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
@@ -62,20 +61,21 @@
"@types/swagger-ui-react": "^4.18.0",
"@types/throttle-debounce": "^5.0.0",
"@types/tinycon": "^0.6.3",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@vitejs/plugin-react": "^4.0.0",
"babel-plugin-macros": "^3.1.0",
"eslint": "^8.41.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-hooks": "^0.4.3",
"eslint-config-standard-with-typescript": "^43.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.5.0",
"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",
"rollup-plugin-visualizer": "^5.9.0",
"typescript": "^5.0.4",
"typescript": "^5.3.3",
"vite": "^4.3.9",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0",

View File

@@ -1,6 +1,6 @@
import { i18n } from "@lingui/core"
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 { ModalsProvider } from "@mantine/modals"
import { Notifications } from "@mantine/notifications"
@@ -63,7 +63,7 @@ function Providers(props: { children: React.ReactNode }) {
}
// 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() {
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)

View File

@@ -1,29 +1,30 @@
import axios from "axios"
import {
AddCategoryRequest,
Category,
CategoryModificationRequest,
CollapseRequest,
Entries,
FeedInfo,
FeedInfoRequest,
FeedModificationRequest,
GetEntriesPaginatedRequest,
IDRequest,
LoginRequest,
MarkRequest,
Metrics,
MultipleMarkRequest,
PasswordResetRequest,
ProfileModificationRequest,
RegistrationRequest,
ServerInfo,
Settings,
StarRequest,
SubscribeRequest,
Subscription,
TagRequest,
UserModel,
type AddCategoryRequest,
type AdminSaveUserRequest,
type Category,
type CategoryModificationRequest,
type CollapseRequest,
type Entries,
type FeedInfo,
type FeedInfoRequest,
type FeedModificationRequest,
type GetEntriesPaginatedRequest,
type IDRequest,
type LoginRequest,
type MarkRequest,
type Metrics,
type MultipleMarkRequest,
type PasswordResetRequest,
type ProfileModificationRequest,
type RegistrationRequest,
type ServerInfo,
type Settings,
type StarRequest,
type SubscribeRequest,
type Subscription,
type TagRequest,
type UserModel,
} from "./types"
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
@@ -42,34 +43,34 @@ axiosInstance.interceptors.response.use(
export const client = {
category: {
getRoot: () => axiosInstance.get<Category>("category/get"),
modify: (req: CategoryModificationRequest) => axiosInstance.post("category/modify", req),
collapse: (req: CollapseRequest) => axiosInstance.post("category/collapse", req),
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("category/entries", { params: req }),
markEntries: (req: MarkRequest) => axiosInstance.post("category/mark", req),
add: (req: AddCategoryRequest) => axiosInstance.post("category/add", req),
delete: (req: IDRequest) => axiosInstance.post("category/delete", req),
getRoot: async () => await axiosInstance.get<Category>("category/get"),
modify: async (req: CategoryModificationRequest) => await axiosInstance.post("category/modify", req),
collapse: async (req: CollapseRequest) => await axiosInstance.post("category/collapse", req),
getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("category/entries", { params: req }),
markEntries: async (req: MarkRequest) => await axiosInstance.post("category/mark", req),
add: async (req: AddCategoryRequest) => await axiosInstance.post("category/add", req),
delete: async (req: IDRequest) => await axiosInstance.post("category/delete", req),
},
entry: {
mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req),
markMultiple: (req: MultipleMarkRequest) => axiosInstance.post("entry/markMultiple", req),
star: (req: StarRequest) => axiosInstance.post("entry/star", req),
getTags: () => axiosInstance.get<string[]>("entry/tags"),
tag: (req: TagRequest) => axiosInstance.post("entry/tag", req),
mark: async (req: MarkRequest) => await axiosInstance.post("entry/mark", req),
markMultiple: async (req: MultipleMarkRequest) => await axiosInstance.post("entry/markMultiple", req),
star: async (req: StarRequest) => await axiosInstance.post("entry/star", req),
getTags: async () => await axiosInstance.get<string[]>("entry/tags"),
tag: async (req: TagRequest) => await axiosInstance.post("entry/tag", req),
},
feed: {
get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`),
modify: (req: FeedModificationRequest) => axiosInstance.post("feed/modify", req),
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("feed/entries", { params: req }),
markEntries: (req: MarkRequest) => axiosInstance.post("feed/mark", req),
fetchFeed: (req: FeedInfoRequest) => axiosInstance.post<FeedInfo>("feed/fetch", req),
refreshAll: () => axiosInstance.get("feed/refreshAll"),
subscribe: (req: SubscribeRequest) => axiosInstance.post<number>("feed/subscribe", req),
unsubscribe: (req: IDRequest) => axiosInstance.post("feed/unsubscribe", req),
importOpml: (req: File) => {
get: async (id: string) => await axiosInstance.get<Subscription>(`feed/get/${id}`),
modify: async (req: FeedModificationRequest) => await axiosInstance.post("feed/modify", req),
getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("feed/entries", { params: req }),
markEntries: async (req: MarkRequest) => await axiosInstance.post("feed/mark", req),
fetchFeed: async (req: FeedInfoRequest) => await axiosInstance.post<FeedInfo>("feed/fetch", req),
refreshAll: async () => await axiosInstance.get("feed/refreshAll"),
subscribe: async (req: SubscribeRequest) => await axiosInstance.post<number>("feed/subscribe", req),
unsubscribe: async (req: IDRequest) => await axiosInstance.post("feed/unsubscribe", req),
importOpml: async (req: File) => {
const formData = new FormData()
formData.append("file", req)
return axiosInstance.post("feed/import", formData, {
return await axiosInstance.post("feed/import", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
@@ -77,23 +78,23 @@ export const client = {
},
},
user: {
login: (req: LoginRequest) => axiosInstance.post("user/login", req),
register: (req: RegistrationRequest) => axiosInstance.post("user/register", req),
passwordReset: (req: PasswordResetRequest) => axiosInstance.post("user/passwordReset", req),
getSettings: () => axiosInstance.get<Settings>("user/settings"),
saveSettings: (settings: Settings) => axiosInstance.post("user/settings", settings),
getProfile: () => axiosInstance.get<UserModel>("user/profile"),
saveProfile: (req: ProfileModificationRequest) => axiosInstance.post("user/profile", req),
deleteProfile: () => axiosInstance.post("user/profile/deleteAccount"),
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings),
getProfile: async () => await axiosInstance.get<UserModel>("user/profile"),
saveProfile: async (req: ProfileModificationRequest) => await axiosInstance.post("user/profile", req),
deleteProfile: async () => await axiosInstance.post("user/profile/deleteAccount"),
},
server: {
getServerInfos: () => axiosInstance.get<ServerInfo>("server/get"),
getServerInfos: async () => await axiosInstance.get<ServerInfo>("server/get"),
},
admin: {
getAllUsers: () => axiosInstance.get<UserModel[]>("admin/user/getAll"),
saveUser: (req: UserModel) => axiosInstance.post("admin/user/save", req),
deleteUser: (req: IDRequest) => axiosInstance.post("admin/user/delete", req),
getMetrics: () => axiosInstance.get<Metrics>("admin/metrics"),
getAllUsers: async () => await axiosInstance.get<UserModel[]>("admin/user/getAll"),
saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req),
deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req),
getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"),
},
}
@@ -109,7 +110,7 @@ export const errorToStrings = (err: unknown) => {
if (err.response) {
const { data } = err.response
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]
}
}

View File

@@ -1,11 +1,11 @@
import { t } from "@lingui/macro"
import { DEFAULT_THEME } from "@mantine/core"
import { IconType } from "react-icons"
import { type IconType } from "react-icons"
import { FaAt } from "react-icons/fa"
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: {
id: "all",
name: t`All`,

View File

@@ -1,9 +1,9 @@
/* eslint-disable import/first */
import { configureStore } from "@reduxjs/toolkit"
import { client } from "app/client"
import { type client } from "app/client"
import { reducers } from "app/store"
import { Entries, Entry } from "app/types"
import { AxiosResponse } from "axios"
import { type Entries, type Entry } from "app/types"
import { type AxiosResponse } from "axios"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { mockReset } from "vitest-mock-extended"
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 { Constants } from "app/constants"
import { createAppAsyncThunk, RootState } from "app/store"
import { Entry, MarkRequest, TagRequest } from "app/types"
import { createAppAsyncThunk, type RootState } from "app/store"
import { type Entry, type MarkRequest, type TagRequest } from "app/types"
import { scrollToWithCallback } from "app/utils"
import { flushSync } from "react-dom"
// eslint-disable-next-line import/no-cycle
@@ -11,7 +11,10 @@ import { reloadTree } from "./tree"
import { reloadTags } from "./user"
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 }
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 { createAppAsyncThunk } from "app/store"
@@ -22,8 +22,9 @@ export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSo
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
)
export const redirectToRootCategory = createAppAsyncThunk("redirect/category/root", (_, thunkApi) =>
thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
export const redirectToRootCategory = createAppAsyncThunk(
"redirect/category/root",
async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
)
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
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 { createAppAsyncThunk } from "app/store"
import { ServerInfo } from "app/types"
import { type ServerInfo } from "app/types"
interface ServerState {
serverInfos?: ServerInfo
@@ -12,7 +12,7 @@ const initialState: ServerState = {
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({
name: "server",
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 { createAppAsyncThunk } from "app/store"
import { Category, CollapseRequest } from "app/types"
import { type Category, type CollapseRequest } from "app/types"
import { visitCategoryTree } from "app/utils"
// eslint-disable-next-line import/no-cycle
import { markEntry } from "./entries"
@@ -20,9 +20,10 @@ const initialState: TreeState = {
sidebarVisible: true,
}
export const reloadTree = createAppAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
export const collapseTreeCategory = createAppAsyncThunk("tree/category/collapse", async (req: CollapseRequest) =>
client.category.collapse(req)
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
export const collapseTreeCategory = createAppAsyncThunk(
"tree/category/collapse",
async (req: CollapseRequest) => await client.category.collapse(req)
)
export const treeSlice = createSlice({

View File

@@ -3,7 +3,7 @@ import { showNotification } from "@mantine/notifications"
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
import { client } from "app/client"
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
import { reloadEntries } from "./entries"
@@ -15,9 +15,9 @@ interface UserState {
const initialState: UserState = {}
export const reloadSettings = createAppAsyncThunk("settings/reload", () => client.user.getSettings().then(r => r.data))
export const reloadProfile = createAppAsyncThunk("profile/reload", () => client.user.getProfile().then(r => r.data))
export const reloadTags = createAppAsyncThunk("entries/tags", () => client.entry.getTags().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", async () => await client.user.getProfile().then(r => r.data))
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return

View File

@@ -1,6 +1,6 @@
import { configureStore, createAsyncThunk } from "@reduxjs/toolkit"
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 redirectReducer from "./slices/redirect"
import serverReducer from "./slices/server"

View File

@@ -134,7 +134,7 @@ export interface MetricMeter {
units: string
}
export type MetricTimer = {
export interface MetricTimer {
count: number
max: number
mean: number
@@ -155,10 +155,10 @@ export type MetricTimer = {
}
export interface Metrics {
counters: { [key: string]: MetricCounter }
gauges: { [key: string]: MetricGauge }
meters: { [key: string]: MetricMeter }
timers: { [key: string]: MetricTimer }
counters: Record<string, MetricCounter>
gauges: Record<string, MetricGauge>
meters: Record<string, MetricMeter>
timers: Record<string, MetricTimer>
}
export interface MultipleMarkRequest {
@@ -273,6 +273,15 @@ export interface UserModel {
admin: boolean
}
export interface AdminSaveUserRequest {
id?: number
name: string
email?: string
password?: string
enabled: boolean
admin: boolean
}
export type ReadingMode = "all" | "unread"
export type ReadingOrder = "asc" | "desc"

View File

@@ -1,5 +1,5 @@
import { throttle } from "throttle-debounce"
import { Category } from "./types"
import { type Category } from "./types"
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
visitor(category)

View File

@@ -1,8 +1,8 @@
import { ActionIcon, Button, Tooltip, useMantineTheme } from "@mantine/core"
import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
import { ButtonProps } from "@mantine/core/lib/Button/Button"
import { type ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
import { type ButtonProps } from "@mantine/core/lib/Button/Button"
import { useActionButton } from "hooks/useActionButton"
import { forwardRef, MouseEventHandler, ReactNode } from "react"
import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
interface ActionButtonProps {
className?: string

View File

@@ -1,9 +1,10 @@
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 { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
type Level = "error" | "warning" | "success"
export interface ErrorsAlertProps {
level?: Level
messages: string[]
@@ -31,8 +32,6 @@ export function Alert(props: ErrorsAlertProps) {
color = "green"
icon = <TbCircleCheck />
break
default:
throw Error(`unsupported level: ${level}`)
}
return (

View File

@@ -1,5 +1,5 @@
import { ErrorPage } from "pages/ErrorPage"
import React, { ReactNode } from "react"
import React, { type ReactNode } from "react"
interface ErrorBoundaryProps {
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 { useForm } from "@mantine/form"
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 { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb"
@@ -14,8 +14,12 @@ interface UserEditProps {
}
export function UserEdit(props: UserEditProps) {
const form = useForm<UserModel>({
initialValues: props.user ?? ({ enabled: true } as UserModel),
const form = useForm<AdminSaveUserRequest>({
initialValues: props.user ?? {
name: "",
enabled: true,
admin: false,
},
})
const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave })

View File

@@ -1,7 +1,7 @@
import { Input, Textarea } from "@mantine/core"
import RichCodeEditor from "components/code/RichCodeEditor"
import { useMobile } from "hooks/useMobile"
import { ReactNode } from "react"
import { type ReactNode } from "react"
interface CodeEditorProps {
description?: ReactNode

View File

@@ -3,7 +3,7 @@ import { Constants } from "app/constants"
import { calculatePlaceholderSize } from "app/utils"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
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"
export interface ContentProps {
@@ -61,7 +61,7 @@ const transform: TransformCallback = node => {
}
class HighlightMatcher extends Matcher {
private search: string
private readonly search: string
constructor(search: string) {
super("highlight")
@@ -73,7 +73,6 @@ class HighlightMatcher extends Matcher {
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 {
return <Mark>{children}</Mark>
}
@@ -97,5 +96,6 @@ const Content = React.memo((props: ContentProps) => {
</TypographyStylesProvider>
)
})
Content.displayName = "Content"
export { Content }

View File

@@ -9,13 +9,11 @@ export function Enclosure(props: { enclosureType: string; enclosureUrl: string }
return (
<TypographyStylesProvider>
{hasVideo && (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video controls>
<source src={props.enclosureUrl} type={props.enclosureType} />
</video>
)}
{hasAudio && (
// eslint-disable-next-line jsx-a11y/media-has-caption
<audio controls>
<source src={props.enclosureUrl} type={props.enclosureType} />
</audio>

View File

@@ -3,7 +3,7 @@ import { Box } from "@mantine/core"
import { openModal } from "@mantine/modals"
import { Constants } from "app/constants"
import {
ExpendableEntry,
type ExpendableEntry,
loadMoreEntries,
markAllEntries,
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
useEffect(() => {
@@ -128,42 +128,50 @@ export function FeedEntries() {
return () => window.removeEventListener("scroll", listener)
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
useMousetrap("r", () => dispatch(reloadEntries()))
useMousetrap("j", () =>
dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
useMousetrap("r", async () => await dispatch(reloadEntries()))
useMousetrap(
"j",
async () =>
await dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
)
useMousetrap("n", () =>
dispatch(
selectNextEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
useMousetrap(
"n",
async () =>
await dispatch(
selectNextEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
)
useMousetrap("k", () =>
dispatch(
selectPreviousEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
useMousetrap(
"k",
async () =>
await dispatch(
selectPreviousEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
)
useMousetrap("p", () =>
dispatch(
selectPreviousEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
useMousetrap(
"p",
async () =>
await dispatch(
selectPreviousEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
)
useMousetrap("space", () => {
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("?", () =>
openModal({
@@ -291,7 +299,7 @@ export function FeedEntries() {
<InfiniteScroll
id="entries"
initialLoad={false}
loadMore={() => !loading && dispatch(loadMoreEntries())}
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
hasMore={hasMore}
loader={<Box key={0}>{loading && <Loader />}</Box>}
>
@@ -311,7 +319,7 @@ export function FeedEntries() {
onHeaderClick={event => headerClicked(entry, event)}
onHeaderRightClick={event => headerRightClicked(entry, event)}
onBodyClick={() => bodyClicked(entry)}
onSwipedRight={() => swipedRight(entry)}
onSwipedRight={async () => await swipedRight(entry)}
/>
</div>
))}

View File

@@ -1,7 +1,7 @@
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 { Entry, ViewMode } from "app/types"
import { type Entry, type ViewMode } from "app/types"
import { useViewMode } from "hooks/useViewMode"
import React from "react"
import { useSwipeable } from "react-swipeable"

View File

@@ -1,6 +1,6 @@
import { Box } from "@mantine/core"
import { useAppSelector } from "app/store"
import { Entry } from "app/types"
import { type Entry } from "app/types"
import { Content } from "./Content"
import { Enclosure } from "./Enclosure"
import { Media } from "./Media"

View File

@@ -1,5 +1,5 @@
import { Box, createStyles, Text } from "@mantine/core"
import { Entry } from "app/types"
import { type Entry } from "app/types"
import { RelativeDate } from "components/RelativeDate"
import { OnDesktop } from "components/responsive/OnDesktop"
import { FeedEntryTitle } from "./FeedEntryTitle"

View File

@@ -4,7 +4,7 @@ import { Constants } from "app/constants"
import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
import { redirectToFeed } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types"
import { type Entry } from "app/types"
import { truncate } from "app/utils"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { Item, Menu, Separator } from "react-contexify"
@@ -60,19 +60,19 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
<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>
{props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />}
{props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
</Group>
</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>
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
</Group>
</Item>
<Item onClick={() => dispatch(markEntriesUpToEntry(props.entry))}>
<Item onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}>
<Group>
<TbArrowBarToDown size={iconSize} />
<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 { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries"
import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types"
import { type Entry } from "app/types"
import { ActionButton } from "components/ActionButton"
import { useActionButton } from "hooks/useActionButton"
import { useMobile } from "hooks/useMobile"
@@ -22,9 +22,9 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v)
const readStatusButtonClicked = () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))
const onTagsChange = (values: string[]) =>
dispatch(
const readStatusButtonClicked = async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))
const onTagsChange = async (values: string[]) =>
await dispatch(
tagEntry({
entryId: +props.entry.id,
tags: values,
@@ -44,7 +44,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<ActionButton
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
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 && (
@@ -88,7 +88,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<ActionButton
icon={<TbArrowBarToDown size={18} />}
label={<Trans>Mark as read up to here</Trans>}
onClick={() => dispatch(markEntriesUpToEntry(props.entry))}
onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
/>
</Group>
)

View File

@@ -1,5 +1,5 @@
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 { FeedEntryTitle } from "./FeedEntryTitle"
import { FeedFavicon } from "./FeedFavicon"

View File

@@ -1,6 +1,6 @@
import { Highlight } from "@mantine/core"
import { useAppSelector } from "app/store"
import { Entry } from "app/types"
import { type Entry } from "app/types"
export interface FeedEntryTitleProps {
entry: Entry

View File

@@ -1,8 +1,8 @@
import { ActionIcon, Box, createStyles, SimpleGrid } from "@mantine/core"
import { Constants } from "app/constants"
import { useAppSelector } from "app/store"
import { SharingSettings } from "app/types"
import { IconType } from "react-icons"
import { type SharingSettings } from "app/types"
import { type IconType } from "react-icons"
type Color = `#${string}`

View File

@@ -5,7 +5,7 @@ import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store"
import { AddCategoryRequest } from "app/types"
import { type AddCategoryRequest } from "app/types"
import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook"
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 />
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
<Group position="center">
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" leftIcon={<TbFolderPlus size={16} />} loading={addCategory.loading}>

View File

@@ -1,5 +1,5 @@
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 { useAppSelector } from "app/store"
import { flattenCategoryTree } from "app/utils"

View File

@@ -33,7 +33,7 @@ export function ImportOpml() {
</Box>
)}
<form onSubmit={form.onSubmit(v => importOpml.execute(v.file))}>
<form onSubmit={form.onSubmit(async v => await importOpml.execute(v.file))}>
<Stack>
<FileInput
label={<Trans>OPML file</Trans>}
@@ -49,7 +49,7 @@ export function ImportOpml() {
accept="application/xml"
/>
<Group position="center">
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<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 { reloadTree } from "app/slices/tree"
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 { useState } from "react"
import { useAsyncCallback } from "react-async-hook"

View File

@@ -79,8 +79,8 @@ export function Header() {
<ActionButton
icon={<TbArrowUp size={iconSize} />}
label={<Trans>Previous</Trans>}
onClick={() =>
dispatch(
onClick={async () =>
await dispatch(
selectPreviousEntry({
expand: true,
markAsRead: true,
@@ -92,8 +92,8 @@ export function Header() {
<ActionButton
icon={<TbArrowDown size={iconSize} />}
label={<Trans>Next</Trans>}
onClick={() =>
dispatch(
onClick={async () =>
await dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
@@ -108,7 +108,7 @@ export function Header() {
<ActionButton
icon={<TbRefresh size={iconSize} />}
label={<Trans>Refresh</Trans>}
onClick={() => dispatch(reloadEntries())}
onClick={async () => await dispatch(reloadEntries())}
/>
<MarkAllAsReadButton iconSize={iconSize} />
@@ -117,12 +117,12 @@ export function Header() {
<ActionButton
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
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
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
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>
@@ -132,13 +132,13 @@ export function Header() {
</Indicator>
</Popover.Target>
<Popover.Dropdown>
<form onSubmit={searchForm.onSubmit(values => dispatch(search(values.search)))}>
<form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
<TextInput
placeholder={t`Search`}
{...searchForm.getInputProps("search")}
icon={<TbSearch size={iconSize} />}
rightSection={
<ActionIcon onClick={() => searchFromStore && dispatch(search(""))}>
<ActionIcon onClick={async () => await (searchFromStore && dispatch(search("")))}>
<TbX />
</ActionIcon>
}

View File

@@ -1,10 +1,10 @@
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 { client } from "app/client"
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { ViewMode } from "app/types"
import { type ViewMode } from "app/types"
import { useViewMode } from "hooks/useViewMode"
import { useState } from "react"
import {
@@ -109,8 +109,8 @@ export function ProfileMenu(props: ProfileMenuProps) {
</Menu.Item>
<Menu.Item
icon={<TbWorldDownload size={iconSize} />}
onClick={() =>
client.feed.refreshAll().then(() => {
onClick={async () =>
await client.feed.refreshAll().then(() => {
showNotification({
message: <Trans>Your feeds have been queued for refresh.</Trans>,
color: "green",

View File

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

View File

@@ -1,5 +1,5 @@
import { Box } from "@mantine/core"
import { MetricMeter } from "app/types"
import { type MetricMeter } from "app/types"
interface MeterProps {
meter: MetricMeter

View File

@@ -1,5 +1,5 @@
import { Box } from "@mantine/core"
import { MetricTimer } from "app/types"
import { type MetricTimer } from "app/types"
interface MetricTimerProps {
timer: MetricTimer

View File

@@ -69,7 +69,7 @@ export function CustomCodeSettings() {
/>
<Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>

View File

@@ -12,7 +12,7 @@ import {
changeShowRead,
} from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { SharingSettings } from "app/types"
import { type SharingSettings } from "app/types"
import { locales } from "i18n"
export function DisplaySettings() {
@@ -35,43 +35,43 @@ export function DisplaySettings() {
value: l.key,
label: l.label,
}))}
onChange={s => s && dispatch(changeLanguage(s))}
onChange={async s => await (s && dispatch(changeLanguage(s)))}
/>
<Switch
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
checked={scrollSpeed ? scrollSpeed > 0 : false}
onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))}
onChange={async e => await dispatch(changeScrollSpeed(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
checked={alwaysScrollToEntry}
onChange={e => dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
onChange={async e => await dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show feeds and categories with no unread entries</Trans>}
checked={showRead}
onChange={e => dispatch(changeShowRead(e.currentTarget.checked))}
onChange={async e => await dispatch(changeShowRead(e.currentTarget.checked))}
/>
<Switch
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
checked={scrollMarks}
onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))}
onChange={async e => await dispatch(changeScrollMarks(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show confirmation when marking all entries as read</Trans>}
checked={markAllAsReadConfirmation}
onChange={e => dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
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" />
@@ -82,7 +82,7 @@ export function DisplaySettings() {
key={site}
label={Constants.sharing[site].label}
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>

View File

@@ -6,7 +6,7 @@ import { client, errorToStrings } from "app/client"
import { redirectToLogin, redirectToSelectedSource } from "app/slices/redirect"
import { reloadProfile } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { ProfileModificationRequest } from "app/types"
import { type ProfileModificationRequest } from "app/types"
import { Alert } from "components/Alert"
import { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook"
@@ -49,7 +49,7 @@ export function ProfileSettings() {
),
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
confirmProps: { color: "red" },
onConfirm: () => deleteProfile.execute(),
onConfirm: async () => await deleteProfile.execute(),
})
useEffect(() => {
@@ -129,7 +129,7 @@ export function ProfileSettings() {
<Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} />
<Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}>

View File

@@ -11,7 +11,7 @@ import {
} from "app/slices/redirect"
import { collapseTreeCategory } from "app/slices/tree"
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 { Loader } from "components/Loader"
import { OnDesktop } from "components/responsive/OnDesktop"

View File

@@ -1,6 +1,6 @@
import { Box, Center, createStyles } from "@mantine/core"
import { FeedFavicon } from "components/content/FeedFavicon"
import React, { ReactNode } from "react"
import React, { type ReactNode } from "react"
import { UnreadCount } from "./UnreadCount"
interface TreeNodeProps {

View File

@@ -1,9 +1,9 @@
import { t, Trans } from "@lingui/macro"
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 { useAppDispatch } from "app/store"
import { Subscription } from "app/types"
import { type Subscription } from "app/types"
import { FeedFavicon } from "components/content/FeedFavicon"
import { useMousetrap } from "hooks/useMousetrap"
import { TbSearch } from "react-icons/tb"
@@ -20,7 +20,7 @@ export function TreeSearch(props: TreeSearchProps) {
.map(f => ({
title: f.name,
icon: <FeedFavicon url={f.iconUrl} />,
onTrigger: () => dispatch(redirectToFeed(f.id)),
onTrigger: async () => await dispatch(redirectToFeed(f.id)),
}))
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"
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"
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 dayjs from "dayjs"
import { useEffect } from "react"
@@ -12,40 +12,40 @@ interface 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
export const locales: Locale[] = [
{ key: "ar", label: "العربية", daysjsImportFn: () => import("dayjs/locale/ar") },
{ key: "ca", label: "Català", daysjsImportFn: () => import("dayjs/locale/ca") },
{ key: "cs", label: "Čeština", daysjsImportFn: () => import("dayjs/locale/cs") },
{ key: "cy", label: "Cymraeg", daysjsImportFn: () => import("dayjs/locale/cy") },
{ key: "da", label: "Danish", daysjsImportFn: () => import("dayjs/locale/da") },
{ key: "de", label: "Deutsch", daysjsImportFn: () => import("dayjs/locale/de") },
{ key: "en", label: "English", daysjsImportFn: () => import("dayjs/locale/en") },
{ key: "es", label: "Español", daysjsImportFn: () => import("dayjs/locale/es") },
{ key: "fa", label: "فارسی", daysjsImportFn: () => import("dayjs/locale/fa") },
{ key: "fi", label: "Suomi", daysjsImportFn: () => import("dayjs/locale/fi") },
{ key: "fr", label: "Français", daysjsImportFn: () => import("dayjs/locale/fr") },
{ key: "gl", label: "Galician", daysjsImportFn: () => import("dayjs/locale/gl") },
{ key: "hu", label: "Magyar", daysjsImportFn: () => import("dayjs/locale/hu") },
{ key: "id", label: "Indonesian", daysjsImportFn: () => import("dayjs/locale/id") },
{ key: "it", label: "Italiano", daysjsImportFn: () => import("dayjs/locale/it") },
{ key: "ja", label: "日本語", daysjsImportFn: () => import("dayjs/locale/ja") },
{ key: "ko", label: "한국어", daysjsImportFn: () => import("dayjs/locale/ko") },
{ key: "ms", label: "Bahasa Malaysian", daysjsImportFn: () => import("dayjs/locale/ms") },
{ key: "nb", label: "Norsk (bokmål)", daysjsImportFn: () => import("dayjs/locale/nb") },
{ key: "nl", label: "Nederlands", daysjsImportFn: () => import("dayjs/locale/nl") },
{ key: "nn", label: "Norsk (nynorsk)", daysjsImportFn: () => import("dayjs/locale/nn") },
{ key: "pl", label: "Polski", daysjsImportFn: () => import("dayjs/locale/pl") },
{ key: "pt", label: "Português", daysjsImportFn: () => import("dayjs/locale/pt") },
{ key: "ru", label: "Русский", daysjsImportFn: () => import("dayjs/locale/ru") },
{ key: "sk", label: "Slovenčina", daysjsImportFn: () => import("dayjs/locale/sk") },
{ key: "sv", label: "Svenska", daysjsImportFn: () => import("dayjs/locale/sv") },
{ key: "tr", label: "Türkçe", daysjsImportFn: () => import("dayjs/locale/tr") },
{ key: "zh", label: "简体中文", daysjsImportFn: () => import("dayjs/locale/zh") },
{ key: "ar", label: "العربية", daysjsImportFn: async () => await import("dayjs/locale/ar") },
{ key: "ca", label: "Català", daysjsImportFn: async () => await import("dayjs/locale/ca") },
{ key: "cs", label: "Čeština", daysjsImportFn: async () => await import("dayjs/locale/cs") },
{ key: "cy", label: "Cymraeg", daysjsImportFn: async () => await import("dayjs/locale/cy") },
{ key: "da", label: "Danish", daysjsImportFn: async () => await import("dayjs/locale/da") },
{ key: "de", label: "Deutsch", daysjsImportFn: async () => await import("dayjs/locale/de") },
{ key: "en", label: "English", daysjsImportFn: async () => await import("dayjs/locale/en") },
{ key: "es", label: "Español", daysjsImportFn: async () => await import("dayjs/locale/es") },
{ key: "fa", label: "فارسی", daysjsImportFn: async () => await import("dayjs/locale/fa") },
{ key: "fi", label: "Suomi", daysjsImportFn: async () => await import("dayjs/locale/fi") },
{ key: "fr", label: "Français", daysjsImportFn: async () => await import("dayjs/locale/fr") },
{ key: "gl", label: "Galician", daysjsImportFn: async () => await import("dayjs/locale/gl") },
{ key: "hu", label: "Magyar", daysjsImportFn: async () => await import("dayjs/locale/hu") },
{ key: "id", label: "Indonesian", daysjsImportFn: async () => await import("dayjs/locale/id") },
{ key: "it", label: "Italiano", daysjsImportFn: async () => await import("dayjs/locale/it") },
{ key: "ja", label: "日本語", daysjsImportFn: async () => await import("dayjs/locale/ja") },
{ key: "ko", label: "한국어", daysjsImportFn: async () => await import("dayjs/locale/ko") },
{ key: "ms", label: "Bahasa Malaysian", daysjsImportFn: async () => await import("dayjs/locale/ms") },
{ key: "nb", label: "Norsk (bokmål)", daysjsImportFn: async () => await import("dayjs/locale/nb") },
{ key: "nl", label: "Nederlands", daysjsImportFn: async () => await import("dayjs/locale/nl") },
{ key: "nn", label: "Norsk (nynorsk)", daysjsImportFn: async () => await import("dayjs/locale/nn") },
{ key: "pl", label: "Polski", daysjsImportFn: async () => await import("dayjs/locale/pl") },
{ key: "pt", label: "Português", daysjsImportFn: async () => await import("dayjs/locale/pt") },
{ key: "ru", label: "Русский", daysjsImportFn: async () => await import("dayjs/locale/ru") },
{ key: "sk", label: "Slovenčina", daysjsImportFn: async () => await import("dayjs/locale/sk") },
{ key: "sv", label: "Svenska", daysjsImportFn: async () => await import("dayjs/locale/sv") },
{ key: "tr", label: "Türkçe", daysjsImportFn: async () => await import("dayjs/locale/tr") },
{ key: "zh", label: "简体中文", daysjsImportFn: async () => await import("dayjs/locale/zh") },
]
function activateLocale(locale: string) {
// lingui
import(`./locales/${locale}/messages.po`).then(data => {
i18n.load(locale, data.messages)
i18n.load(locale, data.messages as Messages)
i18n.activate(locale)
})

View File

@@ -10,8 +10,10 @@ import { Provider } from "react-redux"
dayjs.extend(relativeTime)
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Provider store={store}>
<App />
</Provider>
)
const root = document.getElementById("root")
root &&
ReactDOM.createRoot(root).render(
<Provider store={store}>
<App />
</Provider>
)

View File

@@ -41,7 +41,7 @@ export function WelcomePage() {
label={<Trans>Try the demo!</Trans>}
icon={<TbClock size={iconSize} />}
variant="outline"
onClick={() => login.execute({ name: "demo", password: "demo" })}
onClick={async () => await login.execute({ name: "demo", password: "demo" })}
showLabelOnMobile
/>
</Center>
@@ -93,7 +93,7 @@ function Buttons() {
label={<Trans>Log in</Trans>}
icon={<TbKey size={iconSize} />}
variant="outline"
onClick={() => dispatch(redirectToLogin())}
onClick={async () => await dispatch(redirectToLogin())}
showLabelOnMobile
/>
{serverInfos?.allowRegistrations && (
@@ -101,7 +101,7 @@ function Buttons() {
label={<Trans>Sign up</Trans>}
icon={<TbUserPlus size={iconSize} />}
variant="filled"
onClick={() => dispatch(redirectToRegistration())}
onClick={async () => await dispatch(redirectToRegistration())}
showLabelOnMobile
/>
)}
@@ -139,7 +139,7 @@ function Footer() {
</Anchor>
</Group>
<Box>
<Anchor variant="text" onClick={() => dispatch(redirectToApiDocumentation())}>
<Anchor variant="text" onClick={async () => await dispatch(redirectToApiDocumentation())}>
API documentation
</Anchor>
</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 { closeAllModals, openConfirmModal, openModal } from "@mantine/modals"
import { client, errorToStrings } from "app/client"
import { UserModel } from "app/types"
import { type UserModel } from "app/types"
import { UserEdit } from "components/admin/UserEdit"
import { Alert } from "components/Alert"
import { Loader } from "components/Loader"
import { RelativeDate } from "components/RelativeDate"
import { ReactNode } from "react"
import { type ReactNode } from "react"
import { useAsync, useAsyncCallback } from "react-async-hook"
import { TbCheck, TbPencil, TbPlus, TbTrash, TbX } from "react-icons/tb"
@@ -17,7 +17,7 @@ function BooleanIcon({ value }: { value: boolean }) {
export function AdminUsersPage() {
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 deleteUser = useAsyncCallback(client.admin.deleteUser, {
@@ -56,7 +56,7 @@ export function AdminUsersPage() {
),
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
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 { 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.FeedRefreshWorker.feedFetched": "Feed fetching 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",
}
const shownGauges: { [key: string]: string } = {
const shownGauges: Record<string, string> = {
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active",
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active",
@@ -26,7 +26,7 @@ const shownGauges: { [key: string]: string } = {
}
export function MetricsPage() {
const query = useAsync(() => client.admin.getMetrics(), [])
const query = useAsync(async () => await client.admin.getMetrics(), [])
if (!query.result) return <Loader />
const { meters, gauges, timers } = query.result.data

View File

@@ -118,7 +118,7 @@ export function AboutPage() {
<KeyboardShortcutsHelp />
</Section>
<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>
</Anchor>
</Section>

View File

@@ -7,7 +7,7 @@ import { Constants } from "app/constants"
import { redirectToRootCategory, redirectToSelectedSource } from "app/slices/redirect"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store"
import { CategoryModificationRequest } from "app/types"
import { type CategoryModificationRequest } from "app/types"
import { flattenCategoryTree } from "app/utils"
import { Alert } from "components/Alert"
import { CategorySelect } from "components/content/add/CategorySelect"
@@ -23,7 +23,7 @@ export function CategoryDetailsPage() {
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
const dispatch = useAppDispatch()
const query = useAsync(() => client.category.getRoot(), [])
const query = useAsync(async () => await client.category.getRoot(), [])
const category =
id === Constants.categories.starred.id
? Constants.categories.starred
@@ -47,7 +47,7 @@ export function CategoryDetailsPage() {
const openDeleteCategoryModal = () => {
const categoryName = category?.name
return openConfirmModal({
openConfirmModal({
title: <Trans>Delete Category</Trans>,
children: (
<Text size="sm">
@@ -58,7 +58,7 @@ export function CategoryDetailsPage() {
),
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
confirmProps: { color: "red" },
onConfirm: () => deleteCategory.execute({ id: +id }),
onConfirm: async () => await deleteCategory.execute({ id: +id }),
})
}
@@ -122,7 +122,7 @@ export function CategoryDetailsPage() {
)}
<Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
{editable && (

View File

@@ -6,7 +6,7 @@ import { client, errorToStrings } from "app/client"
import { redirectToRootCategory, redirectToSelectedSource } from "app/slices/redirect"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store"
import { FeedModificationRequest } from "app/types"
import { type FeedModificationRequest } from "app/types"
import { Alert } from "components/Alert"
import { CategorySelect } from "components/content/add/CategorySelect"
import { Loader } from "components/Loader"
@@ -54,7 +54,7 @@ export function FeedDetailsPage() {
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
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 form = useForm<FeedModificationRequest>()
@@ -75,7 +75,7 @@ export function FeedDetailsPage() {
const openUnsubscribeModal = () => {
const feedName = feed?.name
return openConfirmModal({
openConfirmModal({
title: <Trans>Unsubscribe</Trans>,
children: (
<Text size="sm">
@@ -86,7 +86,7 @@ export function FeedDetailsPage() {
),
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
confirmProps: { color: "red" },
onConfirm: () => unsubscribe.execute({ id: +id }),
onConfirm: async () => await unsubscribe.execute({ id: +id }),
})
}
@@ -163,7 +163,7 @@ export function FeedDetailsPage() {
/>
<Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<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 { useViewportSize } from "@mantine/hooks"
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 { useAppDispatch, useAppSelector } from "app/store"
import { flattenCategoryTree } from "app/utils"

View File

@@ -28,7 +28,7 @@ import { useMobile } from "hooks/useMobile"
import { useWebSocket } from "hooks/useWebSocket"
import { LoadingPage } from "pages/LoadingPage"
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 { Outlet } from "react-router-dom"
@@ -79,7 +79,7 @@ const useStyles = createStyles((theme, props: LayoutProps) => ({
function LogoAndTitle() {
const dispatch = useAppDispatch()
return (
<Center inline onClick={() => dispatch(redirectToRootCategory())} style={{ cursor: "pointer" }}>
<Center inline onClick={async () => await dispatch(redirectToRootCategory())} style={{ cursor: "pointer" }}>
<Logo size={24} />
<Title order={3} pl="md">
CommaFeed
@@ -115,7 +115,7 @@ export default function Layout(props: LayoutProps) {
if (!webSocketConnected && treeReloadInterval) {
// 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)
@@ -133,7 +133,7 @@ export default function Layout(props: LayoutProps) {
)
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} />
</ActionIcon>
)

View File

@@ -32,7 +32,7 @@ export function TagDetailsPage() {
</Input.Wrapper>
<Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
</Group>

View File

@@ -4,7 +4,7 @@ import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
import { redirectToRootCategory } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { LoginRequest } from "app/types"
import { type LoginRequest } from "app/types"
import { Alert } from "components/Alert"
import { PageTitle } from "pages/PageTitle"
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 { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
import { PasswordResetRequest } from "app/types"
import { type PasswordResetRequest } from "app/types"
import { Alert } from "components/Alert"
import { PageTitle } from "pages/PageTitle"
import { useState } from "react"

View File

@@ -4,7 +4,7 @@ import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client"
import { redirectToRootCategory } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { RegistrationRequest } from "app/types"
import { type RegistrationRequest } from "app/types"
import { Alert } from "components/Alert"
import { PageTitle } from "pages/PageTitle"
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.frontend.auth.SecurityCheck;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.AdminSaveUserRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.google.common.base.Preconditions;
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")
@Timed
public Response adminSaveUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
@Parameter(required = true) UserModel userModel) {
Preconditions.checkNotNull(userModel);
Preconditions.checkNotNull(userModel.getName());
@Parameter(required = true) AdminSaveUserRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getName());
Long id = userModel.getId();
Long id = req.getId();
if (id == null) {
Preconditions.checkNotNull(userModel.getPassword());
Preconditions.checkNotNull(req.getPassword());
Set<Role> roles = Sets.newHashSet(Role.USER);
if (userModel.isAdmin()) {
if (req.isAdmin()) {
roles.add(Role.ADMIN);
}
try {
userService.register(userModel.getName(), userModel.getPassword(), userModel.getEmail(), roles, true);
userService.register(req.getName(), req.getPassword(), req.getEmail(), roles, true);
} catch (Exception e) {
return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
}
} 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();
}
User u = userDAO.findById(id);
u.setName(userModel.getName());
if (StringUtils.isNotBlank(userModel.getPassword())) {
u.setPassword(encryptionService.getEncryptedPassword(userModel.getPassword(), u.getSalt()));
u.setName(req.getName());
if (StringUtils.isNotBlank(req.getPassword())) {
u.setPassword(encryptionService.getEncryptedPassword(req.getPassword(), u.getSalt()));
}
u.setEmail(userModel.getEmail());
u.setDisabled(!userModel.isEnabled());
u.setEmail(req.getEmail());
u.setDisabled(!req.isEnabled());
userDAO.saveOrUpdate(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));
} else if (!userModel.isAdmin() && roles.contains(Role.ADMIN)) {
} else if (!req.isAdmin() && roles.contains(Role.ADMIN)) {
if (CommaFeedApplication.USERNAME_ADMIN.equals(u.getName())) {
return Response.status(Status.FORBIDDEN).entity("You cannot remove the admin role from the admin user.").build();
}