forked from Archives/Athou_commafeed
add support for tags
This commit is contained in:
@@ -22,6 +22,7 @@ import { FeedDetailsPage } from "pages/app/FeedDetailsPage"
|
|||||||
import { FeedEntriesPage } from "pages/app/FeedEntriesPage"
|
import { FeedEntriesPage } from "pages/app/FeedEntriesPage"
|
||||||
import Layout from "pages/app/Layout"
|
import Layout from "pages/app/Layout"
|
||||||
import { SettingsPage } from "pages/app/SettingsPage"
|
import { SettingsPage } from "pages/app/SettingsPage"
|
||||||
|
import { TagDetailsPage } from "pages/app/TagDetailsPage"
|
||||||
import { LoginPage } from "pages/auth/LoginPage"
|
import { LoginPage } from "pages/auth/LoginPage"
|
||||||
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
||||||
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
||||||
@@ -80,6 +81,10 @@ function AppRoutes() {
|
|||||||
<Route path=":id" element={<FeedEntriesPage sourceType="feed" />} />
|
<Route path=":id" element={<FeedEntriesPage sourceType="feed" />} />
|
||||||
<Route path=":id/details" element={<FeedDetailsPage />} />
|
<Route path=":id/details" element={<FeedDetailsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="tag">
|
||||||
|
<Route path=":id" element={<FeedEntriesPage sourceType="tag" />} />
|
||||||
|
<Route path=":id/details" element={<TagDetailsPage />} />
|
||||||
|
</Route>
|
||||||
<Route path="add" element={<AddPage />} />
|
<Route path="add" element={<AddPage />} />
|
||||||
<Route path="settings" element={<SettingsPage />} />
|
<Route path="settings" element={<SettingsPage />} />
|
||||||
<Route path="admin">
|
<Route path="admin">
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
StarRequest,
|
StarRequest,
|
||||||
SubscribeRequest,
|
SubscribeRequest,
|
||||||
Subscription,
|
Subscription,
|
||||||
|
TagRequest,
|
||||||
UserModel,
|
UserModel,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ export const client = {
|
|||||||
mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req),
|
mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req),
|
||||||
markMultiple: (req: MultipleMarkRequest) => axiosInstance.post("entry/markMultiple", req),
|
markMultiple: (req: MultipleMarkRequest) => axiosInstance.post("entry/markMultiple", req),
|
||||||
star: (req: StarRequest) => axiosInstance.post("entry/star", req),
|
star: (req: StarRequest) => axiosInstance.post("entry/star", req),
|
||||||
|
getTags: () => axiosInstance.get<string[]>("entry/tags"),
|
||||||
|
tag: (req: TagRequest) => axiosInstance.post("entry/tag", req),
|
||||||
},
|
},
|
||||||
feed: {
|
feed: {
|
||||||
get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`),
|
get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`),
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
|||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { RootState } from "app/store"
|
import { RootState } from "app/store"
|
||||||
import { Entries, Entry, MarkRequest } from "app/types"
|
import { Entries, Entry, MarkRequest, TagRequest } from "app/types"
|
||||||
import { scrollToWithCallback } from "app/utils"
|
import { scrollToWithCallback } from "app/utils"
|
||||||
import { flushSync } from "react-dom"
|
import { flushSync } from "react-dom"
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { reloadTree } from "./tree"
|
import { reloadTree } from "./tree"
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import { reloadTags } from "./user"
|
||||||
|
|
||||||
export type EntrySourceType = "category" | "feed"
|
export type EntrySourceType = "category" | "feed" | "tag"
|
||||||
export type EntrySource = { type: EntrySourceType; id: string }
|
export type EntrySource = { type: EntrySourceType; id: string }
|
||||||
export type ExpendableEntry = Entry & { expanded?: boolean }
|
export type ExpendableEntry = Entry & { expanded?: boolean }
|
||||||
|
|
||||||
@@ -40,16 +42,18 @@ const initialState: EntriesState = {
|
|||||||
scrollingToEntry: false,
|
scrollingToEntry: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEndpoint = (sourceType: EntrySourceType) => (sourceType === "category" ? client.category.getEntries : client.feed.getEntries)
|
const getEndpoint = (sourceType: EntrySourceType) =>
|
||||||
|
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
|
||||||
export const loadEntries = createAsyncThunk<Entries, EntrySource, { state: RootState }>("entries/load", async (source, thunkApi) => {
|
export const loadEntries = createAsyncThunk<Entries, EntrySource, { state: RootState }>("entries/load", async (source, thunkApi) => {
|
||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
const endpoint = getEndpoint(source.type)
|
const endpoint = getEndpoint(source.type)
|
||||||
const result = await endpoint({
|
const result = await endpoint({
|
||||||
id: source.id,
|
id: source.type === "tag" ? Constants.categories.all.id : source.id,
|
||||||
order: state.user.settings?.readingOrder,
|
order: state.user.settings?.readingOrder,
|
||||||
readType: state.user.settings?.readingMode,
|
readType: state.user.settings?.readingMode,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
|
tag: source.type === "tag" ? source.id : undefined,
|
||||||
})
|
})
|
||||||
return result.data
|
return result.data
|
||||||
})
|
})
|
||||||
@@ -235,6 +239,10 @@ export const selectNextEntry = createAsyncThunk<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
export const tagEntry = createAsyncThunk<void, TagRequest, { state: RootState }>("entries/entry/tag", async (arg, thunkApi) => {
|
||||||
|
await client.entry.tag(arg)
|
||||||
|
thunkApi.dispatch(reloadTags())
|
||||||
|
})
|
||||||
|
|
||||||
export const entriesSlice = createSlice({
|
export const entriesSlice = createSlice({
|
||||||
name: "entries",
|
name: "entries",
|
||||||
@@ -305,6 +313,13 @@ export const entriesSlice = createSlice({
|
|||||||
state.entries = [...state.entries, ...entriesToAdd]
|
state.entries = [...state.entries, ...entriesToAdd]
|
||||||
state.hasMore = action.payload.hasMore
|
state.hasMore = action.payload.hasMore
|
||||||
})
|
})
|
||||||
|
builder.addCase(tagEntry.pending, (state, action) => {
|
||||||
|
state.entries
|
||||||
|
.filter(e => +e.id === action.meta.arg.entryId)
|
||||||
|
.forEach(e => {
|
||||||
|
e.tags = action.meta.arg.tags
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export const redirectToFeed = createAsyncThunk("redirect/feed", (id: string | nu
|
|||||||
export const redirectToFeedDetails = createAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
|
export const redirectToFeedDetails = createAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
|
||||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
|
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
|
||||||
)
|
)
|
||||||
|
export const redirectToTag = createAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
|
||||||
|
export const redirectToTagDetails = createAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
|
||||||
|
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
|
||||||
|
)
|
||||||
export const redirectToAdd = createAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
|
export const redirectToAdd = createAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
|
||||||
export const redirectToSettings = createAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
|
export const redirectToSettings = createAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
|
||||||
export const redirectToAdminUsers = createAsyncThunk("redirect/admin/users", (_, thunkApi) =>
|
export const redirectToAdminUsers = createAsyncThunk("redirect/admin/users", (_, thunkApi) =>
|
||||||
|
|||||||
@@ -4,17 +4,20 @@ import { createAsyncThunk, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
|||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { RootState } from "app/store"
|
import { RootState } from "app/store"
|
||||||
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel, ViewMode } from "app/types"
|
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel, ViewMode } from "app/types"
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { reloadEntries } from "./entries"
|
import { reloadEntries } from "./entries"
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
settings?: Settings
|
settings?: Settings
|
||||||
profile?: UserModel
|
profile?: UserModel
|
||||||
|
tags?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UserState = {}
|
const initialState: UserState = {}
|
||||||
|
|
||||||
export const reloadSettings = createAsyncThunk("settings/reload", () => client.user.getSettings().then(r => r.data))
|
export const reloadSettings = createAsyncThunk("settings/reload", () => client.user.getSettings().then(r => r.data))
|
||||||
export const reloadProfile = createAsyncThunk("profile/reload", () => client.user.getProfile().then(r => r.data))
|
export const reloadProfile = createAsyncThunk("profile/reload", () => client.user.getProfile().then(r => r.data))
|
||||||
|
export const reloadTags = createAsyncThunk("entries/tags", () => client.entry.getTags().then(r => r.data))
|
||||||
export const changeReadingMode = createAsyncThunk<void, ReadingMode, { state: RootState }>(
|
export const changeReadingMode = createAsyncThunk<void, ReadingMode, { state: RootState }>(
|
||||||
"settings/readingMode",
|
"settings/readingMode",
|
||||||
(readingMode, thunkApi) => {
|
(readingMode, thunkApi) => {
|
||||||
@@ -85,6 +88,9 @@ export const userSlice = createSlice({
|
|||||||
builder.addCase(reloadProfile.fulfilled, (state, action) => {
|
builder.addCase(reloadProfile.fulfilled, (state, action) => {
|
||||||
state.profile = action.payload
|
state.profile = action.payload
|
||||||
})
|
})
|
||||||
|
builder.addCase(reloadTags.fulfilled, (state, action) => {
|
||||||
|
state.tags = action.payload
|
||||||
|
})
|
||||||
builder.addCase(changeReadingMode.pending, (state, action) => {
|
builder.addCase(changeReadingMode.pending, (state, action) => {
|
||||||
if (!state.settings) return
|
if (!state.settings) return
|
||||||
state.settings.readingMode = action.meta.arg
|
state.settings.readingMode = action.meta.arg
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { t } from "@lingui/macro"
|
||||||
import { Checkbox, Group, Popover } from "@mantine/core"
|
import { Checkbox, Group, MultiSelect, Popover } from "@mantine/core"
|
||||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
|
import { Constants } from "app/constants"
|
||||||
|
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { Entry } from "app/types"
|
import { Entry } from "app/types"
|
||||||
import { ActionButton } from "components/ActionButtton"
|
import { ActionButton } from "components/ActionButtton"
|
||||||
import { TbArrowBarToDown, TbExternalLink, TbShare, TbStar, TbStarOff } from "react-icons/tb"
|
import { useEffect, useState } from "react"
|
||||||
|
import { TbArrowBarToDown, TbExternalLink, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||||
import { ShareButtons } from "./ShareButtons"
|
import { ShareButtons } from "./ShareButtons"
|
||||||
|
|
||||||
interface FeedEntryFooterProps {
|
interface FeedEntryFooterProps {
|
||||||
@@ -12,13 +14,30 @@ interface FeedEntryFooterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||||
|
const [scrollPosition, setScrollPosition] = useState(0)
|
||||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||||
|
const tags = useAppSelector(state => state.user.tags)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const showSharingButtons =
|
const showSharingButtons =
|
||||||
sharingSettings && (Object.values(sharingSettings) as Array<typeof sharingSettings[keyof typeof sharingSettings]>).some(v => v)
|
sharingSettings && (Object.values(sharingSettings) as Array<typeof sharingSettings[keyof typeof sharingSettings]>).some(v => v)
|
||||||
|
|
||||||
const readStatusCheckboxClicked = () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))
|
const readStatusCheckboxClicked = () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))
|
||||||
|
const onTagsChange = (values: string[]) =>
|
||||||
|
dispatch(
|
||||||
|
tagEntry({
|
||||||
|
entryId: +props.entry.id,
|
||||||
|
tags: values,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||||
|
|
||||||
|
const listener = () => setScrollPosition(scrollArea ? scrollArea.scrollTop : 0)
|
||||||
|
scrollArea?.addEventListener("scroll", listener)
|
||||||
|
return () => scrollArea?.removeEventListener("scroll", listener)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
@@ -41,7 +60,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{showSharingButtons && (
|
{showSharingButtons && (
|
||||||
<Popover withArrow withinPortal shadow="md">
|
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]}>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<ActionButton icon={<TbShare size={18} />} label={t`Share`} />
|
<ActionButton icon={<TbShare size={18} />} label={t`Share`} />
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
@@ -51,6 +70,25 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
|||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{tags && (
|
||||||
|
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]}>
|
||||||
|
<Popover.Target>
|
||||||
|
<ActionButton icon={<TbTag size={18} />} label={t`Tags`} />
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<MultiSelect
|
||||||
|
data={tags}
|
||||||
|
placeholder="Tags"
|
||||||
|
searchable
|
||||||
|
creatable
|
||||||
|
getCreateLabel={query => t`Create tag: ${query}`}
|
||||||
|
value={props.entry.tags}
|
||||||
|
onChange={onTagsChange}
|
||||||
|
/>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { t } from "@lingui/macro"
|
||||||
import { Box, Stack } from "@mantine/core"
|
import { Box, Stack } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { redirectToCategory, redirectToCategoryDetails, redirectToFeed, redirectToFeedDetails } from "app/slices/redirect"
|
import {
|
||||||
|
redirectToCategory,
|
||||||
|
redirectToCategoryDetails,
|
||||||
|
redirectToFeed,
|
||||||
|
redirectToFeedDetails,
|
||||||
|
redirectToTag,
|
||||||
|
redirectToTagDetails,
|
||||||
|
} from "app/slices/redirect"
|
||||||
import { collapseTreeCategory } from "app/slices/tree"
|
import { collapseTreeCategory } from "app/slices/tree"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { Category, Subscription } from "app/types"
|
import { Category, Subscription } from "app/types"
|
||||||
@@ -9,19 +16,21 @@ 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, FaStar } from "react-icons/fa"
|
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||||
import { TreeNode } from "./TreeNode"
|
import { TreeNode } from "./TreeNode"
|
||||||
import { TreeSearch } from "./TreeSearch"
|
import { TreeSearch } from "./TreeSearch"
|
||||||
|
|
||||||
const allIcon = <FaInbox size={14} />
|
const allIcon = <TbInbox size={16} />
|
||||||
const starredIcon = <FaStar size={14} />
|
const starredIcon = <TbStar size={16} />
|
||||||
const expandedIcon = <FaChevronDown size={14} />
|
const tagIcon = <TbTag size={16} />
|
||||||
const collapsedIcon = <FaChevronRight size={14} />
|
const expandedIcon = <TbChevronDown size={16} />
|
||||||
|
const collapsedIcon = <TbChevronRight size={16} />
|
||||||
|
|
||||||
const errorThreshold = 9
|
const errorThreshold = 9
|
||||||
export function Tree() {
|
export function Tree() {
|
||||||
const root = useAppSelector(state => state.tree.rootCategory)
|
const root = useAppSelector(state => state.tree.rootCategory)
|
||||||
const source = useAppSelector(state => state.entries.source)
|
const source = useAppSelector(state => state.entries.source)
|
||||||
|
const tags = useAppSelector(state => state.user.tags)
|
||||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@@ -46,6 +55,10 @@ export function Tree() {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const tagClicked = (e: React.MouseEvent, id: string) => {
|
||||||
|
if (e.detail === 2) dispatch(redirectToTagDetails(id))
|
||||||
|
else dispatch(redirectToTag(id))
|
||||||
|
}
|
||||||
|
|
||||||
const allCategoryNode = () => (
|
const allCategoryNode = () => (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
@@ -114,6 +127,20 @@ export function Tree() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tagNode = (tag: string) => (
|
||||||
|
<TreeNode
|
||||||
|
id={tag}
|
||||||
|
name={tag}
|
||||||
|
icon={tagIcon}
|
||||||
|
unread={0}
|
||||||
|
selected={source.type === "tag" && source.id === tag}
|
||||||
|
level={0}
|
||||||
|
hasError={false}
|
||||||
|
onClick={tagClicked}
|
||||||
|
key={tag}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
const recursiveCategoryNode = (category: Category, level = 0) => (
|
const recursiveCategoryNode = (category: Category, level = 0) => (
|
||||||
<React.Fragment key={`recursiveCategoryNode-${category.id}`}>
|
<React.Fragment key={`recursiveCategoryNode-${category.id}`}>
|
||||||
{categoryNode(category, level)}
|
{categoryNode(category, level)}
|
||||||
@@ -134,6 +161,7 @@ export function Tree() {
|
|||||||
{starredCategoryNode()}
|
{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))}
|
||||||
|
{tags?.map(tag => tagNode(tag))}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const useAppLoading = () => {
|
|||||||
const profile = useAppSelector(state => state.user.profile)
|
const profile = useAppSelector(state => state.user.profile)
|
||||||
const settings = useAppSelector(state => state.user.settings)
|
const settings = useAppSelector(state => state.user.settings)
|
||||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||||
|
const tags = useAppSelector(state => state.user.tags)
|
||||||
|
|
||||||
const steps: Step[] = [
|
const steps: Step[] = [
|
||||||
{
|
{
|
||||||
@@ -24,6 +25,10 @@ export const useAppLoading = () => {
|
|||||||
label: t`Loading subscriptions...`,
|
label: t`Loading subscriptions...`,
|
||||||
done: !!rootCategory,
|
done: !!rootCategory,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t`Loading tags...`,
|
||||||
|
done: !!tags,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const loading = steps.some(s => !s.done)
|
const loading = steps.some(s => !s.done)
|
||||||
|
|||||||
@@ -181,6 +181,10 @@ msgstr "Confirm password"
|
|||||||
msgid "Cozy"
|
msgid "Cozy"
|
||||||
msgstr "Cozy"
|
msgstr "Cozy"
|
||||||
|
|
||||||
|
#: src/components/content/FeedEntryFooter.tsx
|
||||||
|
msgid "Create tag: {query}"
|
||||||
|
msgstr "Create tag: {query}"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "Current password"
|
msgid "Current password"
|
||||||
msgstr "Current password"
|
msgstr "Current password"
|
||||||
@@ -379,6 +383,10 @@ msgstr "Loading settings..."
|
|||||||
msgid "Loading subscriptions..."
|
msgid "Loading subscriptions..."
|
||||||
msgstr "Loading subscriptions..."
|
msgstr "Loading subscriptions..."
|
||||||
|
|
||||||
|
#: src/hooks/useAppLoading.ts
|
||||||
|
msgid "Loading tags..."
|
||||||
|
msgstr "Loading tags..."
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
|
|||||||
@@ -181,6 +181,10 @@ msgstr "Confirmer le mot de passe"
|
|||||||
msgid "Cozy"
|
msgid "Cozy"
|
||||||
msgstr "Cozy"
|
msgstr "Cozy"
|
||||||
|
|
||||||
|
#: src/components/content/FeedEntryFooter.tsx
|
||||||
|
msgid "Create tag: {query}"
|
||||||
|
msgstr "Créer le tag: {query}"
|
||||||
|
|
||||||
#: src/components/settings/ProfileSettings.tsx
|
#: src/components/settings/ProfileSettings.tsx
|
||||||
msgid "Current password"
|
msgid "Current password"
|
||||||
msgstr "Mot de passe actuel"
|
msgstr "Mot de passe actuel"
|
||||||
@@ -379,6 +383,10 @@ msgstr "Chargement des paramètres ..."
|
|||||||
msgid "Loading subscriptions..."
|
msgid "Loading subscriptions..."
|
||||||
msgstr "Chargement des abonnements ..."
|
msgstr "Chargement des abonnements ..."
|
||||||
|
|
||||||
|
#: src/hooks/useAppLoading.ts
|
||||||
|
msgid "Loading tags..."
|
||||||
|
msgstr "Chargement des tags ..."
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ActionIcon, Anchor, Box, Center, Divider, Group, Title, useMantineTheme
|
|||||||
import { useViewportSize } from "@mantine/hooks"
|
import { useViewportSize } from "@mantine/hooks"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { EntrySourceType, loadEntries } from "app/slices/entries"
|
import { EntrySourceType, loadEntries } from "app/slices/entries"
|
||||||
import { redirectToCategoryDetails, redirectToFeedDetails } from "app/slices/redirect"
|
import { redirectToCategoryDetails, redirectToFeedDetails, redirectToTagDetails } from "app/slices/redirect"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { flattenCategoryTree } from "app/utils"
|
import { flattenCategoryTree } from "app/utils"
|
||||||
import { FeedEntries } from "components/content/FeedEntries"
|
import { FeedEntries } from "components/content/FeedEntries"
|
||||||
@@ -40,7 +40,8 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
|
|||||||
|
|
||||||
const titleClicked = () => {
|
const titleClicked = () => {
|
||||||
if (props.sourceType === "category") dispatch(redirectToCategoryDetails(id))
|
if (props.sourceType === "category") dispatch(redirectToCategoryDetails(id))
|
||||||
else dispatch(redirectToFeedDetails(id))
|
else if (props.sourceType === "feed") dispatch(redirectToFeedDetails(id))
|
||||||
|
else if (props.sourceType === "tag") dispatch(redirectToTagDetails(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useViewportSize } from "@mantine/hooks"
|
|||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect"
|
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect"
|
||||||
import { reloadTree, setMobileMenuOpen } from "app/slices/tree"
|
import { reloadTree, setMobileMenuOpen } from "app/slices/tree"
|
||||||
import { reloadProfile, reloadSettings } from "app/slices/user"
|
import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { Logo } from "components/Logo"
|
import { Logo } from "components/Logo"
|
||||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||||
@@ -90,6 +90,7 @@ export default function Layout({ sidebar, header }: LayoutProps) {
|
|||||||
dispatch(reloadSettings())
|
dispatch(reloadSettings())
|
||||||
dispatch(reloadProfile())
|
dispatch(reloadProfile())
|
||||||
dispatch(reloadTree())
|
dispatch(reloadTree())
|
||||||
|
dispatch(reloadTags())
|
||||||
|
|
||||||
// reload tree periodically
|
// reload tree periodically
|
||||||
const id = setInterval(() => dispatch(reloadTree()), 30000)
|
const id = setInterval(() => dispatch(reloadTree()), 30000)
|
||||||
|
|||||||
42
commafeed-client/src/pages/app/TagDetailsPage.tsx
Normal file
42
commafeed-client/src/pages/app/TagDetailsPage.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { t, Trans } from "@lingui/macro"
|
||||||
|
|
||||||
|
import { Anchor, Box, Button, Container, Group, Input, Stack, Title } from "@mantine/core"
|
||||||
|
import { Constants } from "app/constants"
|
||||||
|
import { redirectToSelectedSource } from "app/slices/redirect"
|
||||||
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
export function TagDetailsPage() {
|
||||||
|
const { id = Constants.categories.all.id } = useParams()
|
||||||
|
|
||||||
|
const apiKey = useAppSelector(state => state.user.profile?.apiKey)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Stack>
|
||||||
|
<Title order={3}>{id}</Title>
|
||||||
|
<Input.Wrapper label={t`Generated feed url`}>
|
||||||
|
<Box>
|
||||||
|
{apiKey && (
|
||||||
|
<Anchor
|
||||||
|
href={`rest/category/entriesAsFeed?id=${Constants.categories.all.id}&apiKey=${apiKey}&tag=${id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<Trans>Link</Trans>
|
||||||
|
</Anchor>
|
||||||
|
)}
|
||||||
|
{!apiKey && <Trans>Generate an API key in your profile first.</Trans>}
|
||||||
|
</Box>
|
||||||
|
</Input.Wrapper>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -103,7 +103,7 @@ public class EntryREST {
|
|||||||
@Path("/tag")
|
@Path("/tag")
|
||||||
@POST
|
@POST
|
||||||
@UnitOfWork
|
@UnitOfWork
|
||||||
@ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread")
|
@ApiOperation(value = "Set feed entry tags")
|
||||||
@Timed
|
@Timed
|
||||||
public Response tagEntry(@ApiParam(hidden = true) @SecurityCheck User user,
|
public Response tagEntry(@ApiParam(hidden = true) @SecurityCheck User user,
|
||||||
@Valid @ApiParam(value = "Tag Request", required = true) TagRequest req) {
|
@Valid @ApiParam(value = "Tag Request", required = true) TagRequest req) {
|
||||||
|
|||||||
Reference in New Issue
Block a user