use 120 max line length for both server and client code (was 100 and 140)

This commit is contained in:
Athou
2026-11-03 06:48:11 +01:00
parent 29987a460d
commit 69cf674a02
157 changed files with 1228 additions and 1958 deletions

View File

@@ -4,7 +4,7 @@
"indentStyle": "space",
"indentWidth": 4,
"lineEnding": "lf",
"lineWidth": 140
"lineWidth": 120
},
"javascript": {
"formatter": {

View File

@@ -38,7 +38,8 @@ function Providers(
children: React.ReactNode
}>
) {
const primaryColor = useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
const primaryColor =
useAppSelector(state => state.user.settings?.primaryColor) || Constants.theme.defaultPrimaryColor
return (
<I18nProvider i18n={i18n}>
<MantineProvider
@@ -87,7 +88,10 @@ function AppRoutes() {
<Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
<Route path="passwordReset" element={<PasswordResetPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarVisible={sidebarVisible} />}>
<Route
path="app"
element={<Layout header={<Header />} sidebar={<Tree />} sidebarVisible={sidebarVisible} />}
>
<Route path="category">
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
<Route path=":id/details" element={<CategoryDetailsPage />} />

View File

@@ -61,7 +61,8 @@ export const client = {
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 }),
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),
@@ -76,7 +77,8 @@ export const client = {
feed: {
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 }),
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"),
@@ -107,7 +109,8 @@ export const client = {
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
initialSetup: async (req: InitialSetupRequest) => await axiosInstance.post("user/initialSetup", req),
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
passwordResetCallback: async (req: PasswordResetConfirmationRequest) => await axiosInstance.post("user/passwordResetCallback", req),
passwordResetCallback: async (req: PasswordResetConfirmationRequest) =>
await axiosInstance.post("user/passwordResetCallback", req),
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings),
sendTestPushNotification: async (settings: PushNotificationSettings) =>
@@ -156,9 +159,13 @@ function isCommaFeedApplicationError(err: AxiosError): err is AxiosError<CommaFe
}
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
return (
!!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
)
}
function isMessageArrayError(err: AxiosError): err is AxiosError<{ errors: string[] }> {
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "errors" in err.response.data
return (
!!err.response && !!err.response.data && typeof err.response.data === "object" && "errors" in err.response.data
)
}

View File

@@ -1,6 +1,14 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { Constants } from "@/app/constants"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "@/app/entries/thunks"
import {
loadEntries,
loadMoreEntries,
markAllEntries,
markEntry,
markMultipleEntries,
starEntry,
tagEntry,
} from "@/app/entries/thunks"
import type { Entry } from "@/app/types"
export type EntrySourceType = "category" | "feed" | "tag"
@@ -85,12 +93,16 @@ export const entriesSlice = createSlice({
}
})
builder.addCase(markAllEntries.pending, (state, action) => {
for (const e of state.entries.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))) {
for (const e of state.entries.filter(e =>
action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true
)) {
e.read = true
}
})
builder.addCase(starEntry.pending, (state, action) => {
for (const e of state.entries.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)) {
for (const e of state.entries.filter(
e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId
)) {
e.starred = action.meta.arg.starred
}
})

View File

