add support for starring entries

This commit is contained in:
Athou
2022-08-19 10:34:04 +02:00
parent 91bc7fa4b0
commit 973fe56cc8
14 changed files with 104 additions and 19 deletions

View File

@@ -64,7 +64,7 @@ function Providers(props: { children: React.ReactNode }) {
function AppRoutes() { function AppRoutes() {
return ( return (
<Routes> <Routes>
<Route path="/" element={<Navigate to={`/app/category/${Constants.categoryIds.all}`} replace />} /> <Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
<Route path="login" element={<LoginPage />} /> <Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegistrationPage />} /> <Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} /> <Route path="passwordRecovery" element={<PasswordRecoveryPage />} />

View File

@@ -18,6 +18,7 @@ import {
RegistrationRequest, RegistrationRequest,
ServerInfo, ServerInfo,
Settings, Settings,
StarRequest,
SubscribeRequest, SubscribeRequest,
Subscription, Subscription,
UserModel, UserModel,
@@ -44,6 +45,7 @@ export const client = {
}, },
entry: { entry: {
mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req), mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req),
star: (req: StarRequest) => axiosInstance.post("entry/star", req),
}, },
feed: { feed: {
get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`), get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`),

View File

@@ -1,9 +1,27 @@
import { t } from "@lingui/macro"
import { DEFAULT_THEME } from "@mantine/core" import { DEFAULT_THEME } from "@mantine/core"
import { Category } from "./types"
export const Constants = { const categories: { [key: string]: Category } = {
categoryIds: { all: {
all: "all", id: "all",
name: t`All`,
expanded: false,
children: [],
feeds: [],
position: 0,
}, },
starred: {
id: "starred",
name: t`Starred`,
expanded: false,
children: [],
feeds: [],
position: 1,
},
}
export const Constants = {
categories,
layout: { layout: {
mobileBreakpoint: DEFAULT_THEME.breakpoints.md, mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
headerHeight: 60, headerHeight: 60,

View File

@@ -28,7 +28,7 @@ interface EntriesState {
const initialState: EntriesState = { const initialState: EntriesState = {
source: { source: {
type: "category", type: "category",
id: Constants.categoryIds.all, id: Constants.categories.all.id,
}, },
sourceLabel: "", sourceLabel: "",
sourceWebsiteUrl: "", sourceWebsiteUrl: "",
@@ -87,6 +87,13 @@ export const markAllEntries = createAsyncThunk<void, { sourceType: EntrySourceTy
thunkApi.dispatch(reloadTree()) thunkApi.dispatch(reloadTree())
} }
) )
export const starEntry = createAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => {
client.entry.star({
id: arg.entry.id,
feedId: +arg.entry.feedId,
starred: arg.starred,
})
})
export const selectEntry = createAsyncThunk<void, Entry, { state: RootState }>("entries/entry/select", (arg, thunkApi) => { export const selectEntry = createAsyncThunk<void, Entry, { state: RootState }>("entries/entry/select", (arg, thunkApi) => {
const state = thunkApi.getState() const state = thunkApi.getState()
const entry = state.entries.entries.find(e => e.id === arg.id) const entry = state.entries.entries.find(e => e.id === arg.id)
@@ -159,6 +166,13 @@ export const entriesSlice = createSlice({
e.read = true e.read = true
}) })
}) })
builder.addCase(starEntry.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)
.forEach(e => {
e.starred = action.meta.arg.starred
})
})
builder.addCase(loadEntries.pending, (state, action) => { builder.addCase(loadEntries.pending, (state, action) => {
state.source = action.meta.arg state.source = action.meta.arg
state.entries = [] state.entries = []

View File

@@ -21,7 +21,7 @@ export const redirectToCategory = createAsyncThunk("redirect/category", (id: str
thunkApi.dispatch(redirectTo(`/app/category/${id}`)) thunkApi.dispatch(redirectTo(`/app/category/${id}`))
) )
export const redirectToRootCategory = createAsyncThunk("redirect/category/root", (_, thunkApi) => export const redirectToRootCategory = createAsyncThunk("redirect/category/root", (_, thunkApi) =>
thunkApi.dispatch(redirectToCategory(Constants.categoryIds.all)) thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
) )
export const redirectToCategoryDetails = createAsyncThunk("redirect/category/details", (id: string, thunkApi) => export const redirectToCategoryDetails = createAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`)) thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))

View File

@@ -1,10 +1,10 @@
import { t } from "@lingui/macro" import { t } from "@lingui/macro"
import { Checkbox, Group } from "@mantine/core" import { Checkbox, Group } from "@mantine/core"
import { markEntry } from "app/slices/entries" import { markEntry, starEntry } from "app/slices/entries"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButtton"
import { TbExternalLink } from "react-icons/tb" import { TbExternalLink, TbStar, TbStarOff } from "react-icons/tb"
interface FeedEntryFooterProps { interface FeedEntryFooterProps {
entry: Entry entry: Entry
@@ -27,6 +27,11 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
}} }}
/> />
)} )}
<ActionButton
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
label={props.entry.starred ? t`Unstar` : t`Star`}
onClick={() => dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}
/>
<a href={props.entry.url} target="_blank" rel="noreferrer"> <a href={props.entry.url} target="_blank" rel="noreferrer">
<ActionButton icon={<TbExternalLink size={18} />} label={t`Open link`} /> <ActionButton icon={<TbExternalLink size={18} />} label={t`Open link`} />
</a> </a>

View File

@@ -10,7 +10,7 @@ export function CategorySelect(props: CategorySelectProps) {
const rootCategory = useAppSelector(state => state.tree.rootCategory) const rootCategory = useAppSelector(state => state.tree.rootCategory)
const categories = rootCategory && flattenCategoryTree(rootCategory) const categories = rootCategory && flattenCategoryTree(rootCategory)
const selectData: SelectItem[] | undefined = categories const selectData: SelectItem[] | undefined = categories
?.filter(c => c.id !== Constants.categoryIds.all) ?.filter(c => c.id !== Constants.categories.all.id)
.sort((c1, c2) => c1.name.localeCompare(c2.name)) .sort((c1, c2) => c1.name.localeCompare(c2.name))
.map(c => ({ .map(c => ({
label: c.name, label: c.name,
@@ -19,7 +19,7 @@ export function CategorySelect(props: CategorySelectProps) {
if (props.withAll) { if (props.withAll) {
selectData?.unshift({ selectData?.unshift({
label: t`All`, label: t`All`,
value: Constants.categoryIds.all, value: Constants.categories.all.id,
}) })
} }

View File

@@ -27,7 +27,7 @@ export function Subscribe() {
initialValues: { initialValues: {
url: "", url: "",
title: "", title: "",
categoryId: Constants.categoryIds.all, categoryId: Constants.categories.all.id,
}, },
}) })

View File

@@ -9,11 +9,12 @@ import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { OnDesktop } from "components/responsive/OnDesktop" import { OnDesktop } from "components/responsive/OnDesktop"
import React from "react" import React from "react"
import { FaChevronDown, FaChevronRight, FaInbox } from "react-icons/fa" import { FaChevronDown, FaChevronRight, FaInbox, FaStar } from "react-icons/fa"
import { TreeNode } from "./TreeNode" import { TreeNode } from "./TreeNode"
import { TreeSearch } from "./TreeSearch" import { TreeSearch } from "./TreeSearch"
const allIcon = <FaInbox size={14} /> const allIcon = <FaInbox size={14} />
const starredIcon = <FaStar size={14} />
const expandedIcon = <FaChevronDown size={14} /> const expandedIcon = <FaChevronDown size={14} />
const collapsedIcon = <FaChevronRight size={14} /> const collapsedIcon = <FaChevronRight size={14} />
@@ -47,11 +48,24 @@ export function Tree() {
const allCategoryNode = () => ( const allCategoryNode = () => (
<TreeNode <TreeNode
id={Constants.categoryIds.all} id={Constants.categories.all.id}
name={t`All`} name={t`All`}
icon={allIcon} icon={allIcon}
unread={categoryUnreadCount(root)} unread={categoryUnreadCount(root)}
selected={source.type === "category" && source.id === Constants.categoryIds.all} selected={source.type === "category" && source.id === Constants.categories.all.id}
expanded={false}
level={0}
hasError={false}
onClick={categoryClicked}
/>
)
const starredCategoryNode = () => (
<TreeNode
id={Constants.categories.starred.id}
name={t`Starred`}
icon={starredIcon}
unread={0}
selected={source.type === "category" && source.id === Constants.categories.starred.id}
expanded={false} expanded={false}
level={0} level={0}
hasError={false} hasError={false}
@@ -109,6 +123,7 @@ export function Tree() {
</OnDesktop> </OnDesktop>
<Box> <Box>
{allCategoryNode()} {allCategoryNode()}
{starredCategoryNode()}
{root.children.map(c => recursiveCategoryNode(c))} {root.children.map(c => recursiveCategoryNode(c))}
{root.feeds.map(f => feedNode(f))} {root.feeds.map(f => feedNode(f))}
</Box> </Box>

View File

@@ -56,6 +56,7 @@ msgstr "Add user"
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr "Admin"
#: src/app/constants.ts
#: src/components/content/add/CategorySelect.tsx #: src/components/content/add/CategorySelect.tsx
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
#: src/components/sidebar/Tree.tsx #: src/components/sidebar/Tree.tsx
@@ -586,6 +587,15 @@ msgstr "Something bad just happened..."
msgid "Space" msgid "Space"
msgstr "Space" msgstr "Space"
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Star"
#: src/app/constants.ts
#: src/components/sidebar/Tree.tsx
msgid "Starred"
msgstr "Starred"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/pages/app/AddPage.tsx #: src/pages/app/AddPage.tsx
@@ -624,6 +634,10 @@ msgstr "Try out CommaFeed with the demo account: demo/demo"
msgid "Unread" msgid "Unread"
msgstr "Unread" msgstr "Unread"
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Unstar"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Unsubscribe" msgid "Unsubscribe"

View File

@@ -56,6 +56,7 @@ msgstr "Ajouter un utilisateur"
msgid "Admin" msgid "Admin"
msgstr "Administrateur" msgstr "Administrateur"
#: src/app/constants.ts
#: src/components/content/add/CategorySelect.tsx #: src/components/content/add/CategorySelect.tsx
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
#: src/components/sidebar/Tree.tsx #: src/components/sidebar/Tree.tsx
@@ -586,6 +587,15 @@ msgstr "Quelque chose s'est mal passé..."
msgid "Space" msgid "Space"
msgstr "Espace" msgstr "Espace"
#: src/components/content/FeedEntryFooter.tsx
msgid "Star"
msgstr "Ajouter aux favoris"
#: src/app/constants.ts
#: src/components/sidebar/Tree.tsx
msgid "Starred"
msgstr "Favoris"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/pages/app/AddPage.tsx #: src/pages/app/AddPage.tsx
@@ -624,6 +634,10 @@ msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"
msgid "Unread" msgid "Unread"
msgstr "Non lu" msgstr "Non lu"
#: src/components/content/FeedEntryFooter.tsx
msgid "Unstar"
msgstr "Retirer des favoris"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Unsubscribe" msgid "Unsubscribe"

View File

@@ -31,7 +31,7 @@ function Section(props: { title: string; icon: React.ReactNode; children: React.
} }
function NextUnreadBookmarklet() { function NextUnreadBookmarklet() {
const [categoryId, setCategoryId] = useState(Constants.categoryIds.all) const [categoryId, setCategoryId] = useState(Constants.categories.all.id)
const [order, setOrder] = useState("desc") const [order, setOrder] = useState("desc")
const baseUrl = window.location.href.substring(0, window.location.href.lastIndexOf("#")) const baseUrl = window.location.href.substring(0, window.location.href.lastIndexOf("#"))
const href = `javascript:window.location.href='${baseUrl}next?category=${categoryId}&order=${order}&t='+new Date().getTime();` const href = `javascript:window.location.href='${baseUrl}next?category=${categoryId}&order=${order}&t='+new Date().getTime();`

View File

@@ -18,13 +18,16 @@ import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
export function CategoryDetailsPage() { export function CategoryDetailsPage() {
const { id = Constants.categoryIds.all } = useParams() const { id = Constants.categories.all.id } = useParams()
const apiKey = useAppSelector(state => state.user.profile?.apiKey) const apiKey = useAppSelector(state => state.user.profile?.apiKey)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const query = useAsync(() => client.category.getRoot(), []) const query = useAsync(() => client.category.getRoot(), [])
const category = query.result && flattenCategoryTree(query.result.data).find(c => c.id === id) const category =
id === Constants.categories.starred.id
? Constants.categories.starred
: query.result && flattenCategoryTree(query.result.data).find(c => c.id === id)
const form = useForm<CategoryModificationRequest>() const form = useForm<CategoryModificationRequest>()
const { setValues } = form const { setValues } = form
@@ -69,7 +72,7 @@ export function CategoryDetailsPage() {
}) })
}, [setValues, category]) }, [setValues, category])
const editable = id !== Constants.categoryIds.all const editable = id !== Constants.categories.all.id && id !== Constants.categories.starred.id
if (!category) return <Loader /> if (!category) return <Loader />
return ( return (
<Container> <Container>

View File

@@ -29,7 +29,7 @@ interface FeedEntriesPageProps {
export function FeedEntriesPage(props: FeedEntriesPageProps) { export function FeedEntriesPage(props: FeedEntriesPageProps) {
const location = useLocation() const location = useLocation()
const { id = Constants.categoryIds.all } = useParams() const { id = Constants.categories.all.id } = useParams()
const viewport = useViewportSize() const viewport = useViewportSize()
const theme = useMantineTheme() const theme = useMantineTheme()
const rootCategory = useAppSelector(state => state.tree.rootCategory) const rootCategory = useAppSelector(state => state.tree.rootCategory)