forked from Archives/Athou_commafeed
add button in the header to star entry (#1025)
This commit is contained in:
@@ -104,6 +104,9 @@ export const Constants = {
|
|||||||
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
||||||
entryContextMenuId: (entry: Entry) => entry.id,
|
entryContextMenuId: (entry: Entry) => entry.id,
|
||||||
},
|
},
|
||||||
|
tooltip: {
|
||||||
|
delay: 500,
|
||||||
|
},
|
||||||
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
||||||
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,13 +113,19 @@ export const markAllEntries = createAppAsyncThunk(
|
|||||||
thunkApi.dispatch(reloadTree())
|
thunkApi.dispatch(reloadTree())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
export const starEntry = createAppAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => {
|
export const starEntry = createAppAsyncThunk(
|
||||||
client.entry.star({
|
"entries/entry/star",
|
||||||
id: arg.entry.id,
|
(arg: { entry: Entry; starred: boolean }) => {
|
||||||
feedId: +arg.entry.feedId,
|
client.entry.star({
|
||||||
starred: arg.starred,
|
id: arg.entry.id,
|
||||||
})
|
feedId: +arg.entry.feedId,
|
||||||
})
|
starred: arg.starred,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred,
|
||||||
|
}
|
||||||
|
)
|
||||||
export const selectEntry = createAppAsyncThunk(
|
export const selectEntry = createAppAsyncThunk(
|
||||||
"entries/entry/select",
|
"entries/entry/select",
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
|
|||||||
|
|
||||||
export type ScrollMode = "always" | "never" | "if_needed"
|
export type ScrollMode = "always" | "never" | "if_needed"
|
||||||
|
|
||||||
|
export type IconDisplayMode = "always" | "never" | "on_desktop" | "on_mobile"
|
||||||
|
|
||||||
export interface AddCategoryRequest {
|
export interface AddCategoryRequest {
|
||||||
name: string
|
name: string
|
||||||
parentId?: string
|
parentId?: string
|
||||||
@@ -242,6 +244,8 @@ export interface Settings {
|
|||||||
customJs?: string
|
customJs?: string
|
||||||
scrollSpeed: number
|
scrollSpeed: number
|
||||||
scrollMode: ScrollMode
|
scrollMode: ScrollMode
|
||||||
|
starIconDisplayMode: IconDisplayMode
|
||||||
|
externalLinkIconDisplayMode: IconDisplayMode
|
||||||
markAllAsReadConfirmation: boolean
|
markAllAsReadConfirmation: boolean
|
||||||
customContextMenu: boolean
|
customContextMenu: boolean
|
||||||
mobileFooter: boolean
|
mobileFooter: boolean
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
|||||||
import { type Settings, type UserModel } from "app/types"
|
import { type Settings, type UserModel } from "app/types"
|
||||||
import {
|
import {
|
||||||
changeCustomContextMenu,
|
changeCustomContextMenu,
|
||||||
|
changeExternalLinkIconDisplayMode,
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
changeMarkAllAsReadConfirmation,
|
changeMarkAllAsReadConfirmation,
|
||||||
changeMobileFooter,
|
changeMobileFooter,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
changeScrollSpeed,
|
changeScrollSpeed,
|
||||||
changeSharingSetting,
|
changeSharingSetting,
|
||||||
changeShowRead,
|
changeShowRead,
|
||||||
|
changeStarIconDisplayMode,
|
||||||
reloadProfile,
|
reloadProfile,
|
||||||
reloadSettings,
|
reloadSettings,
|
||||||
reloadTags,
|
reloadTags,
|
||||||
@@ -69,6 +71,14 @@ export const userSlice = createSlice({
|
|||||||
if (!state.settings) return
|
if (!state.settings) return
|
||||||
state.settings.scrollMode = action.meta.arg
|
state.settings.scrollMode = action.meta.arg
|
||||||
})
|
})
|
||||||
|
builder.addCase(changeStarIconDisplayMode.pending, (state, action) => {
|
||||||
|
if (!state.settings) return
|
||||||
|
state.settings.starIconDisplayMode = action.meta.arg
|
||||||
|
})
|
||||||
|
builder.addCase(changeExternalLinkIconDisplayMode.pending, (state, action) => {
|
||||||
|
if (!state.settings) return
|
||||||
|
state.settings.externalLinkIconDisplayMode = action.meta.arg
|
||||||
|
})
|
||||||
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
|
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
|
||||||
if (!state.settings) return
|
if (!state.settings) return
|
||||||
state.settings.markAllAsReadConfirmation = action.meta.arg
|
state.settings.markAllAsReadConfirmation = action.meta.arg
|
||||||
@@ -92,6 +102,8 @@ export const userSlice = createSlice({
|
|||||||
changeShowRead.fulfilled,
|
changeShowRead.fulfilled,
|
||||||
changeScrollMarks.fulfilled,
|
changeScrollMarks.fulfilled,
|
||||||
changeScrollMode.fulfilled,
|
changeScrollMode.fulfilled,
|
||||||
|
changeStarIconDisplayMode.fulfilled,
|
||||||
|
changeExternalLinkIconDisplayMode.fulfilled,
|
||||||
changeMarkAllAsReadConfirmation.fulfilled,
|
changeMarkAllAsReadConfirmation.fulfilled,
|
||||||
changeCustomContextMenu.fulfilled,
|
changeCustomContextMenu.fulfilled,
|
||||||
changeMobileFooter.fulfilled,
|
changeMobileFooter.fulfilled,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createAppAsyncThunk } from "app/async-thunk"
|
import { createAppAsyncThunk } from "app/async-thunk"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { reloadEntries } from "app/entries/thunks"
|
import { reloadEntries } from "app/entries/thunks"
|
||||||
import type { ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types"
|
import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } 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))
|
||||||
@@ -43,6 +43,22 @@ export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scro
|
|||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, scrollMode })
|
client.user.saveSettings({ ...settings, scrollMode })
|
||||||
})
|
})
|
||||||
|
export const changeStarIconDisplayMode = createAppAsyncThunk(
|
||||||
|
"settings/starIconDisplayMode",
|
||||||
|
(starIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||||
|
const { settings } = thunkApi.getState().user
|
||||||
|
if (!settings) return
|
||||||
|
client.user.saveSettings({ ...settings, starIconDisplayMode })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export const changeExternalLinkIconDisplayMode = createAppAsyncThunk(
|
||||||
|
"settings/externalLinkIconDisplayMode",
|
||||||
|
(externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||||
|
const { settings } = thunkApi.getState().user
|
||||||
|
if (!settings) return
|
||||||
|
client.user.saveSettings({ ...settings, externalLinkIconDisplayMode })
|
||||||
|
}
|
||||||
|
)
|
||||||
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||||
"settings/markAllAsReadConfirmation",
|
"settings/markAllAsReadConfirmation",
|
||||||
(markAllAsReadConfirmation: boolean, thunkApi) => {
|
(markAllAsReadConfirmation: boolean, thunkApi) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
|
||||||
import { type ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
|
import { type ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
|
||||||
|
import { Constants } from "app/constants"
|
||||||
import { useActionButton } from "hooks/useActionButton"
|
import { useActionButton } from "hooks/useActionButton"
|
||||||
import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
|
import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((pr
|
|||||||
const variant = props.variant ?? "subtle"
|
const variant = props.variant ?? "subtle"
|
||||||
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
|
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
|
||||||
return iconOnly ? (
|
return iconOnly ? (
|
||||||
<Tooltip label={props.label} openDelay={500}>
|
<Tooltip label={props.label} openDelay={Constants.tooltip.delay}>
|
||||||
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
|
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
|
||||||
{props.icon}
|
{props.icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/macro"
|
||||||
import { Tooltip } from "@mantine/core"
|
import { Tooltip } from "@mantine/core"
|
||||||
|
import { Constants } from "app/constants"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export function RelativeDate(props: { date: Date | number | undefined }) {
|
|||||||
if (!props.date) return <Trans>N/A</Trans>
|
if (!props.date) return <Trans>N/A</Trans>
|
||||||
const date = dayjs(props.date)
|
const date = dayjs(props.date)
|
||||||
return (
|
return (
|
||||||
<Tooltip label={date.toDate().toLocaleString()} openDelay={500}>
|
<Tooltip label={date.toDate().toLocaleString()} openDelay={Constants.tooltip.delay}>
|
||||||
<span>{date.from(dayjs(now))}</span>
|
<span>{date.from(dayjs(now))}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
|
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
|
import { useAppSelector } from "app/store"
|
||||||
import { type Entry, type ViewMode } from "app/types"
|
import { type Entry, type ViewMode } from "app/types"
|
||||||
import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader"
|
import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader"
|
||||||
import { FeedEntryHeader } from "components/content/header/FeedEntryHeader"
|
import { FeedEntryHeader } from "components/content/header/FeedEntryHeader"
|
||||||
|
import { useMobile } from "hooks/useMobile"
|
||||||
import { useViewMode } from "hooks/useViewMode"
|
import { useViewMode } from "hooks/useViewMode"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useSwipeable } from "react-swipeable"
|
import { useSwipeable } from "react-swipeable"
|
||||||
@@ -103,6 +105,15 @@ export function FeedEntry(props: FeedEntryProps) {
|
|||||||
maxWidth: props.maxWidth,
|
maxWidth: props.maxWidth,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const externalLinkDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
|
||||||
|
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
|
||||||
|
const mobile = useMobile()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
const swipeHandlers = useSwipeable({
|
const swipeHandlers = useSwipeable({
|
||||||
onSwipedLeft: props.onSwipedLeft,
|
onSwipedLeft: props.onSwipedLeft,
|
||||||
})
|
})
|
||||||
@@ -147,8 +158,21 @@ export function FeedEntry(props: FeedEntryProps) {
|
|||||||
onContextMenu={props.onHeaderRightClick}
|
onContextMenu={props.onHeaderRightClick}
|
||||||
>
|
>
|
||||||
<Box px={paddingX} py={paddingY} {...swipeHandlers}>
|
<Box px={paddingX} py={paddingY} {...swipeHandlers}>
|
||||||
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
|
{compactHeader && (
|
||||||
{!compactHeader && <FeedEntryHeader entry={props.entry} expanded={props.expanded} />}
|
<FeedEntryCompactHeader
|
||||||
|
entry={props.entry}
|
||||||
|
showStarIcon={showStarIcon}
|
||||||
|
showExternalLinkIcon={showExternalLinkIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!compactHeader && (
|
||||||
|
<FeedEntryHeader
|
||||||
|
entry={props.entry}
|
||||||
|
expanded={props.expanded}
|
||||||
|
showStarIcon={showStarIcon}
|
||||||
|
showExternalLinkIcon={showExternalLinkIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</a>
|
</a>
|
||||||
{props.expanded && (
|
{props.expanded && (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Box, Text } from "@mantine/core"
|
|||||||
import { type Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||||
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
||||||
|
import { Star } from "components/content/header/Star"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
@@ -9,6 +10,8 @@ import { FeedEntryTitle } from "./FeedEntryTitle"
|
|||||||
|
|
||||||
export interface FeedEntryHeaderProps {
|
export interface FeedEntryHeaderProps {
|
||||||
entry: Entry
|
entry: Entry
|
||||||
|
showStarIcon?: boolean
|
||||||
|
showExternalLinkIcon?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = tss
|
const useStyles = tss
|
||||||
@@ -46,6 +49,7 @@ export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
|||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Box className={classes.wrapper}>
|
<Box className={classes.wrapper}>
|
||||||
|
{props.showStarIcon && <Star entry={props.entry} />}
|
||||||
<Box>
|
<Box>
|
||||||
<FeedFavicon url={props.entry.iconUrl} />
|
<FeedFavicon url={props.entry.iconUrl} />
|
||||||
</Box>
|
</Box>
|
||||||
@@ -62,7 +66,7 @@ export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
|||||||
<RelativeDate date={props.entry.date} />
|
<RelativeDate date={props.entry.date} />
|
||||||
</Text>
|
</Text>
|
||||||
</OnDesktop>
|
</OnDesktop>
|
||||||
<OpenExternalLink entry={props.entry} />
|
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Box, Space, Text } from "@mantine/core"
|
import { Box, Flex, Space, Text } from "@mantine/core"
|
||||||
import { type Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||||
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
||||||
|
import { Star } from "components/content/header/Star"
|
||||||
import { RelativeDate } from "components/RelativeDate"
|
import { RelativeDate } from "components/RelativeDate"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||||
@@ -9,6 +10,8 @@ import { FeedEntryTitle } from "./FeedEntryTitle"
|
|||||||
export interface FeedEntryHeaderProps {
|
export interface FeedEntryHeaderProps {
|
||||||
entry: Entry
|
entry: Entry
|
||||||
expanded: boolean
|
expanded: boolean
|
||||||
|
showStarIcon?: boolean
|
||||||
|
showExternalLinkIcon?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = tss
|
const useStyles = tss
|
||||||
@@ -17,16 +20,9 @@ const useStyles = tss
|
|||||||
}>()
|
}>()
|
||||||
.create(({ colorScheme, read }) => ({
|
.create(({ colorScheme, read }) => ({
|
||||||
main: {
|
main: {
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
},
|
|
||||||
mainText: {
|
|
||||||
fontWeight: colorScheme === "light" && !read ? "bold" : "inherit",
|
fontWeight: colorScheme === "light" && !read ? "bold" : "inherit",
|
||||||
},
|
},
|
||||||
details: {
|
details: {
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
fontSize: "90%",
|
fontSize: "90%",
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
@@ -37,13 +33,18 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
|||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box className={classes.main}>
|
<Flex align="flex-start" justify="space-between">
|
||||||
<Box className={classes.mainText}>
|
<Flex align="flex-start" className={classes.main}>
|
||||||
|
{props.showStarIcon && (
|
||||||
|
<Box ml={-6}>
|
||||||
|
<Star entry={props.entry} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<FeedEntryTitle entry={props.entry} />
|
<FeedEntryTitle entry={props.entry} />
|
||||||
</Box>
|
</Flex>
|
||||||
<OpenExternalLink entry={props.entry} />
|
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
|
||||||
</Box>
|
</Flex>
|
||||||
<Box className={classes.details}>
|
<Flex align="center" className={classes.details}>
|
||||||
<FeedFavicon url={props.entry.iconUrl} />
|
<FeedFavicon url={props.entry.iconUrl} />
|
||||||
<Space w={6} />
|
<Space w={6} />
|
||||||
<Text c="dimmed">
|
<Text c="dimmed">
|
||||||
@@ -51,7 +52,7 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
|||||||
<span> · </span>
|
<span> · </span>
|
||||||
<RelativeDate date={props.entry.date} />
|
<RelativeDate date={props.entry.date} />
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Flex>
|
||||||
{props.expanded && (
|
{props.expanded && (
|
||||||
<Box className={classes.details}>
|
<Box className={classes.details}>
|
||||||
<Text c="dimmed">
|
<Text c="dimmed">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/macro"
|
||||||
import { ActionIcon, Anchor, Tooltip } from "@mantine/core"
|
import { ActionIcon, Anchor, Tooltip } from "@mantine/core"
|
||||||
|
import { Constants } from "app/constants"
|
||||||
import { markEntry } from "app/entries/thunks"
|
import { markEntry } from "app/entries/thunks"
|
||||||
import { useAppDispatch } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { type Entry } from "app/types"
|
import { type Entry } from "app/types"
|
||||||
@@ -19,9 +20,9 @@ export function OpenExternalLink(props: { entry: Entry }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Anchor href={props.entry.url} target="_blank" rel="noreferrer" onClick={onClick}>
|
<Anchor href={props.entry.url} target="_blank" rel="noreferrer" onClick={onClick}>
|
||||||
<Tooltip label={<Trans>Open link</Trans>}>
|
<Tooltip label={<Trans>Open link</Trans>} openDelay={Constants.tooltip.delay}>
|
||||||
<ActionIcon variant="transparent" c="dimmed">
|
<ActionIcon variant="transparent" c="dimmed">
|
||||||
<TbExternalLink />
|
<TbExternalLink size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|||||||
29
commafeed-client/src/components/content/header/Star.tsx
Normal file
29
commafeed-client/src/components/content/header/Star.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Trans } from "@lingui/macro"
|
||||||
|
import { ActionIcon, Tooltip } from "@mantine/core"
|
||||||
|
import { Constants } from "app/constants"
|
||||||
|
import { starEntry } from "app/entries/thunks"
|
||||||
|
import { useAppDispatch } from "app/store"
|
||||||
|
import type { Entry } from "app/types"
|
||||||
|
import { TbStar, TbStarFilled } from "react-icons/tb"
|
||||||
|
|
||||||
|
export function Star(props: { entry: Entry }) {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const onClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
dispatch(
|
||||||
|
starEntry({
|
||||||
|
entry: props.entry,
|
||||||
|
starred: !props.entry.starred,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { t, Trans } from "@lingui/macro"
|
||||||
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||||
|
import { type ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { type ScrollMode, type SharingSettings } from "app/types"
|
import { type IconDisplayMode, type ScrollMode, type SharingSettings } from "app/types"
|
||||||
import {
|
import {
|
||||||
changeCustomContextMenu,
|
changeCustomContextMenu,
|
||||||
|
changeExternalLinkIconDisplayMode,
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
changeMarkAllAsReadConfirmation,
|
changeMarkAllAsReadConfirmation,
|
||||||
changeMobileFooter,
|
changeMobileFooter,
|
||||||
@@ -13,16 +15,38 @@ import {
|
|||||||
changeScrollSpeed,
|
changeScrollSpeed,
|
||||||
changeSharingSetting,
|
changeSharingSetting,
|
||||||
changeShowRead,
|
changeShowRead,
|
||||||
|
changeStarIconDisplayMode,
|
||||||
} from "app/user/thunks"
|
} from "app/user/thunks"
|
||||||
import { locales } from "i18n"
|
import { locales } from "i18n"
|
||||||
import { type ReactNode } from "react"
|
import { type ReactNode } from "react"
|
||||||
|
|
||||||
|
const displayModeData: ComboboxData = [
|
||||||
|
{
|
||||||
|
value: "always",
|
||||||
|
label: t`Always`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "on_desktop",
|
||||||
|
label: t`On desktop`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "on_mobile",
|
||||||
|
label: t`On mobile`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "never",
|
||||||
|
label: t`Never`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export function DisplaySettings() {
|
export function DisplaySettings() {
|
||||||
const language = useAppSelector(state => state.user.settings?.language)
|
const language = useAppSelector(state => state.user.settings?.language)
|
||||||
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
|
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
|
||||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||||
const scrollMode = useAppSelector(state => state.user.settings?.scrollMode)
|
const scrollMode = useAppSelector(state => state.user.settings?.scrollMode)
|
||||||
|
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
|
||||||
|
const externalLinkIconDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
|
||||||
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
|
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
|
||||||
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
|
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
|
||||||
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
||||||
@@ -59,18 +83,34 @@ export function DisplaySettings() {
|
|||||||
onChange={async e => await 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={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>}
|
label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>}
|
||||||
checked={mobileFooter}
|
checked={mobileFooter}
|
||||||
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Divider label={<Trans>Entry headers</Trans>} labelPosition="center" />
|
||||||
|
|
||||||
|
<Select
|
||||||
|
description={<Trans>Show star icon</Trans>}
|
||||||
|
value={starIconDisplayMode}
|
||||||
|
data={displayModeData}
|
||||||
|
onChange={async s => await dispatch(changeStarIconDisplayMode(s as IconDisplayMode))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
description={<Trans>Show external link icon</Trans>}
|
||||||
|
value={externalLinkIconDisplayMode}
|
||||||
|
data={displayModeData}
|
||||||
|
onChange={async s => await dispatch(changeExternalLinkIconDisplayMode(s as IconDisplayMode))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
|
||||||
|
checked={customContextMenu}
|
||||||
|
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
|
||||||
|
/>
|
||||||
|
|
||||||
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
|
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
|
||||||
|
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Badge, Tooltip } from "@mantine/core"
|
import { Badge, Tooltip } from "@mantine/core"
|
||||||
|
import { Constants } from "app/constants"
|
||||||
import { tss } from "tss"
|
import { tss } from "tss"
|
||||||
|
|
||||||
const useStyles = tss.create(() => ({
|
const useStyles = tss.create(() => ({
|
||||||
@@ -16,7 +17,7 @@ export function UnreadCount(props: { unreadCount: number }) {
|
|||||||
|
|
||||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||||
return (
|
return (
|
||||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count}>
|
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
||||||
<Badge className={classes.badge} variant="light">
|
<Badge className={classes.badge} variant="light">
|
||||||
{count}
|
{count}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ public class UserSettings extends AbstractModel {
|
|||||||
always, never, if_needed
|
always, never, if_needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum IconDisplayMode {
|
||||||
|
always, never, on_desktop, on_mobile
|
||||||
|
}
|
||||||
|
|
||||||
@OneToOne(fetch = FetchType.LAZY)
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "user_id", nullable = false, unique = true)
|
@JoinColumn(name = "user_id", nullable = false, unique = true)
|
||||||
private User user;
|
private User user;
|
||||||
@@ -74,6 +78,14 @@ public class UserSettings extends AbstractModel {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private ScrollMode scrollMode;
|
private ScrollMode scrollMode;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private IconDisplayMode starIconDisplayMode;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private IconDisplayMode externalLinkIconDisplayMode;
|
||||||
|
|
||||||
private boolean markAllAsReadConfirmation;
|
private boolean markAllAsReadConfirmation;
|
||||||
private boolean customContextMenu;
|
private boolean customContextMenu;
|
||||||
private boolean mobileFooter;
|
private boolean mobileFooter;
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ public class Settings implements Serializable {
|
|||||||
requiredMode = RequiredMode.REQUIRED)
|
requiredMode = RequiredMode.REQUIRED)
|
||||||
private String scrollMode;
|
private String scrollMode;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "whether to show the star icon in the header of entries",
|
||||||
|
allowableValues = "always,never,on_desktop,on_mobile",
|
||||||
|
requiredMode = RequiredMode.REQUIRED)
|
||||||
|
private String starIconDisplayMode;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "whether to show the external link icon in the header of entries",
|
||||||
|
allowableValues = "always,never,on_desktop,on_mobile",
|
||||||
|
requiredMode = RequiredMode.REQUIRED)
|
||||||
|
private String externalLinkIconDisplayMode;
|
||||||
|
|
||||||
@Schema(description = "ask for confirmation when marking all entries as read", requiredMode = RequiredMode.REQUIRED)
|
@Schema(description = "ask for confirmation when marking all entries as read", requiredMode = RequiredMode.REQUIRED)
|
||||||
private boolean markAllAsReadConfirmation;
|
private boolean markAllAsReadConfirmation;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.commafeed.backend.model.User;
|
|||||||
import com.commafeed.backend.model.UserRole;
|
import com.commafeed.backend.model.UserRole;
|
||||||
import com.commafeed.backend.model.UserRole.Role;
|
import com.commafeed.backend.model.UserRole.Role;
|
||||||
import com.commafeed.backend.model.UserSettings;
|
import com.commafeed.backend.model.UserSettings;
|
||||||
|
import com.commafeed.backend.model.UserSettings.IconDisplayMode;
|
||||||
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||||
import com.commafeed.backend.model.UserSettings.ScrollMode;
|
import com.commafeed.backend.model.UserSettings.ScrollMode;
|
||||||
@@ -110,6 +111,8 @@ public class UserREST {
|
|||||||
s.setLanguage(settings.getLanguage());
|
s.setLanguage(settings.getLanguage());
|
||||||
s.setScrollSpeed(settings.getScrollSpeed());
|
s.setScrollSpeed(settings.getScrollSpeed());
|
||||||
s.setScrollMode(settings.getScrollMode().name());
|
s.setScrollMode(settings.getScrollMode().name());
|
||||||
|
s.setStarIconDisplayMode(settings.getStarIconDisplayMode().name());
|
||||||
|
s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode().name());
|
||||||
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
|
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
|
||||||
s.setCustomContextMenu(settings.isCustomContextMenu());
|
s.setCustomContextMenu(settings.isCustomContextMenu());
|
||||||
s.setMobileFooter(settings.isMobileFooter());
|
s.setMobileFooter(settings.isMobileFooter());
|
||||||
@@ -131,6 +134,8 @@ public class UserREST {
|
|||||||
s.setLanguage("en");
|
s.setLanguage("en");
|
||||||
s.setScrollSpeed(400);
|
s.setScrollSpeed(400);
|
||||||
s.setScrollMode(ScrollMode.if_needed.name());
|
s.setScrollMode(ScrollMode.if_needed.name());
|
||||||
|
s.setStarIconDisplayMode(IconDisplayMode.always.name());
|
||||||
|
s.setExternalLinkIconDisplayMode(IconDisplayMode.always.name());
|
||||||
s.setMarkAllAsReadConfirmation(true);
|
s.setMarkAllAsReadConfirmation(true);
|
||||||
s.setCustomContextMenu(true);
|
s.setCustomContextMenu(true);
|
||||||
s.setMobileFooter(false);
|
s.setMobileFooter(false);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="add-icon-display-modes" author="athou">
|
||||||
|
<addColumn tableName="USERSETTINGS">
|
||||||
|
<column name="starIconDisplayMode" type="VARCHAR(32)" value="always">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
<column name="externalLinkIconDisplayMode" type="VARCHAR(32)" value="always">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -30,5 +30,6 @@
|
|||||||
<include file="changelogs/db.changelog-4.1.xml" />
|
<include file="changelogs/db.changelog-4.1.xml" />
|
||||||
<include file="changelogs/db.changelog-4.2.xml" />
|
<include file="changelogs/db.changelog-4.2.xml" />
|
||||||
<include file="changelogs/db.changelog-4.3.xml" />
|
<include file="changelogs/db.changelog-4.3.xml" />
|
||||||
|
<include file="changelogs/db.changelog-4.4.xml" />
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user