Merge branch 'main' into feat/add-links-in-search

This commit is contained in:
Tom Rogers 2023-08-07 22:41:47 +02:00 committed by GitHub
commit b5a6b86915
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 2892 additions and 135 deletions

View file

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

View file

@ -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(),

View file

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

View 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(
'Whats 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

View file

@ -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': {

View file

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

View file

@ -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') {

View file

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

View file

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

View file

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

View file

@ -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' })
}

View file

@ -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',
})
}

View file

@ -67,7 +67,7 @@ const moduleExports = {
permanent: true,
},
{
source: '/settings/rss/',
source: '/settings/rss',
destination: '/settings/feeds',
permanent: true,
},

View file

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