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

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