forked from Archives/Athou_commafeed
add support for starring entries
This commit is contained in:
@@ -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 />} />
|
||||||
|
|||||||
@@ -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}`),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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`))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function Subscribe() {
|
|||||||
initialValues: {
|
initialValues: {
|
||||||
url: "",
|
url: "",
|
||||||
title: "",
|
title: "",
|
||||||
categoryId: Constants.categoryIds.all,
|
categoryId: Constants.categories.all.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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();`
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user