mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge branch 'main' into feat/add-links-in-search
This commit is contained in:
commit
b5a6b86915
17 changed files with 2892 additions and 135 deletions
|
|
@ -5,7 +5,7 @@ buildscript {
|
|||
hilt_version = '2.44.2'
|
||||
gradle_plugin_version = '7.4.2'
|
||||
room_version = '2.4.3'
|
||||
kotlin_version = '1.9.0'
|
||||
kotlin_version = '1.7.10'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { WeixinQqHandler } from './websites/weixin-qq-handler'
|
|||
import { WikipediaHandler } from './websites/wikipedia-handler'
|
||||
import { YoutubeHandler } from './websites/youtube-handler'
|
||||
import { TheAtlanticHandler } from './websites/the-atlantic-handler'
|
||||
import { ArsTechnicaHandler } from './websites/ars-technica-handler'
|
||||
|
||||
const validateUrlString = (url: string): boolean => {
|
||||
const u = new URL(url)
|
||||
|
|
@ -57,6 +58,7 @@ const validateUrlString = (url: string): boolean => {
|
|||
}
|
||||
|
||||
const contentHandlers: ContentHandler[] = [
|
||||
new ArsTechnicaHandler(),
|
||||
new TheAtlanticHandler(),
|
||||
new AppleNewsHandler(),
|
||||
new BloombergHandler(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import axios from 'axios'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import { ContentHandler, PreHandleResult } from '../content-handler'
|
||||
|
||||
/**
|
||||
* Some of the content on Ars Technica is split over several pages.
|
||||
* If this is the case we should unfurl the entire article into one. l
|
||||
*/
|
||||
export class ArsTechnicaHandler extends ContentHandler {
|
||||
constructor() {
|
||||
super()
|
||||
this.name = 'ArsTechnica'
|
||||
}
|
||||
|
||||
shouldPreHandle(url: string): boolean {
|
||||
const u = new URL(url)
|
||||
return u.hostname.endsWith('arstechnica.com')
|
||||
}
|
||||
|
||||
hasMultiplePages(document: Document): boolean {
|
||||
return document.querySelectorAll('nav.page-numbers')?.length != 0
|
||||
}
|
||||
|
||||
async grabContentFromUrl(url: string): Promise<Document> {
|
||||
const response = await axios.get(url)
|
||||
const data = response.data as string
|
||||
return parseHTML(data).document
|
||||
}
|
||||
|
||||
async extractArticleContentsFromLink(url: string): Promise<Document[]> {
|
||||
const dom = await this.grabContentFromUrl(url)
|
||||
const articleContent = dom.querySelector('[itemprop="articleBody"]')
|
||||
return [].slice.call(articleContent?.childNodes || [])
|
||||
}
|
||||
|
||||
async expandLinksAndCombine(document: Document): Promise<Document> {
|
||||
const pageNumbers = document.querySelector('nav.page-numbers')
|
||||
const articleBody = document.querySelector('[itemprop="articleBody"]')
|
||||
|
||||
if (!pageNumbers || !articleBody) {
|
||||
// We shouldn't ever really get here, but sometimes weird things happen.
|
||||
return document
|
||||
}
|
||||
|
||||
const pageLinkNodes = pageNumbers.querySelectorAll('a')
|
||||
// Remove the "Next" Link, as it will duplicate some content.
|
||||
const pageLinks =
|
||||
Array.from(pageLinkNodes)
|
||||
?.slice(0, pageLinkNodes.length - 1)
|
||||
?.map(({ href }) => href) ?? []
|
||||
|
||||
const pageContents = await Promise.all(
|
||||
pageLinks.map(this.extractArticleContentsFromLink.bind(this))
|
||||
)
|
||||
|
||||
for (const articleContents of pageContents) {
|
||||
// We place all the content in a span to indicate that a page has been parsed.
|
||||
const span = document.createElement('SPAN')
|
||||
span.className = 'nextPageContents'
|
||||
span.append(...articleContents)
|
||||
articleBody.append(span)
|
||||
}
|
||||
pageNumbers.remove()
|
||||
|
||||
return document
|
||||
}
|
||||
|
||||
async preHandle(url: string): Promise<PreHandleResult> {
|
||||
// We simply retrieve the article without Javascript enabled using a GET command.
|
||||
const dom = await this.grabContentFromUrl(url)
|
||||
if (!this.hasMultiplePages(dom)) {
|
||||
return {
|
||||
content: dom.body.outerHTML,
|
||||
title: dom.title,
|
||||
dom,
|
||||
}
|
||||
}
|
||||
|
||||
const expandedDom = await this.expandLinksAndCombine(dom)
|
||||
return {
|
||||
content: expandedDom.body.outerHTML,
|
||||
title: dom.title,
|
||||
dom: expandedDom,
|
||||
}
|
||||
}
|
||||
}
|
||||
82
packages/content-handler/test/ars-technica.test.ts
Normal file
82
packages/content-handler/test/ars-technica.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { ArsTechnicaHandler } from '../src/websites/ars-technica-handler'
|
||||
import fs from 'fs'
|
||||
import nock from 'nock'
|
||||
import { expect } from 'chai'
|
||||
import { parseHTML } from 'linkedom'
|
||||
|
||||
describe('Testing parsing multi-page articles from arstechnica.', () => {
|
||||
let orignalArticle: Document | undefined
|
||||
let htmlPg1: string | null
|
||||
let htmlPg2: string | null
|
||||
let htmlPg3: string | null
|
||||
|
||||
const load = (path: string): string => {
|
||||
return fs.readFileSync(path, 'utf8')
|
||||
}
|
||||
|
||||
before(() => {
|
||||
htmlPg1 = load('./test/data/ars-multipage/ars-technica-page-1.html')
|
||||
htmlPg2 = load('./test/data/ars-multipage/ars-technica-page-2.html')
|
||||
htmlPg3 = load('./test/data/ars-multipage/ars-technica-page-3.html')
|
||||
|
||||
orignalArticle = parseHTML(htmlPg1).document
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
nock('https://arstechnica.com').get('/article/').reply(200, htmlPg1!)
|
||||
nock('https://arstechnica.com').get('/article/2/').reply(200, htmlPg2!)
|
||||
nock('https://arstechnica.com').get('/article/3/').reply(200, htmlPg3!)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
})
|
||||
|
||||
it('should parse the title of the atlantic article.', async () => {
|
||||
const response = await new ArsTechnicaHandler().preHandle(
|
||||
'https://arstechnica.com/article/'
|
||||
)
|
||||
|
||||
// We grab the title from the doucment.
|
||||
expect(response.title).not.to.be.undefined
|
||||
expect(response.title).to.equal(
|
||||
'What’s going on with the reports of a room-temperature superconductor? | Ars Technica'
|
||||
)
|
||||
})
|
||||
|
||||
it('should remove the navigation links', async () => {
|
||||
const response = await new ArsTechnicaHandler().preHandle(
|
||||
'https://arstechnica.com/article/'
|
||||
)
|
||||
|
||||
expect(orignalArticle?.querySelector('nav.page-numbers')).not.to.be.null
|
||||
expect(response.dom?.querySelectorAll('nav.page-numbers').length).to.equal(0);
|
||||
})
|
||||
|
||||
it('should append all new content into the main article', async () => {
|
||||
const response = await new ArsTechnicaHandler().preHandle(
|
||||
'https://arstechnica.com/article/'
|
||||
)
|
||||
|
||||
// We name the div to ensure we can validate that it has been inserted.
|
||||
expect(
|
||||
orignalArticle?.getElementsByClassName('nextPageContents')?.length || 0
|
||||
).to.equal(0)
|
||||
expect(
|
||||
response.dom?.getElementsByClassName('nextPageContents')?.length || 0
|
||||
).not.to.equal(0)
|
||||
})
|
||||
|
||||
it('should remove any related content links.', async () => {
|
||||
const response = await new ArsTechnicaHandler().preHandle(
|
||||
'https://arstechnica.com/article/'
|
||||
)
|
||||
|
||||
// This exists in the HTML, but we remove it when preparsing.
|
||||
expect(
|
||||
response.dom?.getElementsByClassName(
|
||||
'ArticleRelatedContentModule_root__BBa6g'
|
||||
).length
|
||||
).to.eql(0)
|
||||
})
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -77,9 +77,9 @@ export const Button = styled('button', {
|
|||
fontFamily: 'Inter',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
color: '$grayTextContrast',
|
||||
color: 'white',
|
||||
p: '10px 12px',
|
||||
bg: 'rgb(125, 125, 125, 0.1)',
|
||||
bg: 'rgb(125, 125, 125, 0.3)',
|
||||
'&:hover': {
|
||||
bg: 'rgb(47, 47, 47, 0.1)',
|
||||
'.ctaButtonIcon': {
|
||||
|
|
|
|||
|
|
@ -479,9 +479,12 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
if (textToCopy) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy)
|
||||
showSuccessToast('Highlight copied', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
showSuccessToast(
|
||||
focusedHighlight ? 'Highlight copied' : 'Text copied',
|
||||
{
|
||||
position: 'bottom-right',
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
showErrorToast('Error copying highlight, permission denied.', {
|
||||
position: 'bottom-right',
|
||||
|
|
|
|||
|
|
@ -1,41 +1,55 @@
|
|||
import { Action, createAction, useKBar, useRegisterActions } from "kbar"
|
||||
import { articleQuery } from "../../../lib/networking/queries/useGetArticleQuery"
|
||||
import debounce from "lodash/debounce"
|
||||
import { useRouter } from "next/router"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Action, createAction, useKBar, useRegisterActions } from 'kbar'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import toast, { Toaster } from "react-hot-toast"
|
||||
import TopBarProgress from "react-topbar-progress-indicator"
|
||||
import { useFetchMore } from "../../../lib/hooks/useFetchMoreScroll"
|
||||
import { usePersistedState } from "../../../lib/hooks/usePersistedState"
|
||||
import { libraryListCommands } from "../../../lib/keyboardShortcuts/navigationShortcuts"
|
||||
import { useKeyboardShortcuts } from "../../../lib/keyboardShortcuts/useKeyboardShortcuts"
|
||||
import { PageType, State } from "../../../lib/networking/fragments/articleFragment"
|
||||
import TopBarProgress from 'react-topbar-progress-indicator'
|
||||
import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { libraryListCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import {
|
||||
PageType,
|
||||
State,
|
||||
} from '../../../lib/networking/fragments/articleFragment'
|
||||
import {
|
||||
SearchItem,
|
||||
TypeaheadSearchItemsData,
|
||||
typeaheadSearchQuery
|
||||
} from "../../../lib/networking/queries/typeaheadSearch"
|
||||
import type { LibraryItem, LibraryItemsQueryInput } from "../../../lib/networking/queries/useGetLibraryItemsQuery"
|
||||
import { useGetLibraryItemsQuery } from "../../../lib/networking/queries/useGetLibraryItemsQuery"
|
||||
import { useGetViewerQuery, UserBasicData } from "../../../lib/networking/queries/useGetViewerQuery"
|
||||
import { Button } from "../../elements/Button"
|
||||
import { StyledText } from "../../elements/StyledText"
|
||||
import { ConfirmationModal } from "../../patterns/ConfirmationModal"
|
||||
import { LinkedItemCardAction } from "../../patterns/LibraryCards/CardTypes"
|
||||
import { LinkedItemCard } from "../../patterns/LibraryCards/LinkedItemCard"
|
||||
import { Box, HStack, VStack } from "./../../elements/LayoutPrimitives"
|
||||
import { AddLinkModal } from "./AddLinkModal"
|
||||
import { EditLibraryItemModal } from "./EditItemModals"
|
||||
import { EmptyLibrary } from "./EmptyLibrary"
|
||||
import { HighlightItemsLayout } from "./HighlightsLayout"
|
||||
import { LibraryFilterMenu } from "./LibraryFilterMenu"
|
||||
import { LibraryHeader, MultiSelectMode } from "./LibraryHeader"
|
||||
import { UploadModal } from "../UploadModal"
|
||||
import { BulkAction, bulkActionMutation } from "../../../lib/networking/mutations/bulkActionMutation"
|
||||
import { showErrorToast, showSuccessToast } from "../../../lib/toastHelpers"
|
||||
import { SetPageLabelsModalPresenter } from "../article/SetLabelsModalPresenter"
|
||||
import { NotebookPresenter } from "../article/NotebookPresenter"
|
||||
typeaheadSearchQuery,
|
||||
} from '../../../lib/networking/queries/typeaheadSearch'
|
||||
import type {
|
||||
LibraryItem,
|
||||
LibraryItemsQueryInput,
|
||||
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import {
|
||||
useGetViewerQuery,
|
||||
UserBasicData,
|
||||
} from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
|
||||
import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes'
|
||||
import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard'
|
||||
import { Box, HStack, VStack } from './../../elements/LayoutPrimitives'
|
||||
import { AddLinkModal } from './AddLinkModal'
|
||||
import { EditLibraryItemModal } from './EditItemModals'
|
||||
import { EmptyLibrary } from './EmptyLibrary'
|
||||
import { HighlightItemsLayout } from './HighlightsLayout'
|
||||
import { LibraryFilterMenu } from './LibraryFilterMenu'
|
||||
import { LibraryHeader, MultiSelectMode } from './LibraryHeader'
|
||||
import { UploadModal } from '../UploadModal'
|
||||
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showSuccessToastWithUndo,
|
||||
} from '../../../lib/toastHelpers'
|
||||
import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter'
|
||||
import { NotebookPresenter } from '../article/NotebookPresenter'
|
||||
import { saveUrlMutation } from "../../../lib/networking/mutations/saveUrlMutation"
|
||||
import { articleQuery } from "../../../lib/networking/queries/useGetArticleQuery"
|
||||
|
||||
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
|
||||
export type LibraryMode = 'reads' | 'highlights'
|
||||
|
|
@ -83,7 +97,6 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
|
||||
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
|
||||
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
|
||||
const [linkToRemove, setLinkToRemove] = useState<LibraryItem>()
|
||||
const [linkToEdit, setLinkToEdit] = useState<LibraryItem>()
|
||||
const [linkToUnsubscribe, setLinkToUnsubscribe] = useState<LibraryItem>()
|
||||
|
||||
|
|
@ -99,6 +112,19 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
mutate,
|
||||
} = useGetLibraryItemsQuery(queryInputs)
|
||||
|
||||
useEffect(() => {
|
||||
const handleRevalidate = () => {
|
||||
;(async () => {
|
||||
console.log('revalidating library')
|
||||
await mutate()
|
||||
})()
|
||||
}
|
||||
document.addEventListener('revalidateLibrary', handleRevalidate)
|
||||
return () => {
|
||||
document.removeEventListener('revalidateLibrary', handleRevalidate)
|
||||
}
|
||||
}, [mutate])
|
||||
|
||||
useEffect(() => {
|
||||
if (queryValue.startsWith('#')) {
|
||||
debouncedFetchSearchResults(
|
||||
|
|
@ -404,8 +430,8 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
}
|
||||
|
||||
const modalTargetItem = useMemo(() => {
|
||||
return labelsTarget || linkToEdit || linkToRemove || linkToUnsubscribe
|
||||
}, [labelsTarget, linkToEdit, linkToRemove, linkToUnsubscribe])
|
||||
return labelsTarget || linkToEdit || linkToUnsubscribe
|
||||
}, [labelsTarget, linkToEdit, linkToUnsubscribe])
|
||||
|
||||
const [checkedItems, setCheckedItems] = useState<string[]>([])
|
||||
const [multiSelectMode, setMultiSelectMode] = useState<MultiSelectMode>('off')
|
||||
|
|
@ -827,8 +853,6 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
setActiveItem={(item: LibraryItem) => {
|
||||
activateCard(item.node.id)
|
||||
}}
|
||||
linkToRemove={linkToRemove}
|
||||
setLinkToRemove={setLinkToRemove}
|
||||
linkToEdit={linkToEdit}
|
||||
setLinkToEdit={setLinkToEdit}
|
||||
linkToUnsubscribe={linkToUnsubscribe}
|
||||
|
|
@ -865,8 +889,6 @@ type HomeFeedContentProps = {
|
|||
setShowEditTitleModal: (show: boolean) => void
|
||||
setActiveItem: (item: LibraryItem) => void
|
||||
|
||||
linkToRemove: LibraryItem | undefined
|
||||
setLinkToRemove: (set: LibraryItem | undefined) => void
|
||||
linkToEdit: LibraryItem | undefined
|
||||
setLinkToEdit: (set: LibraryItem | undefined) => void
|
||||
linkToUnsubscribe: LibraryItem | undefined
|
||||
|
|
@ -983,23 +1005,11 @@ type LibraryItemsLayoutProps = {
|
|||
} & HomeFeedContentProps
|
||||
|
||||
function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
|
||||
const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] =
|
||||
useState(false)
|
||||
const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] =
|
||||
useState(false)
|
||||
const [showUploadModal, setShowUploadModal] = useState(false)
|
||||
const [, updateState] = useState({})
|
||||
|
||||
const removeItem = () => {
|
||||
if (!props.linkToRemove) {
|
||||
return
|
||||
}
|
||||
|
||||
props.actionHandler('delete', props.linkToRemove)
|
||||
props.setLinkToRemove(undefined)
|
||||
setShowRemoveLinkConfirmation(false)
|
||||
}
|
||||
|
||||
const unsubscribe = () => {
|
||||
if (!props.linkToUnsubscribe) {
|
||||
return
|
||||
|
|
@ -1049,9 +1059,7 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
|
|||
setShowEditTitleModal={props.setShowEditTitleModal}
|
||||
setLinkToEdit={props.setLinkToEdit}
|
||||
setShowUnsubscribeConfirmation={setShowUnsubscribeConfirmation}
|
||||
setLinkToRemove={props.setLinkToRemove}
|
||||
setLinkToUnsubscribe={props.setLinkToUnsubscribe}
|
||||
setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation}
|
||||
actionHandler={props.actionHandler}
|
||||
multiSelectMode={props.multiSelectMode}
|
||||
/>
|
||||
|
|
@ -1086,43 +1094,6 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
|
|||
item={props.linkToEdit as LibraryItem}
|
||||
/>
|
||||
)}
|
||||
{showRemoveLinkConfirmation && (
|
||||
<ConfirmationModal
|
||||
richMessage={
|
||||
<VStack alignment="center" distribution="center">
|
||||
<StyledText style="modalTitle" css={{ margin: '0px 8px' }}>
|
||||
Are you sure you want to delete this item? All associated notes
|
||||
and highlights will be deleted.
|
||||
</StyledText>
|
||||
{props.linkToRemove?.node && props.viewer && (
|
||||
<Box
|
||||
css={{
|
||||
transform: 'scale(0.6)',
|
||||
opacity: 0.8,
|
||||
pointerEvents: 'none',
|
||||
filter: 'grayscale(1)',
|
||||
}}
|
||||
>
|
||||
<LinkedItemCard
|
||||
item={props.linkToRemove?.node}
|
||||
viewer={props.viewer}
|
||||
layout="GRID_LAYOUT"
|
||||
multiSelectMode={props.multiSelectMode}
|
||||
isChecked={false}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setIsChecked={() => {}}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handleAction={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
}
|
||||
onAccept={removeItem}
|
||||
acceptButtonLabel="Delete Item"
|
||||
onOpenChange={() => setShowRemoveLinkConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{showUnsubscribeConfirmation && (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure you want to unsubscribe?'}
|
||||
|
|
@ -1174,9 +1145,7 @@ type LibraryItemsProps = {
|
|||
setShowEditTitleModal: (show: boolean) => void
|
||||
setLinkToEdit: (set: LibraryItem | undefined) => void
|
||||
setShowUnsubscribeConfirmation: (show: true) => void
|
||||
setLinkToRemove: (set: LibraryItem | undefined) => void
|
||||
setLinkToUnsubscribe: (set: LibraryItem | undefined) => void
|
||||
setShowRemoveLinkConfirmation: (show: true) => void
|
||||
|
||||
isChecked: (itemId: string) => boolean
|
||||
setIsChecked: (itemId: string, set: boolean) => void
|
||||
|
|
@ -1272,10 +1241,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
|
|||
setIsChecked={props.setIsChecked}
|
||||
multiSelectMode={props.multiSelectMode}
|
||||
handleAction={(action: LinkedItemCardAction) => {
|
||||
if (action === 'delete') {
|
||||
props.setShowRemoveLinkConfirmation(true)
|
||||
props.setLinkToRemove(linkedItem)
|
||||
} else if (action === 'editTitle') {
|
||||
if (action === 'editTitle') {
|
||||
props.setShowEditTitleModal(true)
|
||||
props.setLinkToEdit(linkedItem)
|
||||
} else if (action == 'unsubscribe') {
|
||||
|
|
|
|||
|
|
@ -32,13 +32,10 @@ export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
|
|||
height: HEADER_HEIGHT,
|
||||
display: props.alwaysDisplayToolbar ? 'flex' : 'transparent',
|
||||
pointerEvents: props.alwaysDisplayToolbar ? 'unset' : 'none',
|
||||
borderBottom: props.alwaysDisplayToolbar
|
||||
? '1px solid $thBorderColor'
|
||||
: '1px solid transparent',
|
||||
borderBottom: '1px solid transparent',
|
||||
'@xlgDown': {
|
||||
bg: '$readerBg',
|
||||
pointerEvents: 'unset',
|
||||
borderBottom: '1px solid $thBorderColor',
|
||||
},
|
||||
'@mdDown': {
|
||||
bg: '$readerBg',
|
||||
|
|
|
|||
|
|
@ -15,11 +15,9 @@ export type ReaderSettings = {
|
|||
setMarginWidth: (newMarginWidth: number) => void
|
||||
|
||||
showSetLabelsModal: boolean
|
||||
showDeleteConfirmation: boolean
|
||||
showEditDisplaySettingsModal: boolean
|
||||
|
||||
setShowSetLabelsModal: (showSetLabelsModal: boolean) => void
|
||||
setShowDeleteConfirmation: (showDeleteConfirmation: boolean) => void
|
||||
setShowEditDisplaySettingsModal: (
|
||||
showEditDisplaySettingsModal: boolean
|
||||
) => void
|
||||
|
|
@ -70,7 +68,6 @@ export const useReaderSettings = (): ReaderSettings => {
|
|||
const [showSetLabelsModal, setShowSetLabelsModal] = useState(false)
|
||||
const [showEditDisplaySettingsModal, setShowEditDisplaySettingsModal] =
|
||||
useState(false)
|
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false)
|
||||
|
||||
const updateFontSize = useCallback(
|
||||
(newFontSize: number) => {
|
||||
|
|
@ -209,12 +206,10 @@ export const useReaderSettings = (): ReaderSettings => {
|
|||
setFontSize,
|
||||
setLineHeight,
|
||||
setMarginWidth,
|
||||
showDeleteConfirmation,
|
||||
showSetLabelsModal,
|
||||
showEditDisplaySettingsModal,
|
||||
setShowSetLabelsModal,
|
||||
setShowEditDisplaySettingsModal,
|
||||
setShowDeleteConfirmation,
|
||||
actionHandler,
|
||||
setFontFamily,
|
||||
fontFamily,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { State } from '../fragments/articleFragment'
|
||||
|
||||
export type UpdatePageInput = {
|
||||
pageId: string
|
||||
title: string
|
||||
title?: string
|
||||
byline?: string | undefined
|
||||
description: string
|
||||
description?: string
|
||||
savedAt?: string
|
||||
publishedAt?: string
|
||||
state?: State
|
||||
}
|
||||
|
||||
export async function updatePageMutation(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import type { PageType, State } from '../fragments/articleFragment'
|
||||
import { PageType, State } from '../fragments/articleFragment'
|
||||
import { ContentReader } from '../fragments/articleFragment'
|
||||
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
|
||||
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
|
||||
import { unsubscribeMutation } from '../mutations/unsubscribeMutation'
|
||||
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
|
||||
import { Label } from './../fragments/labelFragment'
|
||||
import { showErrorToast, showSuccessToast } from '../../toastHelpers'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showSuccessToastWithUndo,
|
||||
} from '../../toastHelpers'
|
||||
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
|
||||
import { updatePageMutation } from '../mutations/updatePageMutation'
|
||||
|
||||
export interface ReadableItem {
|
||||
id: string
|
||||
|
|
@ -344,9 +349,26 @@ export function useGetLibraryItemsQuery({
|
|||
break
|
||||
case 'delete':
|
||||
updateData(undefined)
|
||||
deleteLinkMutation(item.node.id).then((res) => {
|
||||
|
||||
const pageId = item.node.id
|
||||
deleteLinkMutation(pageId).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToast('Link removed', { position: 'bottom-right' })
|
||||
showSuccessToastWithUndo('Page deleted', async () => {
|
||||
const result = await updatePageMutation({
|
||||
pageId: pageId,
|
||||
state: State.SUCCEEDED,
|
||||
})
|
||||
|
||||
mutate()
|
||||
|
||||
if (result) {
|
||||
showSuccessToast('Page recovered')
|
||||
} else {
|
||||
showErrorToast(
|
||||
'Error recovering page, check your deleted items'
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error removing link', { position: 'bottom-right' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { toast, ToastOptions } from 'react-hot-toast'
|
|||
import { CheckCircle, WarningCircle, X } from 'phosphor-react'
|
||||
import { Box, HStack } from '../components/elements/LayoutPrimitives'
|
||||
import { styled } from '@stitches/react'
|
||||
import { Button } from '../components/elements/Button'
|
||||
|
||||
const toastStyles = {
|
||||
minWidth: 265,
|
||||
|
|
@ -67,10 +68,64 @@ const showToast = (
|
|||
)
|
||||
}
|
||||
|
||||
const showToastWithUndo = (
|
||||
message: string,
|
||||
background: string,
|
||||
undoAction: () => Promise<void>,
|
||||
options?: ToastOptions
|
||||
) => {
|
||||
return toast(
|
||||
({ id }) => (
|
||||
<FullWidthContainer alignment="center">
|
||||
<CheckCircle size={24} color="white" />
|
||||
<MessageContainer>{message}</MessageContainer>
|
||||
<HStack distribution="end" css={{ marginLeft: 16 }}>
|
||||
<Button
|
||||
style="ctaLightGray"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
|
||||
toast.dismiss(id)
|
||||
;(async () => {
|
||||
await undoAction()
|
||||
})()
|
||||
}}
|
||||
>
|
||||
Undo
|
||||
</Button>
|
||||
</HStack>
|
||||
</FullWidthContainer>
|
||||
),
|
||||
{
|
||||
style: {
|
||||
...toastStyles,
|
||||
background: background,
|
||||
},
|
||||
duration: 3500,
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const showSuccessToast = (message: string, options?: ToastOptions) => {
|
||||
return showToast(message, '#55B938', 'success', options)
|
||||
return showToast(message, '#55B938', 'success', {
|
||||
position: 'bottom-right',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const showErrorToast = (message: string, options?: ToastOptions) => {
|
||||
return showToast(message, '#cc0000', 'error', options)
|
||||
return showToast(message, '#cc0000', 'error', {
|
||||
position: 'bottom-right',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const showSuccessToastWithUndo = (
|
||||
message: string,
|
||||
undoAction: () => Promise<void>
|
||||
) => {
|
||||
return showToastWithUndo(message, '#55B938', undoAction, {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const moduleExports = {
|
|||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/settings/rss/',
|
||||
source: '/settings/rss',
|
||||
destination: '/settings/feeds',
|
||||
permanent: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ import { ArticleActionsMenu } from '../../../components/templates/article/Articl
|
|||
import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showSuccessToastWithUndo,
|
||||
} from '../../../lib/toastHelpers'
|
||||
import { SetLabelsModal } from '../../../components/templates/article/SetLabelsModal'
|
||||
import { DisplaySettingsModal } from '../../../components/templates/article/DisplaySettingsModal'
|
||||
import { useReaderSettings } from '../../../lib/hooks/useReaderSettings'
|
||||
|
|
@ -41,6 +45,8 @@ import { VerticalArticleActionsMenu } from '../../../components/templates/articl
|
|||
import { PdfHeaderSpacer } from '../../../components/templates/article/PdfHeaderSpacer'
|
||||
import { EpubContainerProps } from '../../../components/templates/article/EpubContainer'
|
||||
import { useSetPageLabels } from '../../../lib/hooks/useSetPageLabels'
|
||||
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
|
||||
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
|
||||
() => import('./../../../components/templates/article/PdfArticleContainer'),
|
||||
|
|
@ -138,7 +144,7 @@ export default function Home(): JSX.Element {
|
|||
}
|
||||
break
|
||||
case 'delete':
|
||||
readerSettings.setShowDeleteConfirmation(true)
|
||||
await deleteCurrentItem()
|
||||
break
|
||||
case 'openOriginalArticle':
|
||||
const url = article?.url
|
||||
|
|
@ -206,10 +212,23 @@ export default function Home(): JSX.Element {
|
|||
|
||||
const deleteCurrentItem = useCallback(async () => {
|
||||
if (article) {
|
||||
removeItemFromCache(cache, mutate, article.id)
|
||||
await deleteLinkMutation(article.id).then((res) => {
|
||||
const pageId = article.id
|
||||
|
||||
removeItemFromCache(cache, mutate, pageId)
|
||||
await deleteLinkMutation(pageId).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToast('Page deleted', { position: 'bottom-right' })
|
||||
showSuccessToastWithUndo('Page deleted', async () => {
|
||||
const result = await updatePageMutation({
|
||||
pageId: pageId,
|
||||
state: State.SUCCEEDED,
|
||||
})
|
||||
document.dispatchEvent(new Event('revalidateLibrary'))
|
||||
if (result) {
|
||||
showSuccessToast('Page recovered')
|
||||
} else {
|
||||
showErrorToast('Error recovering page, check your deleted items')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// todo: revalidate or put back in cache?
|
||||
showErrorToast('Error deleting page', { position: 'bottom-right' })
|
||||
|
|
@ -253,8 +272,6 @@ export default function Home(): JSX.Element {
|
|||
perform: () => {
|
||||
if (
|
||||
readerSettings.showSetLabelsModal ||
|
||||
readerSettings.showDeleteConfirmation ||
|
||||
readerSettings.showDeleteConfirmation ||
|
||||
readerSettings.showEditDisplaySettingsModal
|
||||
) {
|
||||
return
|
||||
|
|
@ -550,13 +567,6 @@ export default function Home(): JSX.Element {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{readerSettings.showDeleteConfirmation && (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure you want to delete this page?'}
|
||||
onAccept={deleteCurrentItem}
|
||||
onOpenChange={() => readerSettings.setShowDeleteConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{article && showEditModal && (
|
||||
<EditArticleModal
|
||||
article={article}
|
||||
|
|
|
|||
Loading…
Reference in a new issue