mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
give responsibility of marking as read and expanding to caller
This commit is contained in:
@@ -122,47 +122,68 @@ export const starEntry = createAsyncThunk("entries/entry/star", (arg: { entry: E
|
|||||||
starred: arg.starred,
|
starred: arg.starred,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
export const selectEntry = createAsyncThunk<void, Entry, { state: RootState }>("entries/entry/select", (arg, thunkApi) => {
|
export const selectEntry = createAsyncThunk<
|
||||||
|
void,
|
||||||
|
{
|
||||||
|
entry: Entry
|
||||||
|
expand: boolean
|
||||||
|
markAsRead: boolean
|
||||||
|
},
|
||||||
|
{ 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.entry.id)
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
|
|
||||||
// only mark entry as read if we're expanding
|
// mark as read if requested
|
||||||
if (!entry.expanded) {
|
if (arg.markAsRead) {
|
||||||
thunkApi.dispatch(markEntry({ entry, read: true }))
|
thunkApi.dispatch(markEntry({ entry, read: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// set entry as selected
|
// set entry as selected
|
||||||
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
|
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
|
||||||
|
|
||||||
// collapse or expand entry if needed
|
// expand if requested
|
||||||
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
|
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
|
||||||
if (entry === previouslySelectedEntry) {
|
|
||||||
// selecting an entry already selected toggles expanded status
|
|
||||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: !entry.expanded }))
|
|
||||||
} else {
|
|
||||||
if (previouslySelectedEntry) {
|
if (previouslySelectedEntry) {
|
||||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry: previouslySelectedEntry, expanded: false }))
|
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry: previouslySelectedEntry, expanded: false }))
|
||||||
}
|
}
|
||||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: true }))
|
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand }))
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export const selectPreviousEntry = createAsyncThunk<void, void, { state: RootState }>("entries/entry/selectPrevious", (_, thunkApi) => {
|
export const selectPreviousEntry = createAsyncThunk<void, { expand: boolean; markAsRead: boolean }, { state: RootState }>(
|
||||||
|
"entries/entry/selectPrevious",
|
||||||
|
(arg, thunkApi) => {
|
||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
const { entries } = state.entries
|
const { entries } = state.entries
|
||||||
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
|
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
|
||||||
if (previousIndex >= 0) {
|
if (previousIndex >= 0) {
|
||||||
thunkApi.dispatch(selectEntry(entries[previousIndex]))
|
thunkApi.dispatch(
|
||||||
|
selectEntry({
|
||||||
|
entry: entries[previousIndex],
|
||||||
|
expand: arg.expand,
|
||||||
|
markAsRead: arg.markAsRead,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
export const selectNextEntry = createAsyncThunk<void, void, { state: RootState }>("entries/entry/selectNext", (_, thunkApi) => {
|
)
|
||||||
|
export const selectNextEntry = createAsyncThunk<void, { expand: boolean; markAsRead: boolean }, { state: RootState }>(
|
||||||
|
"entries/entry/selectNext",
|
||||||
|
(arg, thunkApi) => {
|
||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
const { entries } = state.entries
|
const { entries } = state.entries
|
||||||
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
|
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
|
||||||
if (nextIndex < entries.length) {
|
if (nextIndex < entries.length) {
|
||||||
thunkApi.dispatch(selectEntry(entries[nextIndex]))
|
thunkApi.dispatch(
|
||||||
|
selectEntry({
|
||||||
|
entry: entries[nextIndex],
|
||||||
|
expand: arg.expand,
|
||||||
|
markAsRead: arg.markAsRead,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const entriesSlice = createSlice({
|
export const entriesSlice = createSlice({
|
||||||
name: "entries",
|
name: "entries",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { t } from "@lingui/macro"
|
|||||||
import { openModal } from "@mantine/modals"
|
import { openModal } from "@mantine/modals"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import {
|
import {
|
||||||
|
ExpendableEntry,
|
||||||
loadMoreEntries,
|
loadMoreEntries,
|
||||||
markAllEntries,
|
markAllEntries,
|
||||||
markEntry,
|
markEntry,
|
||||||
@@ -31,6 +32,25 @@ export function FeedEntries() {
|
|||||||
|
|
||||||
const selectedEntry = entries.find(e => e.id === selectedEntryId)
|
const selectedEntry = entries.find(e => e.id === selectedEntryId)
|
||||||
|
|
||||||
|
const headerClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
|
||||||
|
if (event.button === 1 || event.ctrlKey || event.metaKey) {
|
||||||
|
// middle click
|
||||||
|
dispatch(markEntry({ entry, read: true }))
|
||||||
|
} else if (event.button === 0) {
|
||||||
|
// main click
|
||||||
|
// don't trigger the link
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
selectEntry({
|
||||||
|
entry,
|
||||||
|
expand: !entry.expanded,
|
||||||
|
markAsRead: !entry.expanded,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// references to entries html elements
|
// references to entries html elements
|
||||||
const refs = useRef<{ [id: string]: HTMLDivElement }>({})
|
const refs = useRef<{ [id: string]: HTMLDivElement }>({})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,17 +79,32 @@ export function FeedEntries() {
|
|||||||
dispatch(reloadEntries())
|
dispatch(reloadEntries())
|
||||||
})
|
})
|
||||||
useMousetrap("j", () => {
|
useMousetrap("j", () => {
|
||||||
dispatch(selectNextEntry())
|
dispatch(
|
||||||
|
selectNextEntry({
|
||||||
|
expand: true,
|
||||||
|
markAsRead: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
useMousetrap("k", () => {
|
useMousetrap("k", () => {
|
||||||
dispatch(selectPreviousEntry())
|
dispatch(
|
||||||
|
selectPreviousEntry({
|
||||||
|
expand: true,
|
||||||
|
markAsRead: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
useMousetrap("space", () => {
|
useMousetrap("space", () => {
|
||||||
if (selectedEntry) {
|
if (selectedEntry) {
|
||||||
if (selectedEntry.expanded) {
|
if (selectedEntry.expanded) {
|
||||||
const ref = refs.current[selectedEntry.id]
|
const ref = refs.current[selectedEntry.id]
|
||||||
if (Constants.layout.isBottomVisible(ref)) {
|
if (Constants.layout.isBottomVisible(ref)) {
|
||||||
dispatch(selectNextEntry())
|
dispatch(
|
||||||
|
selectNextEntry({
|
||||||
|
expand: true,
|
||||||
|
markAsRead: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||||
scrollArea?.scrollTo({
|
scrollArea?.scrollTo({
|
||||||
@@ -78,10 +113,21 @@ export function FeedEntries() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(selectEntry(selectedEntry))
|
dispatch(
|
||||||
|
selectEntry({
|
||||||
|
entry: selectedEntry,
|
||||||
|
expand: true,
|
||||||
|
markAsRead: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(selectNextEntry())
|
dispatch(
|
||||||
|
selectNextEntry({
|
||||||
|
expand: true,
|
||||||
|
markAsRead: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
useMousetrap("shift+space", () => {
|
useMousetrap("shift+space", () => {
|
||||||
@@ -89,7 +135,12 @@ export function FeedEntries() {
|
|||||||
if (selectedEntry.expanded) {
|
if (selectedEntry.expanded) {
|
||||||
const ref = refs.current[selectedEntry.id]
|
const ref = refs.current[selectedEntry.id]
|
||||||
if (Constants.layout.isTopVisible(ref)) {
|
if (Constants.layout.isTopVisible(ref)) {
|
||||||
dispatch(selectPreviousEntry())
|
dispatch(
|
||||||
|
selectPreviousEntry({
|
||||||
|
expand: true,
|
||||||
|
markAsRead: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||||
scrollArea?.scrollTo({
|
scrollArea?.scrollTo({
|
||||||
@@ -98,14 +149,25 @@ export function FeedEntries() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(selectPreviousEntry())
|
dispatch(
|
||||||
|
selectPreviousEntry({
|
||||||
|
expand: true,
|
||||||
|
markAsRead: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
useMousetrap(["o", "enter"], () => {
|
useMousetrap(["o", "enter"], () => {
|
||||||
// toggle expanded status
|
// toggle expanded status
|
||||||
if (!selectedEntry) return
|
if (!selectedEntry) return
|
||||||
dispatch(selectEntry(selectedEntry))
|
dispatch(
|
||||||
|
selectEntry({
|
||||||
|
entry: selectedEntry,
|
||||||
|
expand: !selectedEntry.expanded,
|
||||||
|
markAsRead: !selectedEntry.expanded,
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
useMousetrap("v", () => {
|
useMousetrap("v", () => {
|
||||||
// open tab in foreground
|
// open tab in foreground
|
||||||
@@ -160,14 +222,18 @@ export function FeedEntries() {
|
|||||||
useWindow={false}
|
useWindow={false}
|
||||||
getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)}
|
getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)}
|
||||||
>
|
>
|
||||||
{entries.map(e => (
|
{entries.map(entry => (
|
||||||
<div
|
<div
|
||||||
key={e.id}
|
key={entry.id}
|
||||||
ref={el => {
|
ref={el => {
|
||||||
refs.current[e.id] = el!
|
refs.current[entry.id] = el!
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FeedEntry entry={e} expanded={!!e.expanded || viewMode === "expanded"} />
|
<FeedEntry
|
||||||
|
entry={entry}
|
||||||
|
expanded={!!entry.expanded || viewMode === "expanded"}
|
||||||
|
onHeaderClick={event => headerClicked(entry, event)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Anchor, Box, createStyles, Divider, Paper } from "@mantine/core"
|
import { Anchor, Box, createStyles, Divider, Paper } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { markEntry, selectEntry } from "app/slices/entries"
|
import { useAppSelector } from "app/store"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
|
||||||
import { Entry } from "app/types"
|
import { Entry } from "app/types"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { FeedEntryBody } from "./FeedEntryBody"
|
import { FeedEntryBody } from "./FeedEntryBody"
|
||||||
@@ -12,6 +11,7 @@ import { FeedEntryHeader } from "./FeedEntryHeader"
|
|||||||
interface FeedEntryProps {
|
interface FeedEntryProps {
|
||||||
entry: Entry
|
entry: Entry
|
||||||
expanded: boolean
|
expanded: boolean
|
||||||
|
onHeaderClick: (e: React.MouseEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles((theme, props: FeedEntryProps) => {
|
const useStyles = createStyles((theme, props: FeedEntryProps) => {
|
||||||
@@ -38,22 +38,8 @@ const useStyles = createStyles((theme, props: FeedEntryProps) => {
|
|||||||
export function FeedEntry(props: FeedEntryProps) {
|
export function FeedEntry(props: FeedEntryProps) {
|
||||||
const { classes } = useStyles(props)
|
const { classes } = useStyles(props)
|
||||||
const viewMode = useAppSelector(state => state.user.settings?.viewMode)
|
const viewMode = useAppSelector(state => state.user.settings?.viewMode)
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const compactHeader = viewMode === "title" && !props.expanded
|
const compactHeader = viewMode === "title" && !props.expanded
|
||||||
|
|
||||||
const headerClicked = (e: React.MouseEvent) => {
|
|
||||||
if (e.button === 1 || e.ctrlKey || e.metaKey) {
|
|
||||||
// middle click
|
|
||||||
dispatch(markEntry({ entry: props.entry, read: true }))
|
|
||||||
} else if (e.button === 0) {
|
|
||||||
// main click
|
|
||||||
// don't trigger the link
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
dispatch(selectEntry(props.entry))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper shadow="xs" withBorder className={classes.paper}>
|
<Paper shadow="xs" withBorder className={classes.paper}>
|
||||||
<Anchor
|
<Anchor
|
||||||
@@ -61,8 +47,8 @@ export function FeedEntry(props: FeedEntryProps) {
|
|||||||
href={props.entry.url}
|
href={props.entry.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={headerClicked}
|
onClick={props.onHeaderClick}
|
||||||
onAuxClick={headerClicked}
|
onAuxClick={props.onHeaderClick}
|
||||||
>
|
>
|
||||||
<Box p="xs">
|
<Box p="xs">
|
||||||
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
|
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user