@@ -40,7 +40,9 @@ export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_,
const state = thunkApi.getState()
const { source } = state.entries
const offset =
state.user.settings?.readingMode === "all" || state.entries.search || (source.type === "category" && source.id === "starred")
state.user.settings?.readingMode === "all" ||
state.entries.search ||
(source.type === "category" && source.id === "starred")
? state.entries.entries.length
: state.entries.entries.filter(e => !e.read).length
const endpoint = getEndpoint(state.entries.source.type)
@@ -220,7 +222,9 @@ export const selectEntry = createAppAsyncThunk(
const entryIndex = state.entries.entries.indexOf(entry)
const entriesToKeepOnTopWhenScrolling =
viewMode === "expanded" ? 0 : Math.min(state.user.settings?.entriesToKeepOnTopWhenScrolling ?? 0, entryIndex)
viewMode === "expanded"
? 0
: Math.min(state.user.settings?.entriesToKeepOnTopWhenScrolling ?? 0, entryIndex)
const entryToScrollTo = state.entries.entries[entryIndex - entriesToKeepOnTopWhenScrolling]
const entryElement = document.getElementById(Constants.dom.entryId(entry))
@@ -228,7 +232,8 @@ export const selectEntry = createAppAsyncThunk(
if (entryElement && entryElementToScrollTo) {
const scrollMode = state.user.settings?.scrollMode
const entryEntirelyVisible =
Constants.layout.isTopVisible(entryElementToScrollTo) && Constants.layout.isBottomVisible(entryElement)
Constants.layout.isTopVisible(entryElementToScrollTo) &&
Constants.layout.isBottomVisible(entryElement)
if (scrollMode === "always" || (scrollMode === "if_needed" && !entryEntirelyVisible)) {
const scrollSpeed = state.user.settings?.scrollSpeed
const margin = viewMode === "detailed" ? 8 : 3
@@ -242,7 +247,12 @@ export const selectEntry = createAppAsyncThunk(
}
)
const scrollToEntry = (entryElement: HTMLElement, margin: number, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
const scrollToEntry = (
entryElement: HTMLElement,
margin: number,
scrollSpeed: number | undefined,
onScrollEnded: () => void
) => {
const header = document.getElementsByTagName("header").item(0)?.getBoundingClientRect()
const offset = (header?.bottom ?? 0) + margin
scrollToWithCallback({

View File

@@ -2,11 +2,17 @@ import { createAppAsyncThunk } from "@/app/async-thunk"
import { Constants } from "@/app/constants"
import { redirectTo } from "@/app/redirect/slice"
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/login"))
)
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/register"))
)
export const redirectToInitialSetup = createAppAsyncThunk("redirect/initialSetup", (_, thunkApi) => thunkApi.dispatch(redirectTo("/setup")))
export const redirectToInitialSetup = createAppAsyncThunk("redirect/initialSetup", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/setup"))
)
export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", () => {
window.location.href = "api-documentation/"
@@ -38,15 +44,21 @@ export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details"
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
)
export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/tag/${id}`))
)
export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
)
export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/add"))
)
export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/settings"))
)
export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/users"))
@@ -56,6 +68,10 @@ export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
)
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/donate"))
)
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/about"))
)

View File

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

View File

@@ -41,4 +41,5 @@ export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useShallowEqualAppSelector: TypedUseSelectorHook<RootState> = selector => useSelector(selector, shallowEqual)
export const useShallowEqualAppSelector: TypedUseSelectorHook<RootState> = selector =>
useSelector(selector, shallowEqual)

View File

@@ -6,7 +6,10 @@ import { incrementUnreadCount } from "@/app/tree/slice"
import type { CollapseRequest, Subscription } from "@/app/types"
import { flattenCategoryTree, visitCategoryTree } from "@/app/utils"
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await 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",

View File

@@ -2,14 +2,26 @@ import { createAppAsyncThunk } from "@/app/async-thunk"
import { client } from "@/app/client"
import type { Settings } from "@/app/types"
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await 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", async () => await 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", async () => await client.entry.getTags().then(r => r.data))
export const reloadTags = createAppAsyncThunk(
"entries/tags",
async () => await client.entry.getTags().then(r => r.data)
)
export const changeSettings = createAppAsyncThunk("settings/change", async (newSettings: Partial<Settings>, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
await client.user.saveSettings({ ...settings, ...newSettings })
})
export const changeSettings = createAppAsyncThunk(
"settings/change",
async (newSettings: Partial<Settings>, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
await client.user.saveSettings({ ...settings, ...newSettings })
}
)

View File

@@ -43,13 +43,27 @@ export function categoryHasNewEntries(category?: TreeCategory): boolean {
.some(f => f.hasNewEntries)
}
export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => {
export const calculatePlaceholderSize = ({
width,
height,
maxWidth,
}: {
width?: number
height?: number
maxWidth: number
}) => {
const placeholderWidth = width && Math.min(width, maxWidth)
const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height
return { width: placeholderWidth, height: placeholderHeight }
}
export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => {
export const scrollToWithCallback = ({
options,
onScrollEnded,
}: {
options: ScrollToOptions
onScrollEnded: () => void
}) => {
const offset = (options.top ?? 0).toFixed()
const onScroll = throttle(100, () => {

View File

@@ -1,6 +1,14 @@
import type { MessageDescriptor } from "@lingui/core"
import { useLingui } from "@lingui/react"
import { ActionIcon, type ActionIconVariant, Box, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
import {
ActionIcon,
type ActionIconVariant,
Box,
Button,
type ButtonVariant,
Tooltip,
useMantineTheme,
} from "@mantine/core"
import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
import { Constants } from "@/app/constants"
import { useActionButton } from "@/hooks/useActionButton"

View File

@@ -14,7 +14,9 @@ export function MarkAllAsReadConfirmationDialog() {
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
const search = useAppSelector(state => state.entries.search)
const markAllAsReadNavigateToNextUnread = useAppSelector(state => state.user.settings?.markAllAsReadNavigateToNextUnread)
const markAllAsReadNavigateToNextUnread = useAppSelector(
state => state.user.settings?.markAllAsReadNavigateToNextUnread
)
const dispatch = useAppDispatch()
const onConfirm = async () => {
@@ -33,7 +35,8 @@ export function MarkAllAsReadConfirmationDialog() {
)
const isAllCategorySelected = source.type === "category" && source.id === Constants.categories.all.id
if (markAllAsReadNavigateToNextUnread && !isAllCategorySelected) await dispatch(selectNextUnreadTreeItem({ direction: "forward" }))
if (markAllAsReadNavigateToNextUnread && !isAllCategorySelected)
await dispatch(selectNextUnreadTreeItem({ direction: "forward" }))
}
return (
@@ -51,7 +54,8 @@ export function MarkAllAsReadConfirmationDialog() {
)}
{threshold > 0 && (
<Trans>
Are you sure you want to mark entries older than {threshold} days of <Code>{sourceLabel}</Code> as read?
Are you sure you want to mark entries older than {threshold} days of{" "}
<Code>{sourceLabel}</Code> as read?
</Trans>
)}
</Text>

View File

@@ -15,5 +15,12 @@ export const ReceivePushNotificationsChechbox = (props: CheckboxProps) => {
description = <Trans>Push notifications are not configured in your user settings.</Trans>
}
return <Checkbox label={<Trans>Receive push notifications</Trans>} disabled={disabled} description={description} {...props} />
return (
<Checkbox
label={<Trans>Receive push notifications</Trans>}
disabled={disabled}
description={description}
{...props}
/>
)
}

View File

@@ -34,7 +34,11 @@ export function UserEdit(props: Readonly<UserEditProps>) {
<form onSubmit={form.onSubmit(saveUser.execute)}>
<Stack>
<TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required />
<PasswordInput label={<Trans>Password</Trans>} {...form.getInputProps("password")} required={!props.user} />
<PasswordInput
label={<Trans>Password</Trans>}
{...form.getInputProps("password")}
required={!props.user}
/>
<TextInput type="email" label={<Trans>E-mail</Trans>} {...form.getInputProps("email")} />
<Checkbox label={<Trans>Admin</Trans>} {...form.getInputProps("admin", { type: "checkbox" })} />
<Checkbox label={<Trans>Enabled</Trans>} {...form.getInputProps("enabled", { type: "checkbox" })} />

View File

@@ -15,7 +15,9 @@ describe("Content component", () => {
})
it("renders iframe tag when included in content", () => {
const { container } = render(<Content content='<iframe src="https://example.com"></iframe>' />, { wrapper: MantineProvider })
const { container } = render(<Content content='<iframe src="https://example.com"></iframe>' />, {
wrapper: MantineProvider,
})
expect(container.querySelector("iframe")).toHaveAttribute("src", "https://example.com")
})

View File

@@ -1,6 +1,14 @@
import { Box, Mark } from "@mantine/core"
import escapeStringRegexp from "escape-string-regexp"
import { ALLOWED_TAG_LIST, type ChildrenNode, Interweave, Matcher, type MatchResponse, type Node, type TransformCallback } from "interweave"
import {
ALLOWED_TAG_LIST,
type ChildrenNode,
Interweave,
Matcher,
type MatchResponse,
type Node,
type TransformCallback,
} from "interweave"
import React from "react"
import styleToObject from "style-to-object"
import { Constants } from "@/app/constants"

View File

@@ -300,7 +300,9 @@ export function FeedEntries() {
entry={entry}
expanded={!!entry.expanded || viewMode === "expanded"}
selected={entry.id === selectedEntryId}
showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
showSelectionIndicator={
entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")
}
maxWidth={sidebarVisible ? Constants.layout.entryMaxWidth : undefined}
onHeaderClick={event => headerClicked(entry, event)}
onHeaderRightClick={event => headerRightClicked(entry, event)}

View File

@@ -34,67 +34,80 @@ const useStyles = tss
maxWidth?: number
fontSizePercentage: number
}>()
.create(({ theme, colorScheme, read, expanded, viewMode, rtl, showSelectionIndicator, maxWidth, fontSizePercentage }) => {
let backgroundColor: string
if (colorScheme === "dark") {
backgroundColor = read ? "inherit" : theme.colors.dark[5]
} else {
backgroundColor = read && !expanded ? theme.colors.gray[0] : "inherit"
}
.create(
({
theme,
colorScheme,
read,
expanded,
viewMode,
rtl,
showSelectionIndicator,
maxWidth,
fontSizePercentage,
}) => {
let backgroundColor: string
if (colorScheme === "dark") {
backgroundColor = read ? "inherit" : theme.colors.dark[5]
} else {
backgroundColor = read && !expanded ? theme.colors.gray[0] : "inherit"
}
let marginY = 10
if (viewMode === "title") {
marginY = 2
} else if (viewMode === "cozy") {
marginY = 6
}
let marginY = 10
if (viewMode === "title") {
marginY = 2
} else if (viewMode === "cozy") {
marginY = 6
}
let mobileMarginY = 6
if (viewMode === "title") {
mobileMarginY = 2
} else if (viewMode === "cozy") {
mobileMarginY = 4
}
let mobileMarginY = 6
if (viewMode === "title") {
mobileMarginY = 2
} else if (viewMode === "cozy") {
mobileMarginY = 4
}
let backgroundHoverColor = backgroundColor
if (!expanded && !read) {
backgroundHoverColor = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
}
let backgroundHoverColor = backgroundColor
if (!expanded && !read) {
backgroundHoverColor = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
}
let paperBorderLeftColor = ""
if (showSelectionIndicator) {
const borderLeftColor = colorScheme === "dark" ? theme.colors[theme.primaryColor][4] : theme.colors[theme.primaryColor][6]
paperBorderLeftColor = `${borderLeftColor} !important`
}
let paperBorderLeftColor = ""
if (showSelectionIndicator) {
const borderLeftColor =
colorScheme === "dark" ? theme.colors[theme.primaryColor][4] : theme.colors[theme.primaryColor][6]
paperBorderLeftColor = `${borderLeftColor} !important`
}
return {
paper: {
backgroundColor,
borderLeftColor: paperBorderLeftColor,
marginTop: marginY,
marginBottom: marginY,
[`@media (max-width: ${Constants.layout.mobileBreakpoint}px)`]: {
marginTop: mobileMarginY,
marginBottom: mobileMarginY,
},
"@media (hover: hover)": {
"&:hover": {
backgroundColor: backgroundHoverColor,
return {
paper: {
backgroundColor,
borderLeftColor: paperBorderLeftColor,
marginTop: marginY,
marginBottom: marginY,
[`@media (max-width: ${Constants.layout.mobileBreakpoint}px)`]: {
marginTop: mobileMarginY,
marginBottom: mobileMarginY,
},
"@media (hover: hover)": {
"&:hover": {
backgroundColor: backgroundHoverColor,
},
},
},
},
headerLink: {
fontSize: `${fontSizePercentage}%`,
color: "inherit",
textDecoration: "none",
},
body: {
fontSize: `${fontSizePercentage}%`,
direction: rtl ? "rtl" : "ltr",
maxWidth: maxWidth ?? "100%",
},
headerLink: {
fontSize: `${fontSizePercentage}%`,
color: "inherit",
textDecoration: "none",
},
body: {
fontSize: `${fontSizePercentage}%`,
direction: rtl ? "rtl" : "ltr",
maxWidth: maxWidth ?? "100%",
},
}
}
})
)
export function FeedEntry(props: Readonly<FeedEntryProps>) {
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
@@ -116,7 +129,9 @@ export function FeedEntry(props: Readonly<FeedEntryProps>) {
const showExternalLinkIcon =
externalLinkDisplayMode && ["always", mobile ? "on_mobile" : "on_desktop"].includes(externalLinkDisplayMode)
const showStarIcon =
props.entry.markable && starIconDisplayMode && ["always", mobile ? "on_mobile" : "on_desktop"].includes(starIconDisplayMode)
props.entry.markable &&
starIconDisplayMode &&
["always", mobile ? "on_mobile" : "on_desktop"].includes(starIconDisplayMode)
const swipeHandlers = useSwipeable({
onSwipedLeft: props.onSwipedLeft,

View File

@@ -35,7 +35,12 @@ export function FeedEntryContextMenu(props: Readonly<FeedEntryContextMenuProps>)
const { openLinkInBackgroundTab } = useBrowserExtension()
return (
<Menu id={Constants.dom.entryContextMenuId(props.entry)} theme={colorScheme} animation={false} className={classes.menu}>
<Menu
id={Constants.dom.entryContextMenuId(props.entry)}
theme={colorScheme}
animation={false}
className={classes.menu}
>
<Item
onClick={() => {
window.open(props.entry.url, "_blank", "noreferrer")
@@ -63,13 +68,19 @@ export function FeedEntryContextMenu(props: Readonly<FeedEntryContextMenuProps>)
{props.entry.markable && (
<>
<Item onClick={async () => await 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={async () => await 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 ? <TbMail size={iconSize} /> : <TbMailOpened size={iconSize} />}
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}

View File

@@ -1,7 +1,16 @@
import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react"
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import {
TbArrowBarToDown,
TbExternalLink,
TbMail,
TbMailOpened,
TbShare,
TbStar,
TbStarOff,
TbTag,
} from "react-icons/tb"
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "@/app/entries/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store"
import type { Entry } from "@/app/types"
@@ -71,7 +80,12 @@ export function FeedEntryFooter(props: Readonly<FeedEntryFooterProps>) {
{tags && (
<Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target>
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
<Indicator
label={props.entry.tags.length}
disabled={props.entry.tags.length === 0}
inline
size={16}
>
<ActionButton icon={<TbTag size={18} />} label={msg`Tags`} />
</Indicator>
</Popover.Target>

View File

@@ -104,7 +104,9 @@ export function ShareButtons(
}>
) {
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const enabledSharingSites = (Object.keys(Constants.sharing) as Array<keyof SharingSettings>).filter(site => sharingSettings?.[site])
const enabledSharingSites = (Object.keys(Constants.sharing) as Array<keyof SharingSettings>).filter(
site => sharingSettings?.[site]
)
const url = encodeURIComponent(props.url)
const desc = encodeURIComponent(props.description)
const clipboardAvailable = typeof navigator.clipboard !== "undefined"
@@ -119,7 +121,9 @@ export function ShareButtons(
{showNativeSection && (
<SimpleGrid cols={4}>
{clipboardAvailable && <CopyUrlButton url={props.url} />}
{nativeSharingAvailable && <BrowserNativeShareButton url={props.url} description={props.description} />}
{nativeSharingAvailable && (
<BrowserNativeShareButton url={props.url} description={props.description} />
)}
</SimpleGrid>
)}

View File

@@ -36,7 +36,12 @@ export function AddCategory() {
<form onSubmit={form.onSubmit(addCategory.execute)}>
<Stack>
<TextInput label={<Trans>Category</Trans>} placeholder={_(msg`Category`)} {...form.getInputProps("name")} required />
<TextInput
label={<Trans>Category</Trans>}
placeholder={_(msg`Category`)}
{...form.getInputProps("name")}
required
/>
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
<Group justify="center">
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>

View File

@@ -44,8 +44,8 @@ export function ImportOpml() {
placeholder={_(msg`OPML file`)}
description={
<Trans>
An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your
data from other feed reading services.
An opml file is an XML file containing feed URLs and categories. You can get an OPML
file by exporting your data from other feed reading services.
</Trans>
}
{...form.getInputProps("file")}

View File

@@ -85,8 +85,8 @@ export function Subscribe() {
placeholder="https://www.mysite.com/rss"
description={
<Trans>
The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed
will try to find the feed in the page.
The URL for the feed you want to subscribe to. You can also use the website's url
directly and CommaFeed will try to find the feed in the page.
</Trans>
}
required
@@ -101,8 +101,17 @@ export function Subscribe() {
>
<Stack>
<TextInput label={<Trans>Feed URL</Trans>} {...step1Form.getInputProps("url")} disabled />
<TextInput label={<Trans>Feed name</Trans>} {...step1Form.getInputProps("title")} required autoFocus />
<CategorySelect label={<Trans>Category</Trans>} {...step1Form.getInputProps("categoryId")} clearable />
<TextInput
label={<Trans>Feed name</Trans>}
{...step1Form.getInputProps("title")}
required
autoFocus
/>
<CategorySelect
label={<Trans>Category</Trans>}
{...step1Form.getInputProps("categoryId")}
clearable
/>
</Stack>
</Stepper.Step>
</Stepper>
@@ -117,7 +126,11 @@ export function Subscribe() {
</Button>
)}
{activeStep === 1 && (
<Button type="submit" leftSection={<TbRss size={16} />} loading={fetchFeed.loading || subscribe.loading}>
<Button
type="submit"
leftSection={<TbRss size={16} />}
loading={fetchFeed.loading || subscribe.loading}
>
<Trans>Subscribe</Trans>
</Button>
)}

View File

@@ -26,7 +26,16 @@ const fields: Field[] = [
{ name: "categoriesLower", label: "Categories (lower case)" },
]
const textOperators = new Set(["=", "!=", "contains", "beginsWith", "endsWith", "doesNotContain", "doesNotBeginWith", "doesNotEndWith"])
const textOperators = new Set([
"=",
"!=",
"contains",
"beginsWith",
"endsWith",
"doesNotContain",
"doesNotBeginWith",
"doesNotEndWith",
])
function toCelString(query: RuleGroupType): string {
if (query.rules.length === 0) {

View File

@@ -24,7 +24,10 @@ export function Star(
}
return (
<Tooltip label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} openDelay={Constants.tooltip.delay}>
<Tooltip
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
openDelay={Constants.tooltip.delay}
>
<ActionIcon variant="transparent" onClick={onClick}>
{props.entry.starred ? <TbStarFilled size={18} /> : <TbStar size={18} />}
</ActionIcon>

View File

@@ -17,7 +17,13 @@ import {
TbSortDescending,
TbUser,
} from "react-icons/tb"
import { markAllAsReadWithConfirmationIfRequired, reloadEntries, search, selectNextEntry, selectPreviousEntry } from "@/app/entries/thunks"
import {
markAllAsReadWithConfirmationIfRequired,
reloadEntries,
search,
selectNextEntry,
selectPreviousEntry,
} from "@/app/entries/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store"
import { changeSettings } from "@/app/user/thunks"
import { ActionButton } from "@/components/ActionButton"
@@ -143,7 +149,13 @@ export function Header() {
onClick={toggleReadingMode}
/>
<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" ? msg`Asc` : msg`Desc`}
onClick={toggleReadingOrder}
/>
@@ -160,7 +172,11 @@ export function Header() {
placeholder={_(msg`Search`)}
{...searchForm.getInputProps("search")}
leftSection={<TbSearch size={iconSize} />}
rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />}
rightSection={
<CloseButton
onClick={async () => await (searchFromStore && dispatch(search("")))}
/>
}
autoFocus
/>
</form>

View File

@@ -31,7 +31,13 @@ import {
} from "react-icons/tb"
import { throttle } from "throttle-debounce"
import { client } from "@/app/client"
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "@/app/redirect/thunks"
import {
redirectToAbout,
redirectToAdminUsers,
redirectToDonate,
redirectToMetrics,
redirectToSettings,
} from "@/app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store"
import type { ViewMode } from "@/app/types"
import { setFontSizePercentage, setViewMode } from "@/app/user/slice"
@@ -124,7 +130,13 @@ export function ProfileMenu(props: Readonly<ProfileMenuProps>) {
}
return (
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened} preventPositionChangeWhenVisible={false}>
<Menu
position="bottom-end"
closeOnItemClick={false}
opened={opened}
onChange={setOpened}
preventPositionChangeWhenVisible={false}
>
<Menu.Target>{props.control}</Menu.Target>
<Menu.Dropdown>
{profile && <Menu.Label>{profile.name}</Menu.Label>}
@@ -164,7 +176,9 @@ export function ProfileMenu(props: Readonly<ProfileMenuProps>) {
}}
>
<Trans>Fetch all my feeds now</Trans>
{!forceRefreshEnabled && <span> ({dayjs.duration(nextAvailableForceRefresh - now.getTime()).format("HH:mm:ss")})</span>}
{!forceRefreshEnabled && (
<span> ({dayjs.duration(nextAvailableForceRefresh - now.getTime()).format("HH:mm:ss")})</span>
)}
</Menu.Item>
<Divider />

View File

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

View File

@@ -111,7 +111,11 @@ export function DisplaySettings() {
/>
<Switch
label={<Trans>Navigate to the next category/feed with unread entries when marking all entries as read</Trans>}
label={
<Trans>
Navigate to the next category/feed with unread entries when marking all entries as read
</Trans>
}
checked={settings.markAllAsReadNavigateToNextUnread}
onChange={e => dispatch(changeSettings({ markAllAsReadNavigateToNextUnread: e.currentTarget.checked }))}
/>

View File

@@ -1,6 +1,18 @@
import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro"
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
import {
Anchor,
Box,
Button,
Checkbox,
Divider,
Group,
Input,
PasswordInput,
Stack,
Text,
TextInput,
} from "@mantine/core"
import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals"
import { useEffect } from "react"
@@ -91,8 +103,9 @@ export function ProfileSettings() {
label={<Trans>API key</Trans>}
description={
<Trans>
This is your API key. It can be used for some read-only API operations and grants access to the Fever API
and the Google Reader API. Use the form at the bottom of the page to generate a new API key
This is your API key. It can be used for some read-only API operations and grants access
to the Fever API and the Google Reader API. Use the form at the bottom of the page to
generate a new API key
</Trans>
}
readOnly
@@ -103,7 +116,8 @@ export function ProfileSettings() {
label={<Trans>OPML export</Trans>}
description={
<Trans>
Export your subscriptions and categories as an OPML file that can be imported in other feed reading services
Export your subscriptions and categories as an OPML file that can be imported in other
feed reading services
</Trans>
}
>
@@ -118,8 +132,8 @@ export function ProfileSettings() {
label={<Trans>Fever API</Trans>}
description={
<Trans>
CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client.
Login with your username and your <u>API key</u>.
CommaFeed is compatible with the Fever API. Use the following URL in your
Fever-compatible mobile client. Login with your username and your <u>API key</u>.
</Trans>
}
>
@@ -134,8 +148,8 @@ export function ProfileSettings() {
label={<Trans>Google Reader API</Trans>}
description={
<Trans>
CommaFeed is compatible with the Google Reader API. Use the following URL in your Google Reader-compatible
mobile client. Login with your username and your <u>API key</u>.
CommaFeed is compatible with the Google Reader API. Use the following URL in your Google
Reader-compatible mobile client. Login with your username and your <u>API key</u>.
</Trans>
}
>
@@ -165,8 +179,14 @@ export function ProfileSettings() {
description={<Trans>Changing password will generate a new API key</Trans>}
{...form.getInputProps("newPassword")}
/>
<PasswordInput label={<Trans>Confirm password</Trans>} {...form.getInputProps("newPasswordConfirmation")} />
<Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} />
<PasswordInput
label={<Trans>Confirm password</Trans>}
{...form.getInputProps("newPasswordConfirmation")}
/>
<Checkbox
label={<Trans>Generate new API key</Trans>}
{...form.getInputProps("newApiKey", { type: "checkbox" })}
/>
<Group>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>

View File

@@ -50,8 +50,8 @@ export function PushNotificationSettings() {
label={<Trans>Push notification service</Trans>}
description={
<Trans>
Receive push notifications when new feed entries are discovered. Enable "Receive push notifications" in the
settings of each feed for which you want to receive notifications.
Receive push notifications when new feed entries are discovered. Enable "Receive push
notifications" in the settings of each feed for which you want to receive notifications.
</Trans>
}
data={[
@@ -77,7 +77,12 @@ export function PushNotificationSettings() {
required
{...form.getInputProps("serverUrl")}
/>
<TextInput label={<Trans>Topic</Trans>} placeholder="commafeed" required {...form.getInputProps("topic")} />
<TextInput
label={<Trans>Topic</Trans>}
placeholder="commafeed"
required
{...form.getInputProps("topic")}
/>
<TextInput label={<Trans>Access token</Trans>} {...form.getInputProps("userSecret")} />
</>
)}

View File

@@ -119,8 +119,11 @@ export function Tree() {
const categoryNode = (category: Category, level = 0) => {
if (!isCategoryDisplayed(category)) return null
const hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold))
const hasWarning = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => !!f.filterLegacy))
const hasError =
!category.expanded &&
flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold))
const hasWarning =
!category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => !!f.filterLegacy))
return (
<TreeNode
id={category.id}

View File

@@ -80,7 +80,11 @@ export function TreeNode(props: Readonly<TreeNodeProps>) {
data-type={props.type}
data-unread-count={props.unread}
>
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)} className="cf-treenode-icon">
<Box
mr={6}
onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}
className="cf-treenode-icon"
>
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
</Box>
<Box className={classes.nodeText}>{props.name}</Box>

View File

@@ -1,10 +1,26 @@
import { msg } from "@lingui/core/macro"
import { Anchor, Box, Center, Container, Divider, Group, Image, Space, Title, useMantineColorScheme } from "@mantine/core"
import {
Anchor,
Box,
Center,
Container,
Divider,
Group,
Image,
Space,
Title,
useMantineColorScheme,
} from "@mantine/core"
import { useAsyncCallback } from "react-async-hook"
import { SiGithub, SiX } from "react-icons/si"
import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb"
import { client } from "@/app/client"
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "@/app/redirect/thunks"
import {
redirectToApiDocumentation,
redirectToLogin,
redirectToRegistration,
redirectToRootCategory,
} from "@/app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store"
import welcomePageDark from "@/assets/welcome_page_dark.png"
import welcomePageLight from "@/assets/welcome_page_light.png"

View File

@@ -69,7 +69,11 @@ export function AdminUsersPage() {
<Title order={3} mb="md">
<Group>
<Trans>Manage users</Trans>
<ActionIcon color={theme.primaryColor} variant="subtle" onClick={() => openUserEditModal(<Trans>Add user</Trans>)}>
<ActionIcon
color={theme.primaryColor}
variant="subtle"
onClick={() => openUserEditModal(<Trans>Add user</Trans>)}
>
<TbPlus size={20} />
</ActionIcon>
</Group>

View File

@@ -19,8 +19,10 @@ const shownGauges: Record<string, string> = {
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Feed Refresh Engine queue size",
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Refresh Engine active HTTP workers",
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Refresh Engine active database update workers",
"com.commafeed.backend.feed.FeedRefreshEngine.notifier.active": "Feed Refresh Engine active push notifications workers",
"com.commafeed.backend.feed.FeedRefreshEngine.notifier.queue": "Feed Refresh Engine queued push notifications workers",
"com.commafeed.backend.feed.FeedRefreshEngine.notifier.active":
"Feed Refresh Engine active push notifications workers",
"com.commafeed.backend.feed.FeedRefreshEngine.notifier.queue":
"Feed Refresh Engine queued push notifications workers",
"com.commafeed.backend.HttpGetter.cache.size": "HttpGetter cached entries",
"com.commafeed.backend.HttpGetter.cache.memoryUsage": "HttpGetter cache memory usage",
"com.commafeed.frontend.ws.WebSocketSessions.users": "WebSocket users",

View File

@@ -51,7 +51,12 @@ function NextUnreadBookmarklet() {
return (
<Box>
<CategorySelect value={categoryId} onChange={c => c && setCategoryId(c)} withAll description={<Trans>Category</Trans>} />
<CategorySelect
value={categoryId}
onChange={c => c && setCategoryId(c)}
withAll
description={<Trans>Category</Trans>}
/>
<NativeSelect
data={[
{ value: "desc", label: _(msg`Newest first`) },
@@ -73,7 +78,8 @@ function NextUnreadBookmarklet() {
export function AboutPage() {
const version = useAppSelector(state => state.server.serverInfos?.version)
const revision = useAppSelector(state => state.server.serverInfos?.gitCommit)
const { isBrowserExtensionInstalled, browserExtensionVersion, isBrowserExtensionInstallable } = useBrowserExtension()
const { isBrowserExtensionInstalled, browserExtensionVersion, isBrowserExtensionInstallable } =
useBrowserExtension()
const dispatch = useAppDispatch()
return (
@@ -100,7 +106,9 @@ export function AboutPage() {
</Trans>
</Box>
<Box>
<Trans>If you encounter an issue, please report it on the issues page of the GitHub project.</Trans>
<Trans>
If you encounter an issue, please report it on the issues page of the GitHub project.
</Trans>
</Box>
</Section>
<Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}>

View File

@@ -1,7 +1,21 @@
import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro"
import { Anchor, Box, Button, Code, Container, Divider, Group, Input, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core"
import {
Anchor,
Box,
Button,
Code,
Container,
Divider,
Group,
Input,
NumberInput,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core"
import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals"
import { useEffect } from "react"
@@ -130,7 +144,12 @@ export function CategoryDetailsPage() {
clearable
withoutCategoryIds={excludedCategoryIds}
/>
<NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} />
<NumberInput
label={<Trans>Position</Trans>}
{...form.getInputProps("position")}
required
min={0}
/>
</>
)}
@@ -140,7 +159,11 @@ export function CategoryDetailsPage() {
</Button>
{editable && (
<>
<Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={modifyCategory.loading}>
<Button
type="submit"
leftSection={<TbDeviceFloppy size={16} />}
loading={modifyCategory.loading}
>
<Trans>Save</Trans>
</Button>
<Divider orientation="vertical" />

View File

@@ -19,8 +19,8 @@ export function DonatePage() {
<Trans>
<Box>Hey,</Box>
<Box mt="xs">
I'm Jérémie from Belgium and I've been working on CommaFeed in my free time for over 10 years now. Thanks for taking
an interest in helping me continue supporting CommaFeed.
I'm Jérémie from Belgium and I've been working on CommaFeed in my free time for over 10 years
now. Thanks for taking an interest in helping me continue supporting CommaFeed.
</Box>
</Trans>

View File

@@ -133,7 +133,11 @@ export function FeedDetailsPage() {
<Input.Wrapper label={<Trans>Generated feed url</Trans>}>
<Box>
{apiKey && (
<Anchor href={`rest/feed/entriesAsFeed?id=${feed.id}&apiKey=${apiKey}`} target="_blank" rel="noreferrer">
<Anchor
href={`rest/feed/entriesAsFeed?id=${feed.id}&apiKey=${apiKey}`}
target="_blank"
rel="noreferrer"
>
<Trans>Link</Trans>
</Anchor>
)}
@@ -146,10 +150,16 @@ export function FeedDetailsPage() {
<TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required />
<CategorySelect label={<Trans>Category</Trans>} {...form.getInputProps("categoryId")} clearable />
<NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} />
<ReceivePushNotificationsChechbox {...form.getInputProps("pushNotificationsEnabled", { type: "checkbox" })} />
<ReceivePushNotificationsChechbox
{...form.getInputProps("pushNotificationsEnabled", { type: "checkbox" })}
/>
<NumberInput
label={<Trans>Auto-mark as read</Trans>}
description={<Trans>Mark entries in this feed as read after this number of days. Leave empty to disable.</Trans>}
description={
<Trans>
Mark entries in this feed as read after this number of days. Leave empty to disable.
</Trans>
}
suffix={` ${_(msg`days`)}`}
{...form.getInputProps("autoMarkAsReadAfterDays")}
min={1}
@@ -159,21 +169,25 @@ export function FeedDetailsPage() {
label={<Trans>Filtering expression</Trans>}
description={
<Trans>
Build a filter expression to indicate what you want to read. Entries that don't match will be marked as read
automatically.
Build a filter expression to indicate what you want to read. Entries that don't match
will be marked as read automatically.
</Trans>
}
>
{feed.filterLegacy && (
<MantineAlert color="yellow" icon={<TbAlertTriangle />}>
<Trans>
This feed has a legacy filter that cannot be edited and is not applied. Please recreate the filter using
the new expression editor. The legacy filter expression was: <Code>{feed.filterLegacy}</Code>
This feed has a legacy filter that cannot be edited and is not applied. Please
recreate the filter using the new expression editor. The legacy filter expression
was: <Code>{feed.filterLegacy}</Code>
</Trans>
</MantineAlert>
)}
<Box mt="xs">
<FilteringExpressionEditor initialValue={feed.filter} onChange={value => form.setFieldValue("filter", value)} />
<FilteringExpressionEditor
initialValue={feed.filter}
onChange={value => form.setFieldValue("filter", value)}
/>
</Box>
</Input.Wrapper>

View File

@@ -20,7 +20,8 @@ function NoSubscriptionHelp() {
<Box>
<Center>
<Trans>
You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?
You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of
the page?
</Trans>
</Center>
</Box>
@@ -51,7 +52,8 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
const viewport = useViewportSize()
const theme = useMantineTheme()
const noSubscriptions = useAppSelector(
state => state.tree.rootCategory && flattenCategoryTree(state.tree.rootCategory).every(c => c.feeds.length === 0)
state =>
state.tree.rootCategory && flattenCategoryTree(state.tree.rootCategory).every(c => c.feeds.length === 0)
)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const sourceWebsiteUrl = useAppSelector(state => state.entries.sourceWebsiteUrl)
@@ -63,7 +65,8 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
if (!root) return { unreadCount: 0, hasNewEntries: false }
if (props.sourceType === "category") {
const category = id === Constants.categories.all.id ? root : flattenCategoryTree(root).find(c => c.id === id)
const category =
id === Constants.categories.all.id ? root : flattenCategoryTree(root).find(c => c.id === id)
return {
unreadCount: categoryUnreadCount(category),
hasNewEntries: categoryHasNewEntries(category),

View File

@@ -50,8 +50,8 @@ export function InitialSetupPage() {
</Title>
<Box mb="md">
<Trans>
Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get
started.
Welcome! This appears to be the first time you're running CommaFeed. Please create an
administrator account to get started.
</Trans>
</Box>
{setup.error && (

View File

@@ -1,7 +1,19 @@
import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro"
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
import {
Anchor,
Box,
Button,
Center,
Container,
Group,
Paper,
PasswordInput,
Stack,
TextInput,
Title,
} from "@mantine/core"
import { useForm } from "@mantine/form"
import { useAsyncCallback } from "react-async-hook"
import { Link } from "react-router-dom"
@@ -49,7 +61,11 @@ export function LoginPage() {
placeholder={_(msg`User Name or E-mail`)}
{...form.getInputProps("name")}
description={
serverInfos?.demoAccountEnabled ? <Trans>Try out CommaFeed with the demo account: demo/demo</Trans> : ""
serverInfos?.demoAccountEnabled ? (
<Trans>Try out CommaFeed with the demo account: demo/demo</Trans>
) : (
""
)
}
size="md"
required

View File

@@ -1,7 +1,19 @@
import { msg } from "@lingui/core/macro"
import { useLingui } from "@lingui/react"
import { Trans } from "@lingui/react/macro"
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
import {
Anchor,
Box,
Button,
Center,
Container,
Group,
Paper,
PasswordInput,
Stack,
TextInput,
Title,
} from "@mantine/core"
import { useForm } from "@mantine/form"
import { useAsyncCallback } from "react-async-hook"
import { Link } from "react-router-dom"
@@ -71,7 +83,13 @@ export function RegistrationPage() {
<form onSubmit={form.onSubmit(register.execute)}>
<Stack>
<TextInput label="User Name" placeholder="User Name" {...form.getInputProps("name")} size="md" required />
<TextInput
label="User Name"
placeholder="User Name"
{...form.getInputProps("name")}
size="md"
required
/>
<TextInput
type="email"
label={<Trans>E-mail address</Trans>}