import { Trans } from "@lingui/macro" import { openModal } from "@mantine/modals" import { Constants } from "app/constants" import { ExpendableEntry, loadMoreEntries, markAllEntries, markEntry, reloadEntries, selectEntry, selectNextEntry, selectPreviousEntry, } from "app/slices/entries" import { redirectToRootCategory } from "app/slices/redirect" import { useAppDispatch, useAppSelector } from "app/store" import { openLinkInBackgroundTab } from "app/utils" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { Loader } from "components/Loader" import { useMousetrap } from "hooks/useMousetrap" import { useViewMode } from "hooks/useViewMode" import throttle from "lodash/throttle" import { useEffect } from "react" import InfiniteScroll from "react-infinite-scroller" import { FeedEntry } from "./FeedEntry" export function FeedEntries() { const source = useAppSelector(state => state.entries.source) const entries = useAppSelector(state => state.entries.entries) const entriesTimestamp = useAppSelector(state => state.entries.timestamp) const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId) const hasMore = useAppSelector(state => state.entries.hasMore) const { viewMode } = useViewMode() 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) 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, scrollToEntry: true, }) ) } } useEffect(() => { const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) const listener = () => { if (viewMode !== "expanded") return if (scrollingToEntry) 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]) useMousetrap("r", () => dispatch(reloadEntries())) useMousetrap("j", () => dispatch( selectNextEntry({ expand: true, markAsRead: true, scrollToEntry: true, }) ) ) useMousetrap("n", () => dispatch( selectNextEntry({ expand: false, markAsRead: false, scrollToEntry: true, }) ) ) useMousetrap("k", () => dispatch( selectPreviousEntry({ expand: true, markAsRead: true, scrollToEntry: true, }) ) ) useMousetrap("p", () => dispatch( selectPreviousEntry({ expand: false, markAsRead: false, scrollToEntry: true, }) ) ) useMousetrap("space", () => { if (selectedEntry) { if (selectedEntry.expanded) { const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry)) if (entryElement && Constants.layout.isBottomVisible(entryElement)) { dispatch( selectNextEntry({ expand: true, markAsRead: true, scrollToEntry: true, }) ) } else { const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) scrollArea?.scrollTo({ top: scrollArea.scrollTop + scrollArea.clientHeight * 0.8, behavior: "smooth", }) } } else { dispatch( selectEntry({ entry: selectedEntry, expand: true, markAsRead: true, scrollToEntry: true, }) ) } } else { dispatch( selectNextEntry({ expand: true, markAsRead: true, scrollToEntry: true, }) ) } }) useMousetrap("shift+space", () => { if (selectedEntry) { if (selectedEntry.expanded) { const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry)) if (entryElement && Constants.layout.isTopVisible(entryElement)) { dispatch( selectPreviousEntry({ expand: true, markAsRead: true, scrollToEntry: true, }) ) } else { const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) scrollArea?.scrollTo({ top: scrollArea.scrollTop - scrollArea.clientHeight * 0.8, behavior: "smooth", }) } } else { dispatch( selectPreviousEntry({ expand: true, markAsRead: true, scrollToEntry: true, }) ) } } }) useMousetrap(["o", "enter"], () => { // toggle expanded status if (!selectedEntry) return dispatch( selectEntry({ entry: selectedEntry, expand: !selectedEntry.expanded, markAsRead: !selectedEntry.expanded, scrollToEntry: true, }) ) }) useMousetrap("v", () => { // open tab in foreground if (!selectedEntry) return window.open(selectedEntry.url, "_blank", "noreferrer") }) useMousetrap("b", () => { // simulate ctrl+click to open tab in background if (!selectedEntry) return openLinkInBackgroundTab(selectedEntry.url) }) useMousetrap("m", () => { // toggle read status if (!selectedEntry) return dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read })) }) useMousetrap("shift+a", () => { // mark all entries as read dispatch( markAllEntries({ sourceType: source.type, req: { id: source.id, read: true, olderThan: entriesTimestamp, }, }) ) }) useMousetrap("g a", () => dispatch(redirectToRootCategory())) useMousetrap("?", () => openModal({ title: Keyboard shortcuts, size: "xl", children: , }) ) if (!entries) return return ( dispatch(loadMoreEntries())} hasMore={hasMore} loader={} useWindow={false} getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)} > {entries.map(entry => (
{ if (el) el.id = Constants.dom.entryId(entry) }} > headerClicked(entry, event)} />
))}
) }