forked from Archives/Athou_commafeed
select and mark entry as read when scrolling in expanded view
This commit is contained in:
14
commafeed-client/package-lock.json
generated
14
commafeed-client/package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"axios": "^1.1.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"interweave": "^13.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"make-plural": "^7.1.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^18.2.0",
|
||||
@@ -39,6 +40,7 @@
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^3.14.0",
|
||||
"@types/eslint": "^8.4.6",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/mousetrap": "^1.6.9",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
@@ -3100,6 +3102,12 @@
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.186",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz",
|
||||
"integrity": "sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mousetrap": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.9.tgz",
|
||||
@@ -12465,6 +12473,12 @@
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.186",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz",
|
||||
"integrity": "sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mousetrap": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.9.tgz",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"axios": "^1.1.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"interweave": "^13.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"make-plural": "^7.1.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^18.2.0",
|
||||
@@ -46,6 +47,7 @@
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^3.14.0",
|
||||
"@types/eslint": "^8.4.6",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/mousetrap": "^1.6.9",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DEFAULT_THEME } from "@mantine/core"
|
||||
import { IconType } from "react-icons"
|
||||
import { FaAt } from "react-icons/fa"
|
||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
|
||||
import { Category, SharingSettings } from "./types"
|
||||
import { Category, Entry, SharingSettings } from "./types"
|
||||
|
||||
const categories: { [key: string]: Category } = {
|
||||
all: {
|
||||
@@ -90,10 +90,11 @@ export const Constants = {
|
||||
headerHeight: 60,
|
||||
sidebarWidth: 350,
|
||||
entryMaxWidth: 650,
|
||||
isTopVisible: (div: HTMLDivElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
|
||||
isBottomVisible: (div: HTMLDivElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
|
||||
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
|
||||
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
|
||||
},
|
||||
dom: {
|
||||
mainScrollAreaId: "main-scroll-area-id",
|
||||
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ describe("entries", () => {
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: true,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -100,6 +101,7 @@ describe("entries", () => {
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
|
||||
hasMore: true,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -125,6 +127,7 @@ describe("entries", () => {
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
|
||||
hasMore: true,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,8 @@ import { client } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { RootState } from "app/store"
|
||||
import { Entries, Entry, MarkRequest } from "app/types"
|
||||
import { scrollToWithCallback } from "app/utils"
|
||||
import { flushSync } from "react-dom"
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { reloadTree } from "./tree"
|
||||
|
||||
@@ -23,6 +25,7 @@ interface EntriesState {
|
||||
timestamp?: number
|
||||
selectedEntryId?: string
|
||||
hasMore: boolean
|
||||
scrollingToEntry: boolean
|
||||
}
|
||||
|
||||
const initialState: EntriesState = {
|
||||
@@ -34,6 +37,7 @@ const initialState: EntriesState = {
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [],
|
||||
hasMore: true,
|
||||
scrollingToEntry: false,
|
||||
}
|
||||
|
||||
const getEndpoint = (sourceType: EntrySourceType) => (sourceType === "category" ? client.category.getEntries : client.feed.getEntries)
|
||||
@@ -128,6 +132,7 @@ export const selectEntry = createAsyncThunk<
|
||||
entry: Entry
|
||||
expand: boolean
|
||||
markAsRead: boolean
|
||||
scrollToEntry: boolean
|
||||
},
|
||||
{ state: RootState }
|
||||
>("entries/entry/select", (arg, thunkApi) => {
|
||||
@@ -135,55 +140,101 @@ export const selectEntry = createAsyncThunk<
|
||||
const entry = state.entries.entries.find(e => e.id === arg.entry.id)
|
||||
if (!entry) return
|
||||
|
||||
// mark as read if requested
|
||||
if (arg.markAsRead) {
|
||||
thunkApi.dispatch(markEntry({ entry, read: true }))
|
||||
}
|
||||
// flushSync is required because we need the newly selected entry to be expanded
|
||||
// and the previously selected entry to be collapsed to be able to scroll to the right position
|
||||
flushSync(() => {
|
||||
// mark as read if requested
|
||||
if (arg.markAsRead) {
|
||||
thunkApi.dispatch(markEntry({ entry, read: true }))
|
||||
}
|
||||
|
||||
// set entry as selected
|
||||
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
|
||||
// set entry as selected
|
||||
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
|
||||
|
||||
// expand if requested
|
||||
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
|
||||
if (previouslySelectedEntry) {
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry: previouslySelectedEntry, expanded: false }))
|
||||
// expand if requested
|
||||
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
|
||||
if (previouslySelectedEntry) {
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry: previouslySelectedEntry, expanded: false }))
|
||||
}
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand }))
|
||||
})
|
||||
|
||||
if (arg.scrollToEntry) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(entry))
|
||||
if (entryElement) {
|
||||
const scrollSpeed = state.user.settings?.scrollSpeed
|
||||
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
|
||||
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
|
||||
}
|
||||
}
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand }))
|
||||
})
|
||||
export const selectPreviousEntry = createAsyncThunk<void, { expand: boolean; markAsRead: boolean }, { state: RootState }>(
|
||||
"entries/entry/selectPrevious",
|
||||
(arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
|
||||
if (previousIndex >= 0) {
|
||||
thunkApi.dispatch(
|
||||
selectEntry({
|
||||
entry: entries[previousIndex],
|
||||
expand: arg.expand,
|
||||
markAsRead: arg.markAsRead,
|
||||
})
|
||||
)
|
||||
}
|
||||
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
// the entry is entirely visible, no need to scroll
|
||||
if (Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)) {
|
||||
onScrollEnded()
|
||||
return
|
||||
}
|
||||
)
|
||||
export const selectNextEntry = createAsyncThunk<void, { expand: boolean; markAsRead: boolean }, { state: RootState }>(
|
||||
"entries/entry/selectNext",
|
||||
(arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
|
||||
if (nextIndex < entries.length) {
|
||||
thunkApi.dispatch(
|
||||
selectEntry({
|
||||
entry: entries[nextIndex],
|
||||
expand: arg.expand,
|
||||
markAsRead: arg.markAsRead,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
if (scrollArea) {
|
||||
scrollToWithCallback({
|
||||
element: scrollArea,
|
||||
options: {
|
||||
// add a small gap between the top of the content and the top of the page
|
||||
top: entryElement.offsetTop - 3,
|
||||
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
|
||||
},
|
||||
onScrollEnded,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const selectPreviousEntry = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
expand: boolean
|
||||
markAsRead: boolean
|
||||
scrollToEntry: boolean
|
||||
},
|
||||
{ state: RootState }
|
||||
>("entries/entry/selectPrevious", (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
|
||||
if (previousIndex >= 0) {
|
||||
thunkApi.dispatch(
|
||||
selectEntry({
|
||||
entry: entries[previousIndex],
|
||||
expand: arg.expand,
|
||||
markAsRead: arg.markAsRead,
|
||||
scrollToEntry: arg.scrollToEntry,
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
export const selectNextEntry = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
expand: boolean
|
||||
markAsRead: boolean
|
||||
scrollToEntry: boolean
|
||||
},
|
||||
{ state: RootState }
|
||||
>("entries/entry/selectNext", (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
|
||||
if (nextIndex < entries.length) {
|
||||
thunkApi.dispatch(
|
||||
selectEntry({
|
||||
entry: entries[nextIndex],
|
||||
expand: arg.expand,
|
||||
markAsRead: arg.markAsRead,
|
||||
scrollToEntry: arg.scrollToEntry,
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export const entriesSlice = createSlice({
|
||||
name: "entries",
|
||||
@@ -199,6 +250,9 @@ export const entriesSlice = createSlice({
|
||||
e.expanded = action.payload.expanded
|
||||
})
|
||||
},
|
||||
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
|
||||
state.scrollingToEntry = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(markEntry.pending, (state, action) => {
|
||||
|
||||
@@ -54,6 +54,11 @@ export const changeShowRead = createAsyncThunk<void, boolean, { state: RootState
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, showRead })
|
||||
})
|
||||
export const changeScrollMarks = createAsyncThunk<void, boolean, { state: RootState }>("settings/scrollMarks", (scrollMarks, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMarks })
|
||||
})
|
||||
export const changeSharingSetting = createAsyncThunk<void, { site: keyof SharingSettings; value: boolean }, { state: RootState }>(
|
||||
"settings/sharingSetting",
|
||||
(sharingSetting, thunkApi) => {
|
||||
@@ -104,12 +109,22 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.showRead = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeScrollMarks.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.scrollMarks = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeSharingSetting.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
|
||||
})
|
||||
builder.addMatcher(
|
||||
isAnyOf(changeLanguage.fulfilled, changeScrollSpeed.fulfilled, changeShowRead.fulfilled, changeSharingSetting.fulfilled),
|
||||
isAnyOf(
|
||||
changeLanguage.fulfilled,
|
||||
changeScrollSpeed.fulfilled,
|
||||
changeShowRead.fulfilled,
|
||||
changeScrollMarks.fulfilled,
|
||||
changeSharingSetting.fulfilled
|
||||
),
|
||||
() => {
|
||||
showNotification({
|
||||
message: t`Settings saved.`,
|
||||
|
||||
@@ -25,3 +25,29 @@ export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?:
|
||||
const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height
|
||||
return { width: placeholderWidth, height: placeholderHeight }
|
||||
}
|
||||
|
||||
export const scrollToWithCallback = ({
|
||||
element,
|
||||
options,
|
||||
onScrollEnded,
|
||||
}: {
|
||||
element: HTMLElement
|
||||
options: ScrollToOptions
|
||||
onScrollEnded: () => void
|
||||
}) => {
|
||||
const offset = (options.top ?? 0).toFixed()
|
||||
|
||||
const onScroll = () => {
|
||||
if (element.offsetTop.toFixed() === offset) {
|
||||
element.removeEventListener("scroll", onScroll)
|
||||
onScrollEnded()
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener("scroll", onScroll)
|
||||
|
||||
// scrollTo does not trigger if there's nothing to do, trigger it manually
|
||||
onScroll()
|
||||
|
||||
element.scrollTo(options)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { useEffect, useRef } from "react"
|
||||
import throttle from "lodash/throttle"
|
||||
import { useEffect } from "react"
|
||||
import InfiniteScroll from "react-infinite-scroller"
|
||||
import { FeedEntry } from "./FeedEntry"
|
||||
|
||||
@@ -27,7 +28,8 @@ export function FeedEntries() {
|
||||
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
|
||||
const hasMore = useAppSelector(state => state.entries.hasMore)
|
||||
const viewMode = useAppSelector(state => state.user.settings?.viewMode)
|
||||
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const selectedEntry = entries.find(e => e.id === selectedEntryId)
|
||||
@@ -46,80 +48,90 @@ export function FeedEntries() {
|
||||
entry,
|
||||
expand: !entry.expanded,
|
||||
markAsRead: !entry.expanded,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// references to entries html elements
|
||||
const refs = useRef<{ [id: string]: HTMLDivElement }>({})
|
||||
useEffect(() => {
|
||||
// remove refs that are not in entries anymore
|
||||
Object.keys(refs.current).forEach(k => {
|
||||
const found = entries.some(e => e.id === k)
|
||||
if (!found) delete refs.current[k]
|
||||
})
|
||||
}, [entries])
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
|
||||
// scroll to entry when selected entry changes
|
||||
useEffect(() => {
|
||||
if (!selectedEntryId) return
|
||||
if (!selectedEntry?.expanded) return
|
||||
const listener = () => {
|
||||
if (viewMode !== "expanded") return
|
||||
if (scrollingToEntry) return
|
||||
|
||||
const selectedEntryElement = refs.current[selectedEntryId]
|
||||
if (Constants.layout.isTopVisible(selectedEntryElement) && Constants.layout.isBottomVisible(selectedEntryElement)) return
|
||||
const currentEntry = entries
|
||||
// use slice to get a copy of the array because reverse mutates the array in-place
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(e => {
|
||||
const el = document.getElementById(Constants.dom.entryId(e))
|
||||
return el && !Constants.layout.isTopVisible(el)
|
||||
})
|
||||
if (currentEntry) {
|
||||
dispatch(
|
||||
selectEntry({
|
||||
entry: currentEntry,
|
||||
expand: false,
|
||||
markAsRead: !!scrollMarks,
|
||||
scrollToEntry: false,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
const throttledListener = throttle(listener, 100)
|
||||
scrollArea?.addEventListener("scroll", throttledListener)
|
||||
return () => scrollArea?.removeEventListener("scroll", throttledListener)
|
||||
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
|
||||
|
||||
document.getElementById(Constants.dom.mainScrollAreaId)?.scrollTo({
|
||||
// having a small gap between the top of the content and the top of the page is sexier
|
||||
top: selectedEntryElement.offsetTop - 3,
|
||||
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
|
||||
})
|
||||
}, [selectedEntryId, selectedEntry?.expanded, scrollSpeed])
|
||||
|
||||
useMousetrap("r", () => {
|
||||
dispatch(reloadEntries())
|
||||
})
|
||||
useMousetrap("j", () => {
|
||||
useMousetrap("r", () => dispatch(reloadEntries()))
|
||||
useMousetrap("j", () =>
|
||||
dispatch(
|
||||
selectNextEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
useMousetrap("n", () => {
|
||||
)
|
||||
useMousetrap("n", () =>
|
||||
dispatch(
|
||||
selectNextEntry({
|
||||
expand: false,
|
||||
markAsRead: false,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
useMousetrap("k", () => {
|
||||
)
|
||||
useMousetrap("k", () =>
|
||||
dispatch(
|
||||
selectPreviousEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
useMousetrap("p", () => {
|
||||
)
|
||||
useMousetrap("p", () =>
|
||||
dispatch(
|
||||
selectPreviousEntry({
|
||||
expand: false,
|
||||
markAsRead: false,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
useMousetrap("space", () => {
|
||||
if (selectedEntry) {
|
||||
if (selectedEntry.expanded) {
|
||||
const ref = refs.current[selectedEntry.id]
|
||||
if (Constants.layout.isBottomVisible(ref)) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry))
|
||||
if (entryElement && Constants.layout.isBottomVisible(entryElement)) {
|
||||
dispatch(
|
||||
selectNextEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
@@ -135,6 +147,7 @@ export function FeedEntries() {
|
||||
entry: selectedEntry,
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -143,6 +156,7 @@ export function FeedEntries() {
|
||||
selectNextEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -150,12 +164,13 @@ export function FeedEntries() {
|
||||
useMousetrap("shift+space", () => {
|
||||
if (selectedEntry) {
|
||||
if (selectedEntry.expanded) {
|
||||
const ref = refs.current[selectedEntry.id]
|
||||
if (Constants.layout.isTopVisible(ref)) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry))
|
||||
if (entryElement && Constants.layout.isTopVisible(entryElement)) {
|
||||
dispatch(
|
||||
selectPreviousEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
@@ -170,6 +185,7 @@ export function FeedEntries() {
|
||||
selectPreviousEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -183,6 +199,7 @@ export function FeedEntries() {
|
||||
entry: selectedEntry,
|
||||
expand: !selectedEntry.expanded,
|
||||
markAsRead: !selectedEntry.expanded,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -222,12 +239,8 @@ export function FeedEntries() {
|
||||
})
|
||||
)
|
||||
})
|
||||
useMousetrap("g a", () => {
|
||||
dispatch(redirectToRootCategory())
|
||||
})
|
||||
useMousetrap("?", () => {
|
||||
openModal({ title: t`Keyboard shortcuts`, size: "xl", children: <KeyboardShortcutsHelp /> })
|
||||
})
|
||||
useMousetrap("g a", () => dispatch(redirectToRootCategory()))
|
||||
useMousetrap("?", () => openModal({ title: t`Keyboard shortcuts`, size: "xl", children: <KeyboardShortcutsHelp /> }))
|
||||
|
||||
if (!entries) return <Loader />
|
||||
return (
|
||||
@@ -243,7 +256,7 @@ export function FeedEntries() {
|
||||
<div
|
||||
key={entry.id}
|
||||
ref={el => {
|
||||
if (el) refs.current[entry.id] = el
|
||||
if (el) el.id = Constants.dom.entryId(entry)
|
||||
}}
|
||||
>
|
||||
<FeedEntry
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { changeLanguage, changeScrollSpeed, changeSharingSetting, changeShowRead } from "app/slices/user"
|
||||
import { changeLanguage, changeScrollMarks, changeScrollSpeed, changeSharingSetting, changeShowRead } from "app/slices/user"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { SharingSettings } from "app/types"
|
||||
import { locales } from "i18n"
|
||||
@@ -10,6 +10,7 @@ export function DisplaySettings() {
|
||||
const language = useAppSelector(state => state.user.settings?.language)
|
||||
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@@ -37,6 +38,12 @@ export function DisplaySettings() {
|
||||
onChange={e => dispatch(changeShowRead(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={t`In expanded view, scrolling through entries mark them as read`}
|
||||
checked={scrollMarks}
|
||||
onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={t`Sharing sites`} labelPosition="center" />
|
||||
|
||||
<SimpleGrid cols={2}>
|
||||
|
||||
@@ -333,6 +333,10 @@ msgstr "If you like this project, please consider a donation to support the deve
|
||||
msgid "Import"
|
||||
msgstr "Import"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "In expanded view, scrolling through entries mark them as read"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Keep unread"
|
||||
|
||||
@@ -333,6 +333,10 @@ msgstr "Si vous aimez ce projet, n'hésitez pas à faire un don pour encourager
|
||||
msgid "Import"
|
||||
msgstr "Importer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "En mode de lecture étendu, marquer les éléments comme lus lorsque la fenêtre descend."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Garder non lu"
|
||||
|
||||
Reference in New Issue
Block a user