give responsibility of marking as read and expanding to caller

This commit is contained in:
Athou
2022-10-11 14:27:54 +02:00
parent 90d2ad6b19
commit 438b255708
3 changed files with 131 additions and 58 deletions

View File

@@ -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",

View File

@@ -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>

View File

@@ -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} />}