diff --git a/android/Omnivore/build.gradle b/android/Omnivore/build.gradle index b30de2bfc..9965e4843 100644 --- a/android/Omnivore/build.gradle +++ b/android/Omnivore/build.gradle @@ -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 { diff --git a/packages/content-handler/src/index.ts b/packages/content-handler/src/index.ts index e6fa8c0cf..69ce3f362 100644 --- a/packages/content-handler/src/index.ts +++ b/packages/content-handler/src/index.ts @@ -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(), diff --git a/packages/content-handler/src/websites/ars-technica-handler.ts b/packages/content-handler/src/websites/ars-technica-handler.ts new file mode 100644 index 000000000..9091f4cf8 --- /dev/null +++ b/packages/content-handler/src/websites/ars-technica-handler.ts @@ -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 { + const response = await axios.get(url) + const data = response.data as string + return parseHTML(data).document + } + + async extractArticleContentsFromLink(url: string): Promise { + const dom = await this.grabContentFromUrl(url) + const articleContent = dom.querySelector('[itemprop="articleBody"]') + return [].slice.call(articleContent?.childNodes || []) + } + + async expandLinksAndCombine(document: Document): Promise { + 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 { + // 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, + } + } +} diff --git a/packages/content-handler/test/ars-technica.test.ts b/packages/content-handler/test/ars-technica.test.ts new file mode 100644 index 000000000..0d2575bc6 --- /dev/null +++ b/packages/content-handler/test/ars-technica.test.ts @@ -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) + }) +}) diff --git a/packages/content-handler/test/data/ars-multipage/ars-technica-page-1.html b/packages/content-handler/test/data/ars-multipage/ars-technica-page-1.html new file mode 100644 index 000000000..8e97b4aa5 --- /dev/null +++ b/packages/content-handler/test/data/ars-multipage/ars-technica-page-1.html @@ -0,0 +1,855 @@ + + + + + What’s going on with the reports of a room-temperature superconductor? | Ars Technica + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + +
+
+
+
+

+ No answers yet — +

+

What’s going on with the reports of a room-temperature superconductor?

+

Rumors are flying of confirmation, but the situation is still frustratingly vague.

+
+
+
+ +
+ Pellet of LK-99 being repelled by a magnet. +
Enlarge / Pellet of LK-99 being repelled by a magnet.
+ + + + + + + +

In late July, a couple of startling papers appeared on the arXiv, a repository of pre-peer-review manuscripts on topics in physics and astronomy. The papers claim to describe the synthesis of a material that is not only able to superconduct above room temperature, but also above the boiling point of water. And it does so at normal atmospheric pressures.

+

Instead of having to build upon years of work with exotic materials that only work under extreme conditions, the papers seem to describe a material that could be made via some relatively straightforward chemistry and would work if you set it on your desk. It was like finding a shortcut to a material that would revolutionize society.

+

The perfect time to write an article on those results would be when they've been confirmed by multiple labs. But these are not perfect times. Instead, rumors seem to be flying daily about possible confirmation, confusing and contradictory results, and informed discussions of why this material either should or shouldn't work.

+

In this article, we'll explain where things stand and why getting to a place of clarity will be challenging, even if these claims are right.

+

What’s the original claim?

+

The more detailed of the two manuscripts describes how to make the material and measurements of its property. The material itself is a variation of a well-known chemical called lead apatite. Apatites are a class of chemicals that form similar crystal structures; this particular version is primarily composed of lead and phosphate groups—all of its constituents are cheap and readily available.

+ +

The version developed here, which has been termed LK-99, was made by reacting a lead sulfate with a copper-phosphorus compound (the reaction requires high temperatures for over a day under a vacuum). This strips the phosphorus from the copper, oxidizes it, and allows it to displace the sulfur from its compound with the lead. Critically, though, some fraction of the lead itself ends up replaced by copper in the resulting compound.

+

This has a significant impact on the apatite crystal structure because copper is quite a bit smaller than lead. The researchers claim the overall volume of the sample drops by about half of a percentage as a result, and that change is accompanied by shifts in the orientation of various atoms and bonds. That means changes in where the electrons reside within the material.

+

That change appears to be critical to the LK-99's behavior. Superconductivity is associated with a number of very specific properties, and the researchers measure two of them: the expulsion of magnetic field lines (called the Meissner effect) and the existence of a critical temperature at which conductivity changes.

+

It's hard to explain just how strange these experiments are. Under normal circumstances, the superconducting material starts out behaving as a normal chemical and has to be cooled down to the critical point where exceptional behavior emerges. LK-99, by contrast, starts out superconducting and has to be heated beyond the boiling point of water to reach its critical temperature.

+

The only somewhat strange result here comes at temperatures just below the critical temperature. At room temperature and above, the resistance of LK-99 remains at zero as far as the testing equipment is able to measure. But it starts to rise ever so slightly once temperatures reach 60°C and displays a smooth upward slope until the sample hits 90°C, at which point it stays flat until the critical temperature is reached. The researchers did not attempt to explain this.

+ +
+ + + + +
+
+
+
+ + + +
+ + + +
+
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + +
+ + + + +
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+

Channel Ars Technica

+
+
+
+ +
+ +
+ + +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/content-handler/test/data/ars-multipage/ars-technica-page-2.html b/packages/content-handler/test/data/ars-multipage/ars-technica-page-2.html new file mode 100644 index 000000000..2f8cc0b95 --- /dev/null +++ b/packages/content-handler/test/data/ars-multipage/ars-technica-page-2.html @@ -0,0 +1,841 @@ + + + + + What’s going on with the reports of a room-temperature superconductor? | Ars Technica + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + +
+
+
+
+

+ No answers yet — +

+

What’s going on with the reports of a room-temperature superconductor?

+

Rumors are flying of confirmation, but the situation is still frustratingly vague.

+
+
+
+ + + + + + +

What about those other superconductors?

+

The news comes at a somewhat awkward time for the field. A similar claim was made about a high-pressure material a few years ago, but that paper ended up being retracted because of problems with some of its data. The same research group came back with a different material that was said to work at room temperature, but that work hasn't been replicated, and the head of the lab has now been accused of scientific misconduct.

+

There is almost no overlap between that work and LK-99. None of the people involved are the same, so there's no reason to suspect problematic research practices. And the chemistry and physics involved are completely different. The earlier work used high pressure to create chemicals with lots of hydrogen and unusual orbital structures. LK-99 uses no hydrogen at all and gets its orbital structures via a conformational change in a crystal lattice that takes place at ambient pressures.

+

Hydrogen was the focus of the earlier work because its low atomic weight influences the behavior of vibrations within the material in a way that promotes the formation of superconducting pairs of electrons. The mechanism behind LK-99 is less clear, but clearly not that.

+

(The LK-99's creators suggest that the conformational change in the crystal creates a sort of standing wave of electrons called a "charge density wave," and superconductivity involves electrons tunneling between wave sites. The modeling paper, by contrast, suggests that giving electrons the opportunity to both superconduct and participate in additional processes like charge density wave formation increases the probability that they'll superconduct. In any case, neither idea involves phonons.)

+ +

So when will we actually know anything?

+

Hopefully soon. The researchers behind the original report are trying to get information out there. In addition to the drafts placed in the arXiv, they have already published a paper on LK-99, albeit in their native Korean. And a group of South Korean scientists working in the field have also announced that they're going to obtain LK-99 samples and try to confirm its reported behavior.

+

There's also lots of activity outside of South Korea. Producing LK-99 is within reach of a lot of labs, and testing it is much easier since it doesn't require low temperatures or high pressures. That will mean a lot of short-term confusion, but it's likely to enable a consensus to emerge sooner.

+

Whether or not this chemical superconducts at ambient temperatures might not be the final question, though. Assuming it does, there will be many questions about how to develop it into a useful material, how much current it can carry, and how to use it most effectively in the huge range of applications it can be put to. But I'm sure we'll all be happy if we end up needing answers to those questions.

+ +
+ + + + +
+
+
+
+ + + +
+ + + +
+
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + +
+ + + + +
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+

Channel Ars Technica

+
+
+
+ +
+ +
+ + +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/content-handler/test/data/ars-multipage/ars-technica-page-3.html b/packages/content-handler/test/data/ars-multipage/ars-technica-page-3.html new file mode 100644 index 000000000..40b431ef4 --- /dev/null +++ b/packages/content-handler/test/data/ars-multipage/ars-technica-page-3.html @@ -0,0 +1,841 @@ + + + + + What’s going on with the reports of a room-temperature superconductor? | Ars Technica + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + +
+
+
+
+

+ No answers yet — +

+

What’s going on with the reports of a room-temperature superconductor?

+

Rumors are flying of confirmation, but the situation is still frustratingly vague.

+
+
+
+ + + + + + +

What about those other superconductors?

+

The news comes at a somewhat awkward time for the field. A similar claim was made about a high-pressure material a few years ago, but that paper ended up being retracted because of problems with some of its data. The same research group came back with a different material that was said to work at room temperature, but that work hasn't been replicated, and the head of the lab has now been accused of scientific misconduct.

+

There is almost no overlap between that work and LK-99. None of the people involved are the same, so there's no reason to suspect problematic research practices. And the chemistry and physics involved are completely different. The earlier work used high pressure to create chemicals with lots of hydrogen and unusual orbital structures. LK-99 uses no hydrogen at all and gets its orbital structures via a conformational change in a crystal lattice that takes place at ambient pressures.

+

Hydrogen was the focus of the earlier work because its low atomic weight influences the behavior of vibrations within the material in a way that promotes the formation of superconducting pairs of electrons. The mechanism behind LK-99 is less clear, but clearly not that.

+

(The LK-99's creators suggest that the conformational change in the crystal creates a sort of standing wave of electrons called a "charge density wave," and superconductivity involves electrons tunneling between wave sites. The modeling paper, by contrast, suggests that giving electrons the opportunity to both superconduct and participate in additional processes like charge density wave formation increases the probability that they'll superconduct. In any case, neither idea involves phonons.)

+ +

So when will we actually know anything?

+

Hopefully soon. The researchers behind the original report are trying to get information out there. In addition to the drafts placed in the arXiv, they have already published a paper on LK-99, albeit in their native Korean. And a group of South Korean scientists working in the field have also announced that they're going to obtain LK-99 samples and try to confirm its reported behavior.

+

There's also lots of activity outside of South Korea. Producing LK-99 is within reach of a lot of labs, and testing it is much easier since it doesn't require low temperatures or high pressures. That will mean a lot of short-term confusion, but it's likely to enable a consensus to emerge sooner.

+

Whether or not this chemical superconducts at ambient temperatures might not be the final question, though. Assuming it does, there will be many questions about how to develop it into a useful material, how much current it can carry, and how to use it most effectively in the huge range of applications it can be put to. But I'm sure we'll all be happy if we end up needing answers to those questions.

+ +
+ + + + +
+
+
+
+ + + +
+ + + +
+
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + +
+ + + + +
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+

Channel Ars Technica

+
+
+
+ +
+ +
+ + +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index f220190ff..3aadaf088 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -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': { diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx index f095918f3..84372b1aa 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -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', diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 4bec6daa0..ae89e8f0f 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -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() const [linkToEdit, setLinkToEdit] = useState() const [linkToUnsubscribe, setLinkToUnsubscribe] = useState() @@ -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([]) const [multiSelectMode, setMultiSelectMode] = useState('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 && ( - - - Are you sure you want to delete this item? All associated notes - and highlights will be deleted. - - {props.linkToRemove?.node && props.viewer && ( - - {}} - // eslint-disable-next-line @typescript-eslint/no-empty-function - handleAction={() => {}} - /> - - )} - - } - onAccept={removeItem} - acceptButtonLabel="Delete Item" - onOpenChange={() => setShowRemoveLinkConfirmation(false)} - /> - )} {showUnsubscribeConfirmation && ( 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') { diff --git a/packages/web/components/templates/reader/ReaderHeader.tsx b/packages/web/components/templates/reader/ReaderHeader.tsx index d77445989..122922ef6 100644 --- a/packages/web/components/templates/reader/ReaderHeader.tsx +++ b/packages/web/components/templates/reader/ReaderHeader.tsx @@ -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', diff --git a/packages/web/lib/hooks/useReaderSettings.tsx b/packages/web/lib/hooks/useReaderSettings.tsx index d93b419a9..186a9043b 100644 --- a/packages/web/lib/hooks/useReaderSettings.tsx +++ b/packages/web/lib/hooks/useReaderSettings.tsx @@ -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, diff --git a/packages/web/lib/networking/mutations/updatePageMutation.ts b/packages/web/lib/networking/mutations/updatePageMutation.ts index ee15559c2..b7d9b3b67 100644 --- a/packages/web/lib/networking/mutations/updatePageMutation.ts +++ b/packages/web/lib/networking/mutations/updatePageMutation.ts @@ -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( diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index feda1a48e..cddf3d6f4 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -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' }) } diff --git a/packages/web/lib/toastHelpers.tsx b/packages/web/lib/toastHelpers.tsx index 98503bccf..867880a41 100644 --- a/packages/web/lib/toastHelpers.tsx +++ b/packages/web/lib/toastHelpers.tsx @@ -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, + options?: ToastOptions +) => { + return toast( + ({ id }) => ( + + + {message} + + + + + ), + { + 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 +) => { + return showToastWithUndo(message, '#55B938', undoAction, { + position: 'bottom-right', + }) } diff --git a/packages/web/next.config.js b/packages/web/next.config.js index e4970f705..3a874e7c2 100644 --- a/packages/web/next.config.js +++ b/packages/web/next.config.js @@ -67,7 +67,7 @@ const moduleExports = { permanent: true, }, { - source: '/settings/rss/', + source: '/settings/rss', destination: '/settings/feeds', permanent: true, }, diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 6f7a3ae94..a5a729bc6 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -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( () => 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 && ( - readerSettings.setShowDeleteConfirmation(false)} - /> - )} {article && showEditModal && (