More work on react-query switchover

This commit is contained in:
Jackson Harper 2024-07-28 20:41:16 +08:00
parent 06af855621
commit 457d1d9de9
51 changed files with 1264 additions and 3013 deletions

View file

@ -9,6 +9,7 @@ import Views
public class ShareExtensionViewModel: ObservableObject {
@Published public var status: ShareExtensionStatus = .processing
@Published public var title: String = ""
@Published public var urlToLoad: URL?
@Published public var url: String?
@Published public var iconURL: URL?
@Published public var highlightData: HighlightData?
@ -123,6 +124,7 @@ public class ShareExtensionViewModel: ObservableObject {
case .none:
self.url = hostname
self.title = payload.url
self.urlToLoad = URL(string: payload.url)
case let .pdf(localUrl: localUrl):
self.url = hostname
self.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString)

View file

@ -302,6 +302,10 @@ public struct ShareExtensionView: View {
VStack(alignment: .leading, spacing: 15) {
titleBar
.padding(.top, 15)
if let urlToLoad = viewModel.urlToLoad {
InternalWebAppView(request: URLRequest(url: urlToLoad))
}
TabView(selection: $visibleTab) {
infoBox

View file

@ -145,6 +145,9 @@ struct ProfileView: View {
NavigationLink(destination: TextToSpeechView()) {
Text(LocalText.textToSpeechGeneric)
}
NavigationLink(destination: SetupLoginsView()) {
Text("Login to sites")
}
}
#endif

View file

@ -47,6 +47,18 @@ struct WebReader: PlatformViewRepresentable {
return storedSize <= 1 ? 100 : storedSize
}
func transferCookiesToWKWebView(webView: WKWebView) {
let cookieStore = HTTPCookieStorage.shared
let webViewCookieStore = webView.configuration.websiteDataStore.httpCookieStore
if let cookies = cookieStore.cookies {
for cookie in cookies {
print("TRANSFERING COOKIE: ", cookie.name, cookie.value)
webViewCookieStore.setCookie(cookie)
}
}
}
private func makePlatformView(context: Context) -> WKWebView {
let webView = WebViewManager.shared()
let contentController = WKUserContentController()
@ -57,6 +69,8 @@ struct WebReader: PlatformViewRepresentable {
webView.configuration.userContentController = contentController
webView.configuration.userContentController.removeAllScriptMessageHandlers()
transferCookiesToWKWebView(webView: webView)
#if os(iOS)
webView.isOpaque = false
webView.tintColor = UIColor(ThemeManager.currentHighlightColor)

View file

@ -20,6 +20,8 @@ enum WebViewConfigurationManager {
public enum WebViewManager {
public static let sharedView = create()
public static let cookieView = create()
public static func shared() -> OmnivoreWebView {
sharedView
}

View file

@ -0,0 +1,35 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import React from 'react'
export class UntrashIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
strokeWidth="1.5"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 3l18 18" />
<path d="M4 7h3m4 0h9" />
<path d="M10 11l0 6" />
<path d="M14 14l0 3" />
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l.077 -.923" />
<path d="M18.384 14.373l.616 -7.373" />
<path d="M9 5v-1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</svg>
)
}
}

View file

@ -37,7 +37,7 @@ import { Toaster } from 'react-hot-toast'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import useLibraryItemActions from '../../lib/hooks/useLibraryItemActions'
import { SyncLoader } from 'react-spinners'
import { useGetRawSearchItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { useGetLibraryItems } from '../../lib/networking/library_items/useLibraryItems'
import { useRegisterActions } from 'kbar'
type HomeState = {
@ -182,8 +182,9 @@ type NavigationContextType = {
dispatch: React.Dispatch<Action>
}
const NavigationContext =
createContext<NavigationContextType | undefined>(undefined)
const NavigationContext = createContext<NavigationContextType | undefined>(
undefined
)
export const useNavigation = (): NavigationContextType => {
const context = useContext(NavigationContext)
@ -210,13 +211,15 @@ export function HomeContainer(): JSX.Element {
const shouldFallback =
homeData.error || (!homeData.isValidating && !hasTopPicks(homeData))
const searchData = useGetRawSearchItemsQuery(
const searchData = useGetLibraryItems(
undefined,
{
limit: 10,
searchQuery: 'in:inbox',
includeContent: false,
sortDescending: true,
},
// only enable this search if we didn't get home data
shouldFallback
)
@ -227,27 +230,28 @@ export function HomeContainer(): JSX.Element {
}, [viewerData])
const searchItems = useMemo(() => {
return searchData.items.map((item) => {
return {
id: item.id,
date: item.savedAt,
title: item.title,
url: item.url,
slug: item.slug,
score: 1.0,
thumbnail: item.image,
previewContent: item.description,
source: {
name: item.folder == 'following' ? item.subscription : item.siteName,
icon: item.siteIcon,
type: 'LIBRARY',
},
canArchive: true,
canDelete: true,
canShare: true,
canMove: item.folder == 'following',
} as HomeItem
})
return []
// return searchData.items.map((item) => {
// return {
// id: item.id,
// date: item.savedAt,
// title: item.title,
// url: item.url,
// slug: item.slug,
// score: 1.0,
// thumbnail: item.image,
// previewContent: item.description,
// source: {
// name: item.folder == 'following' ? item.subscription : item.siteName,
// icon: item.siteIcon,
// type: 'LIBRARY',
// },
// canArchive: true,
// canDelete: true,
// canShare: true,
// canMove: item.folder == 'following',
// } as HomeItem
// })
}, [searchData])
useEffect(() => {
@ -383,7 +387,7 @@ export function HomeContainer(): JSX.Element {
)
const dataReady =
!homeData.isValidating && (!shouldFallback || !searchData.isValidating)
!homeData.isValidating && (!shouldFallback || !searchData.isLoading)
if (!dataReady || (homeData.error && homeData.errorMessage == 'PENDING')) {
console.log('showing pending')
return (

View file

@ -1,11 +1,13 @@
import type { ReactNode } from 'react'
import { Dropdown, DropdownOption } from '../elements/DropdownElements'
import { LibraryItemNode } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import {
LibraryItemNode,
useUpdateItemReadStatus,
} from '../../lib/networking/library_items/useLibraryItems'
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
import { State } from '../../lib/networking/fragments/articleFragment'
export type CardMenuDropdownAction =
| 'mark-read'
| 'mark-unread'
| 'archive'
| 'unarchive'
| 'delete'
@ -24,13 +26,15 @@ type CardMenuProps = {
}
export function CardMenu(props: CardMenuProps): JSX.Element {
const updateItemReadStatus = useUpdateItemReadStatus()
return (
<Dropdown
triggerElement={props.triggerElement}
onOpenChange={props.onOpenChange}
css={{ bg: '$thNavMenuFooter' }}
>
{!props.item.isArchived ? (
{props.item.state != State.ARCHIVED ? (
<DropdownOption
onSelect={() => props.actionHandler('archive')}
title="Archive"
@ -63,15 +67,23 @@ export function CardMenu(props: CardMenuProps): JSX.Element {
/>
{props.item.readingProgressPercent < 98 ? (
<DropdownOption
onSelect={() => {
props.actionHandler('mark-read')
onSelect={async () => {
await updateItemReadStatus.mutateAsync({
id: props.item.id,
readingProgressPercent: 100,
force: true,
})
}}
title="Mark read"
/>
) : (
<DropdownOption
onSelect={() => {
props.actionHandler('mark-unread')
onSelect={async () => {
await updateItemReadStatus.mutateAsync({
id: props.item.id,
readingProgressPercent: 0,
force: true,
})
}}
title="Mark unread"
/>

View file

@ -14,7 +14,7 @@ import { HighlightViewNote } from './HighlightNotes'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { highlightColorVar } from '../../lib/themeUpdater'
import { ReadableItem } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../lib/networking/library_items/useLibraryItems'
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
import {
autoUpdate,

View file

@ -1,6 +1,6 @@
import { LayoutType } from '../../templates/homeFeed/HomeFeedContainer'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import type { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import type { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
import { MultiSelectMode } from '../../templates/homeFeed/LibraryHeader'
export type LinkedItemCardAction =
@ -18,6 +18,7 @@ export type LinkedItemCardAction =
| 'update-item'
| 'move-to-inbox'
| 'refresh'
| 'restore'
export type LinkedItemCardProps = {
item: LibraryItemNode

View file

@ -1,7 +1,7 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useMemo } from 'react'
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
import { HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { RecommendedFlairIcon } from '../../elements/icons/RecommendedFlairIcon'
import { PinnedFlairIcon } from '../../elements/icons/PinnedFlairIcon'
@ -140,9 +140,7 @@ type FlairIconProps = {
children: React.ReactNode
}
export function FlairIcon(
props: FlairIconProps
): JSX.Element {
export function FlairIcon(props: FlairIconProps): JSX.Element {
return (
<SpanBox title={props.title} css={{ lineHeight: '1' }}>
{props.children}

View file

@ -4,7 +4,7 @@ import { CaretDown, CaretUp } from '@phosphor-icons/react'
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
import { styled } from '@stitches/react'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
import { Button } from '../../elements/Button'
import { theme } from '../../tokens/stitches.config'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'

View file

@ -1,6 +1,11 @@
import { useState } from 'react'
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import {
LibraryItemNode,
useArchiveItem,
useDeleteItem,
useRestoreItem,
} from '../../../lib/networking/library_items/useLibraryItems'
import { LinkedItemCardAction } from './CardTypes'
import { Button } from '../../elements/Button'
import { theme } from '../../tokens/stitches.config'
@ -14,6 +19,8 @@ import { LabelIcon } from '../../elements/icons/LabelIcon'
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
import { BrowserIcon } from '../../elements/icons/BrowserIcon'
import { MoveToInboxIcon } from '../../elements/icons/MoveToInboxIcon'
import { UntrashIcon } from '../../elements/icons/UntrashIcon'
import { State } from '../../../lib/networking/fragments/articleFragment'
type LibraryHoverActionsProps = {
viewer: UserBasicData
@ -26,6 +33,9 @@ type LibraryHoverActionsProps = {
export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
const [menuOpen, setMenuOpen] = useState(false)
const archiveItem = useArchiveItem()
const deleteItem = useDeleteItem()
const restoreItem = useRestoreItem()
return (
<Box
@ -89,16 +99,22 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
</Button>
) : (
<Button
title={props.item.isArchived ? 'Unarchive (e)' : 'Archive (e)'}
title={
props.item.state === State.ARCHIVED
? 'Unarchive (e)'
: 'Archive (e)'
}
style="hoverActionIcon"
onClick={(event) => {
const action = props.item.isArchived ? 'unarchive' : 'archive'
props.handleAction(action)
onClick={async (event) => {
await archiveItem.mutateAsync({
linkId: props.item.id,
archived: props.item.state !== State.ARCHIVED,
})
event.preventDefault()
event.stopPropagation()
}}
>
{props.item.isArchived ? (
{props.item.state === State.ARCHIVED ? (
<UnarchiveIcon
size={21}
color={theme.colors.thNotebookSubtle.toString()}
@ -112,15 +128,29 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
</Button>
)}
<Button
title="Remove (#)"
title={props.item.state == State.DELETED ? 'Restore' : 'Remove (#)'}
style="hoverActionIcon"
onClick={(event) => {
props.handleAction('delete')
onClick={async (event) => {
if (props.item.state == State.DELETED) {
await restoreItem.mutateAsync(props.item.id)
} else {
await deleteItem.mutateAsync(props.item.id)
}
event.preventDefault()
event.stopPropagation()
}}
>
<TrashIcon size={21} color={theme.colors.thNotebookSubtle.toString()} />
{props.item.state == State.DELETED ? (
<UntrashIcon
size={21}
color={theme.colors.thNotebookSubtle.toString()}
/>
) : (
<TrashIcon
size={21}
color={theme.colors.thNotebookSubtle.toString()}
/>
)}
</Button>
<Button
title="Edit labels (l)"

View file

@ -4,9 +4,12 @@ import {
DropdownOption,
DropdownSeparator,
} from '../elements/DropdownElements'
import { ArticleAttributes } from '../../lib/networking/queries/useGetArticleQuery'
import { State } from '../../lib/networking/fragments/articleFragment'
type DropdownMenuProps = {
triggerElement: ReactNode
libraryItem?: ArticleAttributes
articleActionHandler: (action: string, arg?: unknown) => void
}
@ -14,8 +17,18 @@ export function ReaderDropdownMenu(props: DropdownMenuProps): JSX.Element {
return (
<Dropdown triggerElement={props.triggerElement}>
<DropdownOption
onSelect={() => props.articleActionHandler('archive')}
title="Archive (e)"
onSelect={async () => {
if (props.libraryItem?.state === State.ARCHIVED) {
props.articleActionHandler('unarchive')
} else {
props.articleActionHandler('archive')
}
}}
title={
props.libraryItem?.state === State.ARCHIVED
? 'Unarchive (e)'
: 'Archive (e)'
}
/>
<DropdownOption
onSelect={() => props.articleActionHandler('setLabels')}
@ -26,7 +39,9 @@ export function ReaderDropdownMenu(props: DropdownMenuProps): JSX.Element {
title="Edit info (i)"
/>
<DropdownOption
onSelect={() => props.articleActionHandler('delete')}
onSelect={async () => {
props.articleActionHandler('delete')
}}
title="Remove (#)"
/>
<DropdownSeparator />

View file

@ -11,6 +11,7 @@ import { TrashIcon } from '../../elements/icons/TrashIcon'
import { LabelIcon } from '../../elements/icons/LabelIcon'
import { EditInfoIcon } from '../../elements/icons/EditInfoIcon'
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
import { State } from '../../../lib/networking/fragments/articleFragment'
export type ArticleActionsMenuLayout = 'top' | 'side'
@ -163,7 +164,7 @@ export function ArticleActionsMenu(
<TrashIcon size={24} color={theme.colors.thHighContrast.toString()} />
</Button>
{!props.article?.isArchived ? (
{props.article?.state !== State.ARCHIVED ? (
<Button
title="Archive (e)"
style="articleActionIcon"

View file

@ -19,7 +19,7 @@ import { updateTheme, updateThemeLocally } from '../../../lib/themeUpdater'
import { ArticleMutations } from '../../../lib/articleActions'
import { LabelChip } from '../../elements/LabelChip'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { Recommendation } from '../../../lib/networking/library_items/useLibraryItems'
import { Avatar } from '../../elements/Avatar'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { AISummary } from './AISummary'
@ -124,16 +124,21 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
props.highlightOnRelease
)
// iOS app embed can overide the original margin and line height
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] =
useState<number | null>(null)
const [lineHeightOverride, setLineHeightOverride] =
useState<number | null>(null)
const [fontFamilyOverride, setFontFamilyOverride] =
useState<string | null>(null)
const [highContrastTextOverride, setHighContrastTextOverride] =
useState<boolean | undefined>(undefined)
const [justifyTextOverride, setJustifyTextOverride] =
useState<boolean | undefined>(undefined)
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = useState<
number | null
>(null)
const [lineHeightOverride, setLineHeightOverride] = useState<number | null>(
null
)
const [fontFamilyOverride, setFontFamilyOverride] = useState<string | null>(
null
)
const [highContrastTextOverride, setHighContrastTextOverride] = useState<
boolean | undefined
>(undefined)
const [justifyTextOverride, setJustifyTextOverride] = useState<
boolean | undefined
>(undefined)
const highlightHref = useRef(
window.location.hash ? window.location.hash.split('#')[1] : null
)

View file

@ -1,6 +1,6 @@
import { useState } from 'react'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { HighlightView } from '../../patterns/HighlightView'

View file

@ -24,7 +24,7 @@ import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { ArticleMutations } from '../../../lib/articleActions'
import { isTouchScreenDevice } from '../../../lib/deviceType'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
import 'react-sliding-pane/dist/react-sliding-pane.css'
import { NotebookContent } from './Notebook'

View file

@ -13,7 +13,7 @@ import { HighlightViewItem } from './HighlightViewItem'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { TrashIcon } from '../../elements/icons/TrashIcon'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
import { ArticleNotes } from '../../patterns/ArticleNotes'
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'

View file

@ -6,7 +6,7 @@ import { Button } from '../../elements/Button'
import { ExportIcon } from '../../elements/icons/ExportIcon'
import { useCallback } from 'react'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'

View file

@ -17,7 +17,7 @@ import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
import 'react-markdown-editor-lite/lib/index.css'
import { NotebookContent } from './Notebook'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
type NotebookModalProps = {
viewer: UserBasicData

View file

@ -1,4 +1,4 @@
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import 'react-sliding-pane/dist/react-sliding-pane.css'
import { NotebookContent } from './Notebook'

View file

@ -11,6 +11,7 @@ import { EditInfoIcon } from '../../elements/icons/EditInfoIcon'
import { ReaderSettingsIcon } from '../../elements/icons/ReaderSettingsIcon'
import { CircleUtilityMenuIcon } from '../../elements/icons/CircleUtilityMenuIcon'
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
import { State } from '../../../lib/networking/fragments/articleFragment'
export type ArticleActionsMenuLayout = 'top' | 'side'
@ -94,15 +95,12 @@ export function VerticalArticleActionsMenu(
css={{
display: 'flex',
alignItems: 'center',
'@mdDown': {
display: 'none',
},
}}
>
<TrashIcon size={24} color={theme.colors.thHighContrast.toString()} />
</Button>
{!props.article?.isArchived ? (
{props.article?.state !== State.ARCHIVED ? (
<Button
title="Archive (e)"
style="articleActionIcon"
@ -155,6 +153,7 @@ export function VerticalArticleActionsMenu(
</Button>
<ReaderDropdownMenu
libraryItem={props.article}
triggerElement={
<CircleUtilityMenuIcon
size={24}

View file

@ -2,7 +2,7 @@ import dayjs, { Dayjs } from 'dayjs'
import { useCallback, useState } from 'react'
import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation'
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { LibraryItem } from '../../../lib/networking/library_items/useLibraryItems'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { CloseButton } from '../../elements/CloseButton'
import { FormInput } from '../../elements/FormElements'

View file

@ -2,7 +2,7 @@ import Link from 'next/link'
import { DotsThreeVertical } from '@phosphor-icons/react'
import { useCallback } from 'react'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import {

View file

@ -6,7 +6,7 @@ import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import {
LibraryItem,
LibraryItemNode,
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
} from '../../../lib/networking/library_items/useLibraryItems'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'

View file

@ -1,6 +1,6 @@
import { LayoutType } from './HomeFeedContainer'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { LibraryItem } from '../../../lib/networking/library_items/useLibraryItems'
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Toaster } from 'react-hot-toast'
import TopBarProgress from 'react-topbar-progress-indicator'

View file

@ -8,21 +8,18 @@ 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 {
import {
LibraryItem,
LibraryItemNode,
LibraryItems,
LibraryItemsQueryInput,
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
useGetLibraryItems,
} from '../../../lib/networking/library_items/useLibraryItems'
import {
useGetViewerQuery,
UserBasicData,
@ -56,6 +53,7 @@ import { LibraryHeader } from './LibraryHeader'
import { TrashIcon } from '../../elements/icons/TrashIcon'
import { theme } from '../../tokens/stitches.config'
import { emptyTrashMutation } from '../../../lib/networking/mutations/emptyTrashMutation'
import { State } from '../../../lib/networking/fragments/articleFragment'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
@ -99,11 +97,13 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
const gridContainerRef = useRef<HTMLDivElement>(null)
const [labelsTarget, setLabelsTarget] =
useState<LibraryItem | undefined>(undefined)
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
undefined
)
const [notebookTarget, setNotebookTarget] =
useState<LibraryItem | undefined>(undefined)
const [notebookTarget, setNotebookTarget] = useState<LibraryItem | undefined>(
undefined
)
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
@ -114,27 +114,25 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
useState<LibraryItemsQueryInput>(defaultQuery)
const {
itemsPages,
size,
setSize,
isValidating,
performActionOnItem,
mutate,
data: itemsPages,
isLoading,
fetchNextPage,
hasNextPage,
error: fetchItemsError,
} = useGetLibraryItemsQuery(props.folder, queryInputs)
} = useGetLibraryItems(props.folder, queryInputs)
useEffect(() => {
const handleRevalidate = () => {
;(async () => {
console.log('revalidating library')
await mutate()
})()
}
document.addEventListener('revalidateLibrary', handleRevalidate)
return () => {
document.removeEventListener('revalidateLibrary', handleRevalidate)
}
}, [mutate])
// useEffect(() => {
// const handleRevalidate = () => {
// ;(async () => {
// console.log('revalidating library')
// await mutate()
// })()
// }
// document.addEventListener('revalidateLibrary', handleRevalidate)
// return () => {
// document.removeEventListener('revalidateLibrary', handleRevalidate)
// }
// }, [mutate])
useEffect(() => {
if (queryValue.startsWith('#')) {
@ -157,7 +155,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
if (qs !== (queryInputs.searchQuery || '')) {
setQueryInputs({ ...queryInputs, searchQuery: qs })
performActionOnItem('refresh', undefined as unknown as any)
// performActionOnItem('refresh', undefined as unknown as any)
}
// intentionally not watching queryInputs and performActionOnItem
@ -169,25 +167,25 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
window.localStorage.setItem('nav-return', router.asPath)
}, [router.asPath])
const hasMore = useMemo(() => {
if (!itemsPages) {
return false
}
return itemsPages[itemsPages.length - 1].search.pageInfo.hasNextPage
}, [itemsPages])
// const hasMore = useMemo(() => {
// if (!itemsPages) {
// return false
// }
// return itemsPages[itemsPages.length - 1].search.pageInfo.hasNextPage
// }, [itemsPages])
const libraryItems = useMemo(() => {
const items =
itemsPages
?.flatMap((ad) => {
return ad.search.edges.map((it) => ({
itemsPages?.pages
.flatMap((ad: LibraryItems) => {
return ad.edges.map((it) => ({
...it,
isLoading: it.node.state === 'PROCESSING',
}))
})
.filter((item) => props.filterFunc(item.node)) || []
return items
}, [itemsPages, performActionOnItem])
}, [itemsPages])
useEffect(() => {
if (localStorage) {
@ -198,78 +196,78 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
}
}, [libraryItems])
useEffect(() => {
const timeout: NodeJS.Timeout[] = []
// useEffect(() => {
// const timeout: NodeJS.Timeout[] = []
const items = (
itemsPages?.flatMap((ad) => {
return ad.search.edges.map((it) => ({
...it,
isLoading: it.node.state === 'PROCESSING',
}))
}) || []
).filter((it) => it.isLoading)
// const items = (
// itemsPages?.flatMap((ad) => {
// return ad.search.edges.map((it) => ({
// ...it,
// isLoading: it.node.state === 'PROCESSING',
// }))
// }) || []
// ).filter((it) => it.isLoading)
items.map(async (item) => {
let startIdx = 0
// items.map(async (item) => {
// let startIdx = 0
const seeIfUpdated = async () => {
if (startIdx >= TIMEOUT_DELAYS.length) {
item.node.state = State.FAILED
const updatedArticle = { ...item }
updatedArticle.node = { ...item.node }
updatedArticle.isLoading = false
performActionOnItem('update-item', updatedArticle)
return
}
// const seeIfUpdated = async () => {
// if (startIdx >= TIMEOUT_DELAYS.length) {
// item.node.state = State.FAILED
// const updatedArticle = { ...item }
// updatedArticle.node = { ...item.node }
// updatedArticle.isLoading = false
// // performActionOnItem('update-item', updatedArticle)
// return
// }
const username = viewerData?.me?.profile.username
const itemsToUpdate = libraryItems.filter((it) => it.isLoading)
// const username = viewerData?.me?.profile.username
// const itemsToUpdate = libraryItems.filter((it) => it.isLoading)
if (itemsToUpdate.length > 0) {
const link = await articleQuery({
username,
slug: item.node.id,
includeFriendsHighlights: false,
})
// if (itemsToUpdate.length > 0) {
// const link = await articleQuery({
// username,
// slug: item.node.id,
// includeFriendsHighlights: false,
// })
if (link && link.state != 'PROCESSING') {
const updatedArticle = { ...item }
updatedArticle.node = { ...item.node, ...link }
updatedArticle.isLoading = false
console.log(`Updating Metadata of ${item.node.slug}.`)
performActionOnItem('update-item', updatedArticle)
return
}
// if (link && link.state != 'PROCESSING') {
// const updatedArticle = { ...item }
// updatedArticle.node = { ...item.node, ...link }
// updatedArticle.isLoading = false
// console.log(`Updating Metadata of ${item.node.slug}.`)
// // performActionOnItem('update-item', updatedArticle)
// return
// }
console.log(
`Trying to get the metadata of item ${item.node.slug}... Retry ${startIdx} of 5`
)
timeout.push(setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++]))
}
}
// console.log(
// `Trying to get the metadata of item ${item.node.slug}... Retry ${startIdx} of 5`
// )
// timeout.push(setTimeout(seeIfUpdated, TIMEOUT_DELAYS[startIdx++]))
// }
// }
await seeIfUpdated()
})
// await seeIfUpdated()
// })
return () => {
timeout.forEach(clearTimeout)
}
}, [itemsPages])
// return () => {
// timeout.forEach(clearTimeout)
// }
// }, [itemsPages])
const handleFetchMore = useCallback(() => {
if (isValidating || !hasMore) {
return
}
setSize(size + 1)
}, [size, isValidating])
// const handleFetchMore = useCallback(() => {
// if (isLoading || !hasNextPage) {
// return
// }
// setSize(size + 1)
// }, [size, isValidating])
useEffect(() => {
if (isValidating || !hasMore || size !== 1) {
return
}
setSize(size + 1)
}, [size, isValidating])
// useEffect(() => {
// if (isValidating || !hasNextPage || size !== 1) {
// return
// }
// setSize(size + 1)
// }, [size, isValidating])
const focusFirstItem = useCallback(() => {
if (libraryItems.length < 1) {
@ -375,9 +373,9 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
scrollToActiveCard(activeCardId)
alreadyScrolled.current = true
if (activeItem) {
performActionOnItem('refresh', activeItem)
}
// if (activeItem) {
// performActionOnItem('refresh', activeItem)
// }
}
}, [activeCardId, scrollToActiveCard])
@ -389,59 +387,64 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
return
}
switch (action) {
case 'showDetail':
const username = viewerData?.me?.profile.username
if (username) {
setActiveCardId(item.node.id)
if (item.node.state === State.PROCESSING) {
router.push(`/article?url=${encodeURIComponent(item.node.url)}`)
} else {
const dl =
item.node.pageType === PageType.HIGHLIGHTS
? `#${item.node.id}`
: ''
router.push(`/${username}/${item.node.slug}` + dl)
}
}
break
case 'showOriginal':
const url = item.node.originalArticleUrl
if (url) {
window.open(url, '_blank')
}
break
case 'archive':
performActionOnItem('archive', item)
break
case 'unarchive':
performActionOnItem('unarchive', item)
break
case 'delete':
performActionOnItem('delete', item)
break
case 'mark-read':
performActionOnItem('mark-read', item)
break
case 'mark-unread':
performActionOnItem('mark-unread', item)
break
case 'set-labels':
setLabelsTarget(item)
break
case 'open-notebook':
if (!notebookTarget) {
setNotebookTarget(item)
} else {
setNotebookTarget(undefined)
}
break
case 'unsubscribe':
performActionOnItem('unsubscribe', item)
case 'update-item':
performActionOnItem('update-item', item)
break
}
// switch (action) {
// case 'showDetail':
// const username = viewerData?.me?.profile.username
// if (username) {
// setActiveCardId(item.node.id)
// if (item.node.state === State.PROCESSING) {
// router.push(`/article?url=${encodeURIComponent(item.node.url)}`)
// } else {
// const dl =
// item.node.pageType === PageType.HIGHLIGHTS
// ? `#${item.node.id}`
// : ''
// router.push(`/${username}/${item.node.slug}` + dl)
// }
// }
// break
// case 'showOriginal':
// const url = item.node.originalArticleUrl
// if (url) {
// window.open(url, '_blank')
// }
// break
// case 'archive':
// performActionOnItem('archive', item)
// break
// case 'unarchive':
// performActionOnItem('unarchive', item)
// break
// case 'delete':
// performActionOnItem('delete', item)
// break
// case 'restore':
// performActionOnItem('restore', item)
// break
// case 'mark-read':
// performActionOnItem('mark-read', item)
// break
// case 'mark-unread':
// performActionOnItem('mark-unread', item)
// break
// case 'set-labels':
// setLabelsTarget(item)
// break
// case 'open-notebook':
// if (!notebookTarget) {
// setNotebookTarget(item)
// } else {
// setNotebookTarget(undefined)
// }
// break
// case 'unsubscribe':
// performActionOnItem('unsubscribe', item)
// case 'update-item':
// performActionOnItem('update-item', item)
// break
// default:
// console.warn('unknown action: ', action)
// }
}
const modalTargetItem = useMemo(() => {
@ -590,19 +593,20 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
})
)
const ARCHIVE_ACTION = !activeItem?.node.isArchived
? createAction({
section: 'Library',
name: 'Archive selected item',
shortcut: ['e'],
perform: () => handleCardAction('archive', activeItem),
})
: createAction({
section: 'Library',
name: 'UnArchive selected item',
shortcut: ['e'],
perform: () => handleCardAction('unarchive', activeItem),
})
const ARCHIVE_ACTION =
activeItem?.node.state !== State.ARCHIVED
? createAction({
section: 'Library',
name: 'Archive selected item',
shortcut: ['e'],
perform: () => handleCardAction('archive', activeItem),
})
: createAction({
section: 'Library',
name: 'UnArchive selected item',
shortcut: ['e'],
perform: () => handleCardAction('unarchive', activeItem),
})
const ACTIVE_ACTIONS = [
ARCHIVE_ACTION,
@ -676,7 +680,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
activeCardId ? [...ACTIVE_ACTIONS, ...UNACTIVE_ACTIONS] : UNACTIVE_ACTIONS,
[activeCardId, activeItem]
)
useFetchMore(handleFetchMore)
useFetchMore(fetchNextPage)
const setIsChecked = useCallback(
(itemId: string, set: boolean) => {
@ -710,8 +714,8 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
case 'search':
case 'visible':
const allIds = (
itemsPages?.flatMap((ad) => {
return ad.search.edges
itemsPages?.pages.flatMap((ad) => {
return ad.edges
}) || []
).map((item) => item.node.id)
setCheckedItems(allIds)
@ -739,10 +743,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
multiSelectMode === 'search'
? queryInputs.searchQuery || 'in:inbox'
: `includes:${checkedItems.join(',')}`
const expectedCount =
multiSelectMode === 'search'
? itemsPages?.[0].search.pageInfo.totalCount || 0
: checkedItems.length
const expectedCount = checkedItems.length
try {
const res = await bulkActionMutation(
@ -753,7 +754,6 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
)
if (res) {
let successMessage: string | undefined = undefined
console.log(action)
switch (action) {
case BulkAction.ARCHIVE:
successMessage = 'Link Archived'
@ -781,7 +781,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
position: 'bottom-right',
})
}
mutate()
// mutate()
})()
setMultiSelectMode('off')
},
@ -800,7 +800,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
return Promise.resolve()
})
const id = result.url?.match(/[^/]+$/)?.[0] ?? ''
performActionOnItem('refresh', undefined as unknown as any)
// performActionOnItem('refresh', undefined as unknown as any)
} else {
showErrorToast('Error saving link', { position: 'bottom-right' })
}
@ -811,7 +811,6 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
folder={props.folder}
items={libraryItems}
actionHandler={handleCardAction}
reloadItems={mutate}
setIsChecked={setIsChecked}
itemIsChecked={itemIsChecked}
multiSelectMode={multiSelectMode}
@ -836,18 +835,12 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
const href = `${window.location.pathname}?${qp.toString()}`
router.push(href, href, { shallow: true })
window.sessionStorage.setItem('q', qp.toString())
performActionOnItem('refresh', undefined as unknown as any)
// performActionOnItem('refresh', undefined as unknown as any)
}}
loadMore={() => {
if (isValidating) {
return
}
setSize(size + 1)
}}
hasMore={hasMore}
loadMore={fetchNextPage}
hasMore={hasNextPage ?? false}
hasData={!!itemsPages}
totalItems={itemsPages?.[0].search.pageInfo.totalCount || 0}
isValidating={isValidating}
isValidating={isLoading}
fetchItemsError={!!fetchItemsError}
labelsTarget={labelsTarget}
setLabelsTarget={setLabelsTarget}
@ -864,11 +857,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
setLinkToEdit={setLinkToEdit}
linkToUnsubscribe={linkToUnsubscribe}
setLinkToUnsubscribe={setLinkToUnsubscribe}
numItemsSelected={
multiSelectMode == 'search'
? itemsPages?.[0].search.pageInfo.totalCount || 0
: checkedItems.length
}
numItemsSelected={checkedItems.length}
/>
)
}
@ -877,12 +866,10 @@ export type HomeFeedContentProps = {
folder: string
items: LibraryItem[]
searchTerm?: string
reloadItems: () => void
gridContainerRef: React.RefObject<HTMLDivElement>
applySearchQuery: (searchQuery: string) => void
hasMore: boolean
hasData: boolean
totalItems: number
isValidating: boolean
fetchItemsError: boolean
@ -1162,7 +1149,7 @@ export function LibraryItemsLayout(
}}
style={{ height: '100%', width: '100%' }}
>
<LibraryItems
<LibraryItemsList
folder={props.folder}
items={props.items}
layout={props.layout}
@ -1274,7 +1261,7 @@ type LibraryItemsProps = {
) => Promise<void>
}
function LibraryItems(props: LibraryItemsProps): JSX.Element {
function LibraryItemsList(props: LibraryItemsProps): JSX.Element {
return (
<Box
ref={props.gridContainerRef}

View file

@ -20,8 +20,7 @@ import {
import type {
LibraryItem,
LibraryItemsQueryInput,
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
} from '../../../lib/networking/library_items/useLibraryItems'
import {
useGetViewerQuery,
UserBasicData,

View file

@ -17,7 +17,6 @@ export const articleFragment = gql`
readingProgressAnchorIndex
slug
folder
isArchived
description
linkId
state
@ -60,7 +59,6 @@ export type ArticleFragmentData = {
readingProgressTopPercent?: number
readingProgressAnchorIndex: number
slug: string
isArchived: boolean
description: string
linkId?: string
state?: State

View file

@ -1,5 +1,5 @@
import { gql } from 'graphql-request'
import { LibraryItemNode } from '../queries/useGetLibraryItemsQuery'
import { LibraryItemNode } from '../library_items/useLibraryItems'
import { Label } from './labelFragment'
export const highlightFragment = gql`

View file

@ -1,5 +1,7 @@
import { gql } from 'graphql-request'
import { highlightFragment } from '../fragments/highlightFragment'
import { articleFragment } from '../fragments/articleFragment'
import { labelFragment } from '../fragments/labelFragment'
export const recommendationFragment = gql`
fragment RecommendationFields on Recommendation {
@ -41,7 +43,6 @@ export const GQL_SEARCH_QUERY = gql`
pageType
contentReader
createdAt
isArchived
readingProgressPercent
readingProgressTopPercent
readingProgressAnchorIndex
@ -153,3 +154,36 @@ export const GQL_UPDATE_LIBRARY_ITEM = gql`
}
}
`
export const GQL_GET_LIBRARY_ITEM_CONTENT = gql`
query GetArticle(
$username: String!
$slug: String!
$includeFriendsHighlights: Boolean
) {
article(username: $username, slug: $slug) {
... on ArticleSuccess {
article {
...ArticleFields
content
highlights(input: { includeFriends: $includeFriendsHighlights }) {
...HighlightFields
}
labels {
...LabelFields
}
recommendations {
...RecommendationFields
}
}
}
... on ArticleError {
errorCodes
}
}
}
${articleFragment}
${highlightFragment}
${labelFragment}
${recommendationFragment}
`

View file

@ -0,0 +1,515 @@
import { gql, GraphQLClient } from 'graphql-request'
import {
InfiniteData,
QueryClient,
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { ContentReader, PageType, State } from '../fragments/articleFragment'
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
import { makeGqlFetcher, requestHeaders } from '../networkHelpers'
import { Label } from '../fragments/labelFragment'
import { moveToFolderMutation } from '../mutations/moveToLibraryMutation'
import {
GQL_DELETE_LIBRARY_ITEM,
GQL_GET_LIBRARY_ITEM_CONTENT,
GQL_SEARCH_QUERY,
GQL_SET_LINK_ARCHIVED,
GQL_UPDATE_LIBRARY_ITEM,
} from './gql'
import { parseGraphQLResponse } from '../queries/gql-errors'
import { gqlEndpoint } from '../../appConfig'
import { GraphQLResponse } from 'graphql-request/dist/types'
import { ArticleAttributes } from '../queries/useGetArticleQuery'
function gqlFetcher(
query: string,
variables?: unknown,
requiresAuth = true
): Promise<unknown> {
// if (requiresAuth) {
// verifyAuth()
// }
const graphQLClient = new GraphQLClient(gqlEndpoint, {
credentials: 'include',
mode: 'cors',
})
return graphQLClient.request(query, variables, requestHeaders())
}
const updateItemStateInCache = (
queryClient: QueryClient,
itemId: string,
newState: State
) => {
updateItemPropertyInCache(queryClient, itemId, 'state', newState)
}
function createDictionary(
propertyName: string,
value: any
): { [key: string]: any } {
return {
[propertyName]: value,
}
}
const updateItemPropertyInCache = (
queryClient: QueryClient,
itemId: string,
propertyName: string,
propertyValue: any
) => {
const setter = createDictionary(propertyName, propertyValue)
const keys = queryClient
.getQueryCache()
.findAll({ queryKey: ['libraryItems'] })
keys.forEach((query) => {
queryClient.setQueryData(query.queryKey, (data: any) => {
if (!data) return data
return {
...data,
pages: data.pages.map((page: any) => ({
...page,
edges: page.edges.map((edge: any) =>
edge.node.id === itemId
? { ...edge, node: { ...edge.node, ...setter } }
: edge
),
})),
}
})
})
}
const updateItemPropertiesInCache = (
queryClient: QueryClient,
itemId: string,
item: ArticleAttributes
) => {
const keys = queryClient
.getQueryCache()
.findAll({ queryKey: ['libraryItems'] })
console.log('updateItemPropertiesInCache::libraryItems: ', keys)
keys.forEach((query) => {
queryClient.setQueryData(query.queryKey, (data: any) => {
if (!data) return data
return {
...data,
pages: data.pages.map((page: any) => ({
...page,
edges: page.edges.map((edge: any) =>
edge.node.id === itemId
? { ...edge, node: { ...edge.node, ...item } }
: edge
),
})),
}
})
})
}
export function useGetLibraryItems(
folder: string | undefined,
{ limit, searchQuery }: LibraryItemsQueryInput,
enabled = true
) {
const fullQuery = folder
? (`in:${folder} use:folders ` + (searchQuery ?? '')).trim()
: searchQuery ?? ''
return useInfiniteQuery({
queryKey: ['libraryItems', fullQuery],
queryFn: async ({ pageParam }) => {
const response = (await gqlFetcher(GQL_SEARCH_QUERY, {
after: pageParam,
first: limit,
query: fullQuery,
includeContent: false,
})) as LibraryItemsData
return response.search
},
enabled,
initialPageParam: '0',
getNextPageParam: (lastPage: LibraryItems) => {
return lastPage.pageInfo.hasNextPage
? lastPage?.pageInfo?.endCursor
: undefined
},
})
}
export const useArchiveItem = () => {
const queryClient = useQueryClient()
const archiveItem = async (input: SetLinkArchivedInput) => {
const result = (await gqlFetcher(GQL_SET_LINK_ARCHIVED, {
input,
})) as SetLinkArchivedData
if (result.errorCodes?.length) {
throw new Error(result.errorCodes[0])
}
return result.setLinkArchived
}
return useMutation({
mutationFn: archiveItem,
onMutate: async (input: SetLinkArchivedInput) => {
await queryClient.cancelQueries({ queryKey: ['libraryItems'] })
updateItemStateInCache(
queryClient,
input.linkId,
input.archived ? State.ARCHIVED : State.SUCCEEDED
)
return { previousItems: queryClient.getQueryData(['libraryItems']) }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData(['libraryItems'], context.previousItems)
}
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ['libraryItems'],
})
},
})
}
export const useDeleteItem = () => {
const queryClient = useQueryClient()
const deleteItem = async (itemId: string) => {
const result = (await gqlFetcher(GQL_DELETE_LIBRARY_ITEM, {
input: { articleID: itemId, bookmark: false },
})) as SetBookmarkArticleData
if (result.setBookmarkArticle.errorCodes?.length) {
throw new Error(result.setBookmarkArticle.errorCodes[0])
}
return result.setBookmarkArticle
}
return useMutation({
mutationFn: deleteItem,
onMutate: async (itemId: string) => {
await queryClient.cancelQueries({
queryKey: ['libraryItems'],
})
updateItemStateInCache(queryClient, itemId, State.DELETED)
return { previousItems: queryClient.getQueryData(['libraryItems']) }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData(['libraryItems'], context.previousItems)
}
},
onSettled: async () => {
await queryClient.invalidateQueries({
queryKey: ['libraryItems'],
})
},
})
}
export const useRestoreItem = () => {
const queryClient = useQueryClient()
const restoreItem = async (itemId: string) => {
const result = (await gqlFetcher(GQL_UPDATE_LIBRARY_ITEM, {
input: { pageId: itemId, state: State.SUCCEEDED },
})) as UpdateLibraryItemData
if (result.updatePage.errorCodes?.length) {
throw new Error(result.updatePage.errorCodes[0])
}
return result.updatePage
}
return useMutation({
mutationFn: restoreItem,
onMutate: async (itemId: string) => {
await queryClient.cancelQueries({ queryKey: ['libraryItems'] })
updateItemStateInCache(queryClient, itemId, State.SUCCEEDED)
return { previousItems: queryClient.getQueryData(['libraryItems']) }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData(['libraryItems'], context.previousItems)
}
},
onSettled: async () => {
await queryClient.invalidateQueries({
queryKey: ['libraryItems'],
})
},
})
}
export const useUpdateItemReadStatus = () => {
const queryClient = useQueryClient()
const updateItemReadStatus = async (
input: ArticleReadingProgressMutationInput
) => {
const result = (await gqlFetcher(GQL_SAVE_ARTICLE_READING_PROGRESS, {
input,
})) as ArticleReadingProgressMutationData
if (result.saveArticleReadingProgress.errorCodes?.length) {
throw new Error(result.saveArticleReadingProgress.errorCodes[0])
}
return result.saveArticleReadingProgress.updatedArticle
}
return useMutation({
mutationFn: updateItemReadStatus,
onMutate: async (input: ArticleReadingProgressMutationInput) => {
await queryClient.cancelQueries({ queryKey: ['libraryItems'] })
updateItemPropertyInCache(
queryClient,
input.id,
'readingProgressPercent',
input.readingProgressPercent
)
return { previousItems: queryClient.getQueryData(['libraryItems']) }
},
onError: (error, input, context) => {
if (context?.previousItems) {
queryClient.setQueryData(['libraryItems'], context.previousItems)
}
},
onSettled: (data, error, variables, context) => {
if (data) {
updateItemPropertyInCache(
queryClient,
data.id,
'readingProgressPercent',
data.readingProgressPercent
)
}
},
})
}
export const useGetLibraryItemContent = (username: string, slug: string) => {
const queryClient = useQueryClient()
return useQuery({
queryKey: ['libraryItem', slug],
queryFn: async () => {
console.log('input: ', {
slug,
username,
includeFriendsHighlights: false,
})
const response = (await gqlFetcher(GQL_GET_LIBRARY_ITEM_CONTENT, {
slug,
username,
includeFriendsHighlights: false,
})) as ArticleData
if (response.article.errorCodes?.length) {
throw new Error(response.article.errorCodes[0])
}
const article = response.article.article
if (article) {
updateItemPropertiesInCache(queryClient, article.id, article)
}
return response.article.article
},
})
}
type ArticleResult = {
article?: ArticleAttributes
errorCodes?: string[]
}
type ArticleData = {
article: ArticleResult
}
type ArticleReadingProgressUpdatedArticle = {
id: string
readingProgressPercent: number
readingProgressAnchorIndex: string
}
type ArticleReadingProgressResult = {
errorCodes?: string[]
updatedArticle?: ArticleReadingProgressUpdatedArticle
}
type ArticleReadingProgressMutationData = {
saveArticleReadingProgress: ArticleReadingProgressResult
}
export type ArticleReadingProgressMutationInput = {
id: string
force?: boolean
readingProgressPercent?: number
readingProgressTopPercent?: number
readingProgressAnchorIndex?: number
}
const GQL_SAVE_ARTICLE_READING_PROGRESS = gql`
mutation SaveArticleReadingProgress(
$input: SaveArticleReadingProgressInput!
) {
saveArticleReadingProgress(input: $input) {
... on SaveArticleReadingProgressSuccess {
updatedArticle {
id
readingProgressPercent
readingProgressAnchorIndex
}
}
... on SaveArticleReadingProgressError {
errorCodes
}
}
}
`
// export async function articleReadingProgressMutation(
// input: ArticleReadingProgressMutationInput
// ): Promise<boolean> {
// const mutation = gql`
// mutation SaveArticleReadingProgress(
// $input: SaveArticleReadingProgressInput!
// ) {
// saveArticleReadingProgress(input: $input) {
// ... on SaveArticleReadingProgressSuccess {
// updatedArticle {
// id
// readingProgressPercent
// readingProgressAnchorIndex
// }
// }
// ... on SaveArticleReadingProgressError {
// errorCodes
// }
// }
// }
// `
// try {
// await gqlFetcher(mutation, { input })
// return true
// } catch {
// return false
// }
// }
export interface ReadableItem {
id: string
title: string
slug: string
}
export type LibraryItemsQueryInput = {
limit: number
sortDescending: boolean
searchQuery?: string
cursor?: string
includeContent?: boolean
}
type LibraryItemsData = {
search: LibraryItems
errorCodes?: string[]
}
export type LibraryItems = {
edges: LibraryItem[]
pageInfo: PageInfo
errorCodes?: string[]
}
export type LibraryItem = {
cursor: string
node: LibraryItemNode
isLoading?: boolean | undefined
}
export type LibraryItemNode = {
id: string
title: string
url: string
author?: string
image?: string
createdAt: string
publishedAt?: string
contentReader?: ContentReader
originalArticleUrl: string
readingProgressPercent: number
readingProgressTopPercent?: number
readingProgressAnchorIndex: number
slug: string
folder?: string
description: string
ownedByViewer: boolean
uploadFileId: string
labels?: Label[]
pageId: string
shortId: string
quote: string
annotation: string
state: State
pageType: PageType
siteName?: string
siteIcon?: string
subscription?: string
readAt?: string
savedAt?: string
wordsCount?: number
aiSummary?: string
recommendations?: Recommendation[]
highlights?: Highlight[]
}
export type Recommendation = {
id: string
name: string
note?: string
user?: RecommendingUser
recommendedAt: Date
}
export type RecommendingUser = {
userId: string
name: string
username: string
profileImageURL?: string
}
export type PageInfo = {
hasNextPage: boolean
hasPreviousPage: boolean
startCursor: string
endCursor: string
totalCount: number
}
type SetLinkArchivedInput = {
linkId: string
archived: boolean
}
type SetLinkArchivedSuccess = {
linkId: string
message?: string
}
type SetLinkArchivedData = {
setLinkArchived: SetLinkArchivedSuccess
errorCodes?: string[]
}
type SetBookmarkArticle = {
errorCodes?: string[]
}
type SetBookmarkArticleData = {
setBookmarkArticle: SetBookmarkArticle
}
type UpdateLibraryItem = {
errorCodes?: string[]
}
type UpdateLibraryItemData = {
updatePage: UpdateLibraryItem
}

View file

@ -1,94 +1,96 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
import { LibraryItemsData } from './useGetLibraryItemsQuery'
// import { gqlFetcher } from '../networkHelpers'
// import { LibraryItems } from '../library_items/useLibraryItems'
export type LibraryItemsQueryInput = {
limit?: number
searchQuery?: string
includeContent?: boolean
const foo = () => {
return 'bar'
}
// export type LibraryItemsQueryInput = {
// limit?: number
// searchQuery?: string
// includeContent?: boolean
// }
export async function searchQuery({
limit = 10,
searchQuery,
includeContent = false,
}: LibraryItemsQueryInput): Promise<LibraryItemsData | undefined> {
const query = gql`
query Search(
$after: String
$first: Int
$query: String
$includeContent: Boolean
) {
search(
first: $first
after: $after
query: $query
includeContent: $includeContent
) {
... on SearchSuccess {
edges {
cursor
node {
id
title
slug
url
pageType
contentReader
createdAt
isArchived
readingProgressPercent
readingProgressTopPercent
readingProgressAnchorIndex
author
image
description
publishedAt
ownedByViewer
originalArticleUrl
uploadFileId
labels {
id
name
color
}
pageId
shortId
quote
annotation
state
siteName
subscription
readAt
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
totalCount
}
}
... on SearchError {
errorCodes
}
}
}
`
// export async function searchQuery({
// limit = 10,
// searchQuery,
// includeContent = false,
// }: LibraryItemsQueryInput): Promise<LibraryItemsData | undefined> {
// const query = gql`
// query Search(
// $after: String
// $first: Int
// $query: String
// $includeContent: Boolean
// ) {
// search(
// first: $first
// after: $after
// query: $query
// includeContent: $includeContent
// ) {
// ... on SearchSuccess {
// edges {
// cursor
// node {
// id
// title
// slug
// url
// pageType
// contentReader
// createdAt
// readingProgressPercent
// readingProgressTopPercent
// readingProgressAnchorIndex
// author
// image
// description
// publishedAt
// ownedByViewer
// originalArticleUrl
// uploadFileId
// labels {
// id
// name
// color
// }
// pageId
// shortId
// quote
// annotation
// state
// siteName
// subscription
// readAt
// }
// }
// pageInfo {
// hasNextPage
// hasPreviousPage
// startCursor
// endCursor
// totalCount
// }
// }
// ... on SearchError {
// errorCodes
// }
// }
// }
// `
const variables = {
first: limit,
query: searchQuery,
includeContent,
}
// const variables = {
// first: limit,
// query: searchQuery,
// includeContent,
// }
try {
const data = await gqlFetcher(query, { ...variables })
return (data as LibraryItemsData) || undefined
} catch (error) {
console.log('search error', error)
return undefined
}
}
// try {
// const data = await gqlFetcher(query, { ...variables })
// return (data as LibraryItemsData) || undefined
// } catch (error) {
// console.log('search error', error)
// return undefined
// }
// }

View file

@ -1,138 +0,0 @@
import { State } from '../fragments/articleFragment'
export interface ReadableItem {
id: string
title: string
slug: string
}
export type LibraryItemsQueryInput = {
limit: number
sortDescending: boolean
searchQuery?: string
cursor?: string
includeContent?: boolean
}
export type LibraryItemsData = {
search: LibraryItems
errorCodes?: string[]
}
export type LibraryItems = {
edges: LibraryItem[]
pageInfo: PageInfo
errorCodes?: string[]
}
export type LibraryItem = {
cursor: string
node: LibraryItemNode
isLoading?: boolean | undefined
}
export type LibraryItemNode = {
id: string
title: string
url: string
author?: string
image?: string
createdAt: string
publishedAt?: string
contentReader?: ContentReader
originalArticleUrl: string
readingProgressPercent: number
readingProgressTopPercent?: number
readingProgressAnchorIndex: number
slug: string
folder?: string
isArchived: boolean
description: string
ownedByViewer: boolean
uploadFileId: string
labels?: Label[]
pageId: string
shortId: string
quote: string
annotation: string
state: State
pageType: PageType
siteName?: string
siteIcon?: string
subscription?: string
readAt?: string
savedAt?: string
wordsCount?: number
aiSummary?: string
recommendations?: Recommendation[]
highlights?: Highlight[]
}
export type Recommendation = {
id: string
name: string
note?: string
user?: RecommendingUser
recommendedAt: Date
}
export type RecommendingUser = {
userId: string
name: string
username: string
profileImageURL?: string
}
export type PageInfo = {
hasNextPage: boolean
hasPreviousPage: boolean
startCursor: string
endCursor: string
totalCount: number
}
export type SetLinkArchivedInput = {
linkId: string
archived: boolean
}
type SetLinkArchivedSuccess = {
linkId: string
message?: string
}
export type SetLinkArchivedData = {
setLinkArchived: SetLinkArchivedSuccess
errorCodes?: string[]
}
export type DeleteItemInput = {
articleID: string
bookmark: boolean
}
export type SetBookmarkArticle = {
errorCodes?: string[]
}
export type SetBookmarkArticleData = {
setBookmarkArticle: SetBookmarkArticle
}
export type UpdateLibraryItemInput = {
pageId: string
title?: string
byline?: string | undefined
description?: string
savedAt?: string
publishedAt?: string
state?: State
}
export type UpdateLibraryItem = {
errorCodes?: string[]
}
export type UpdateLibraryItemData = {
updatePage: UpdateLibraryItem
}

View file

@ -9,12 +9,9 @@ import {
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
import { ScopedMutator } from 'swr/dist/_internal'
import { Label, labelFragment } from '../fragments/labelFragment'
import {
LibraryItems,
Recommendation,
recommendationFragment,
} from './useGetLibraryItemsQuery'
import { LibraryItems, Recommendation } from '../library_items/useLibraryItems'
import useSWR from 'swr'
import { recommendationFragment } from '../library_items/gql'
type ArticleQueryInput = {
username?: string
@ -49,7 +46,6 @@ export type ArticleAttributes = {
author?: string
image?: string
savedAt: string
isArchived: boolean
createdAt: string
publishedAt?: string
description?: string

View file

@ -2,7 +2,7 @@ import { gql } from 'graphql-request'
import useSWRInfinite from 'swr/infinite'
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
import { gqlFetcher } from '../networkHelpers'
import { PageInfo } from './useGetLibraryItemsQuery'
import { PageInfo } from '../library_items/useLibraryItems'
interface HighlightsResponse {
data?: Array<HighlightsData>

View file

@ -1,582 +0,0 @@
import { gql } from 'graphql-request'
import useSWRInfinite from 'swr/infinite'
import {
showErrorToast,
showSuccessToast,
showSuccessToastWithUndo,
} from '../../toastHelpers'
import { ContentReader, PageType, State } from '../fragments/articleFragment'
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
import { updatePageMutation } from '../mutations/updatePageMutation'
import { gqlFetcher, makeGqlFetcher } from '../networkHelpers'
import { Label } from './../fragments/labelFragment'
import { moveToFolderMutation } from '../mutations/moveToLibraryMutation'
import useSWR from 'swr'
export interface ReadableItem {
id: string
title: string
slug: string
}
export type LibraryItemsQueryInput = {
limit: number
sortDescending: boolean
searchQuery?: string
cursor?: string
includeContent?: boolean
}
type LibraryItemsQueryResponse = {
itemsPages?: LibraryItemsData[]
itemsDataError?: unknown
isLoading: boolean
isValidating: boolean
error: boolean
size: number
setSize: (
size: number | ((_size: number) => number)
) => Promise<unknown[] | undefined>
performActionOnItem: (action: LibraryItemAction, item: LibraryItem) => void
mutate: () => void
}
type LibraryItemsRawQueryResponse = {
items: LibraryItemNode[]
itemsDataError?: unknown
isLoading: boolean
isValidating: boolean
error: boolean
}
type LibraryItemAction =
| 'archive'
| 'unarchive'
| 'delete'
| 'mark-read'
| 'mark-unread'
| 'refresh'
| 'unsubscribe'
| 'update-item'
| 'move-to-inbox'
export type LibraryItemsData = {
search: LibraryItems
errorCodes?: string[]
}
export type LibraryItems = {
edges: LibraryItem[]
pageInfo: PageInfo
errorCodes?: string[]
}
export type LibraryItem = {
cursor: string
node: LibraryItemNode
isLoading?: boolean | undefined
}
export type LibraryItemNode = {
id: string
title: string
url: string
author?: string
image?: string
createdAt: string
publishedAt?: string
contentReader?: ContentReader
originalArticleUrl: string
readingProgressPercent: number
readingProgressTopPercent?: number
readingProgressAnchorIndex: number
slug: string
folder?: string
isArchived: boolean
description: string
ownedByViewer: boolean
uploadFileId: string
labels?: Label[]
pageId: string
shortId: string
quote: string
annotation: string
state: State
pageType: PageType
siteName?: string
siteIcon?: string
subscription?: string
readAt?: string
savedAt?: string
wordsCount?: number
aiSummary?: string
recommendations?: Recommendation[]
highlights?: Highlight[]
}
export type Recommendation = {
id: string
name: string
note?: string
user?: RecommendingUser
recommendedAt: Date
}
export type RecommendingUser = {
userId: string
name: string
username: string
profileImageURL?: string
}
export type PageInfo = {
hasNextPage: boolean
hasPreviousPage: boolean
startCursor: string
endCursor: string
totalCount: number
}
export const recommendationFragment = gql`
fragment RecommendationFields on Recommendation {
id
name
note
user {
userId
name
username
profileImageURL
}
recommendedAt
}
`
export function useGetLibraryItemsQuery(
folder: string,
{ limit, searchQuery, cursor, includeContent = false }: LibraryItemsQueryInput
): LibraryItemsQueryResponse {
const fullQuery = (`in:${folder} use:folders ` + (searchQuery ?? '')).trim()
const query = gql`
query Search(
$after: String
$first: Int
$query: String
$includeContent: Boolean
) {
search(
first: $first
after: $after
query: $query
includeContent: $includeContent
) {
... on SearchSuccess {
edges {
cursor
node {
id
title
slug
url
folder
pageType
contentReader
createdAt
isArchived
readingProgressPercent
readingProgressTopPercent
readingProgressAnchorIndex
author
image
description
publishedAt
ownedByViewer
originalArticleUrl
uploadFileId
labels {
id
name
color
}
pageId
shortId
quote
annotation
state
siteName
siteIcon
subscription
readAt
savedAt
wordsCount
recommendations {
id
name
note
user {
userId
name
username
profileImageURL
}
recommendedAt
}
highlights {
...HighlightFields
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
totalCount
}
}
... on SearchError {
errorCodes
}
}
}
${highlightFragment}
`
const variables = {
after: cursor,
first: limit,
query: fullQuery,
includeContent,
}
const { data, error, mutate, size, setSize, isValidating } = useSWRInfinite(
(pageIndex, previousPageData) => {
const key = [query, variables.first, variables.query, undefined]
const previousResult = previousPageData as LibraryItemsData
if (pageIndex === 0) {
return key
}
return [
query,
limit,
searchQuery,
pageIndex === 0 ? undefined : previousResult.search.pageInfo.endCursor,
]
},
(args: any[]) => {
const pageIndex = args[3] as number
return gqlFetcher(query, { ...variables, after: pageIndex }, true)
},
{ revalidateFirstPage: false }
)
let responseError = error
let responsePages = data as LibraryItemsData[] | undefined
// We need to check the response errors here and return the error
// it will be nested in the data pages, if there is one error,
// we invalidate the data and return the error. We also zero out
// the response in the case of an error.
if (!error && responsePages) {
const errors = responsePages.filter(
(d) => d.search.errorCodes && d.search.errorCodes.length > 0
)
if (errors?.length > 0) {
responseError = errors
responsePages = undefined
}
}
const getIndexOf = (page: LibraryItems, item: LibraryItem) => {
return page.edges.findIndex((i) => i.node.id === item.node.id)
}
const performActionOnItem = async (
action: LibraryItemAction,
item: LibraryItem
) => {
console.log('performing action on items: ', action)
if (!responsePages) {
return
}
const updateData = (mutatedItem: LibraryItem | undefined) => {
if (!responsePages) {
return
}
for (const searchResults of responsePages) {
const itemIndex = getIndexOf(searchResults.search, item)
if (itemIndex !== -1) {
if (typeof mutatedItem === 'undefined') {
searchResults.search.edges.splice(itemIndex, 1)
} else {
searchResults.search.edges.splice(itemIndex, 1, mutatedItem)
}
break
}
}
mutate(responsePages)
}
switch (action) {
case 'move-to-inbox':
updateData({
cursor: item.cursor,
node: {
...item.node,
folder: 'inbox',
},
})
moveToFolderMutation(item.cursor, 'inbox').then((res) => {
if (res) {
showSuccessToast('Link moved', { position: 'bottom-right' })
} else {
showErrorToast('Error moving link', { position: 'bottom-right' })
}
})
mutate()
break
case 'archive':
updateData({
cursor: item.cursor,
node: {
...item.node,
isArchived: true,
},
})
setLinkArchivedMutation({
linkId: item.node.id,
archived: true,
}).then((res) => {
if (res) {
showSuccessToast('Link archived', { position: 'bottom-right' })
} else {
showErrorToast('Error archiving link', { position: 'bottom-right' })
}
})
mutate()
break
case 'unarchive':
updateData({
cursor: item.cursor,
node: {
...item.node,
isArchived: false,
},
})
setLinkArchivedMutation({
linkId: item.node.id,
archived: false,
}).then((res) => {
if (res) {
showSuccessToast('Link unarchived', { position: 'bottom-right' })
} else {
showErrorToast('Error unarchiving link', {
position: 'bottom-right',
})
}
})
mutate()
break
case 'delete':
updateData({
cursor: item.cursor,
node: {
...item.node,
state: State.DELETED,
},
})
const pageId = item.node.id
deleteLinkMutation(pageId).then((res) => {
if (res) {
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' })
}
})
break
case 'mark-read':
updateData({
cursor: item.cursor,
node: {
...item.node,
readingProgressPercent: 100,
readingProgressTopPercent: 100,
},
})
articleReadingProgressMutation({
id: item.node.id,
force: true,
readingProgressPercent: 100,
readingProgressTopPercent: 100,
readingProgressAnchorIndex: 0,
})
mutate()
break
case 'mark-unread':
updateData({
cursor: item.cursor,
node: {
...item.node,
readingProgressPercent: 0,
readingProgressTopPercent: 0,
readingProgressAnchorIndex: 0,
},
})
articleReadingProgressMutation({
id: item.node.id,
force: true,
readingProgressPercent: 0,
readingProgressTopPercent: 0,
readingProgressAnchorIndex: 0,
})
mutate()
break
case 'update-item':
updateData(item)
mutate()
break
case 'refresh':
await mutate()
}
}
return {
isValidating,
itemsPages: responsePages || undefined,
itemsDataError: responseError,
isLoading: !error && !data,
performActionOnItem,
size,
setSize,
mutate,
error: !!error,
}
}
export function useGetRawSearchItemsQuery(
{
limit,
searchQuery,
cursor,
includeContent = false,
}: LibraryItemsQueryInput,
shouldFetch = true
): LibraryItemsRawQueryResponse {
const query = gql`
query Search(
$after: String
$first: Int
$query: String
$includeContent: Boolean
) {
search(
first: $first
after: $after
query: $query
includeContent: $includeContent
) {
... on SearchSuccess {
edges {
cursor
node {
id
title
slug
url
folder
createdAt
author
image
description
publishedAt
originalArticleUrl
siteName
siteIcon
subscription
readAt
savedAt
wordsCount
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
totalCount
}
}
... on SearchError {
errorCodes
}
}
}
`
const variables = {
after: cursor,
first: limit,
query: searchQuery,
includeContent,
}
const { data, error, isValidating, mutate } = useSWR(
shouldFetch ? [query, variables.first, variables.after] : null,
makeGqlFetcher(query, variables),
{
revalidateIfStale: false,
revalidateOnFocus: false,
}
)
const responseError = error
const responseData = data as LibraryItemsData | undefined
// We need to check the response errors here and return the error
// it will be nested in the data pages, if there is one error,
// we invalidate the data and return the error. We also zero out
// the response in the case of an error.
if (responseData?.errorCodes) {
return {
isValidating: false,
items: [],
isLoading: false,
error: true,
}
}
return {
isValidating,
items: responseData?.search.edges.map((edge) => edge.node) ?? [],
itemsDataError: responseError,
isLoading: !error && !data,
error: !!error,
}
}

View file

@ -1,322 +0,0 @@
import { gql, GraphQLClient } from 'graphql-request'
import {
InfiniteData,
QueryClient,
useInfiniteQuery,
useMutation,
useQueryClient,
} from 'react-query'
import {
showErrorToast,
showSuccessToast,
showSuccessToastWithUndo,
} from '../../toastHelpers'
import { ContentReader, PageType, State } from '../fragments/articleFragment'
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
import { updatePageMutation } from '../mutations/updatePageMutation'
import { makeGqlFetcher, requestHeaders } from '../networkHelpers'
import { Label } from '../fragments/labelFragment'
import { moveToFolderMutation } from '../mutations/moveToLibraryMutation'
import {
LibraryItemNode,
LibraryItems,
LibraryItemsData,
LibraryItemsQueryInput,
SetBookmarkArticleData,
SetLinkArchivedData,
SetLinkArchivedInput,
UpdateLibraryItemData,
} from './types'
import {
GQL_DELETE_LIBRARY_ITEM,
GQL_SEARCH_QUERY,
GQL_SET_LINK_ARCHIVED,
GQL_UPDATE_LIBRARY_ITEM,
} from './gql-queries'
import { parseGraphQLResponse } from './gql-errors'
import { gqlEndpoint } from '../../appConfig'
import { GraphQLResponse } from 'graphql-request/dist/types'
function gqlFetcher(
query: string,
variables?: unknown,
requiresAuth = true
): Promise<unknown> {
// if (requiresAuth) {
// verifyAuth()
// }
const graphQLClient = new GraphQLClient(gqlEndpoint, {
credentials: 'include',
mode: 'cors',
})
return graphQLClient.request(query, variables, requestHeaders())
}
const updateItemStateInCache = (
queryClient: QueryClient,
itemId: string,
newState: State
) => {
const keys = queryClient.getQueryCache().findAll('libraryItems')
keys.forEach((query) => {
queryClient.setQueryData(query.queryKey, (data: any) => {
if (!data) return data
return {
...data,
pages: data.pages.map((page: any) => ({
...page,
edges: page.edges.map((edge: any) =>
edge.node.id === itemId
? { ...edge, node: { ...edge.node, state: newState } }
: edge
),
})),
}
})
})
}
export function useGetLibraryItems(
folder: string | undefined,
{ limit, searchQuery }: LibraryItemsQueryInput
) {
const fullQuery = folder
? (`in:${folder} use:folders ` + (searchQuery ?? '')).trim()
: searchQuery ?? ''
return useInfiniteQuery(
['libraryItems', fullQuery],
async ({ pageParam }) => {
const response = (await gqlFetcher(GQL_SEARCH_QUERY, {
after: pageParam,
first: limit,
query: fullQuery,
includeContent: false,
})) as LibraryItemsData
return response.search
},
{
getNextPageParam: (lastPage: LibraryItems) => {
return lastPage.pageInfo.hasNextPage
? lastPage?.pageInfo?.endCursor
: undefined
},
}
)
}
export const useArchiveItem = () => {
const queryClient = useQueryClient()
const archiveItem = async (input: SetLinkArchivedInput) => {
const result = (await gqlFetcher(GQL_SET_LINK_ARCHIVED, {
input,
})) as SetLinkArchivedData
if (result.errorCodes?.length) {
throw new Error(result.errorCodes[0])
}
return result.setLinkArchived
}
return useMutation(archiveItem, {
onMutate: async (input: SetLinkArchivedInput) => {
await queryClient.cancelQueries('libraryItems')
updateItemStateInCache(
queryClient,
input.linkId,
input.archived ? State.ARCHIVED : State.SUCCEEDED
)
return { previousItems: queryClient.getQueryData('libraryItems') }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData('libraryItems', context.previousItems)
}
},
onSettled: () => {
console.log('settled')
queryClient.invalidateQueries('libraryItems')
},
})
}
export const useDeleteItem = () => {
const queryClient = useQueryClient()
const deleteItem = async (itemId: string) => {
const result = (await gqlFetcher(GQL_DELETE_LIBRARY_ITEM, {
input: { articleID: itemId, bookmark: false },
})) as SetBookmarkArticleData
if (result.setBookmarkArticle.errorCodes?.length) {
throw new Error(result.setBookmarkArticle.errorCodes[0])
}
return result.setBookmarkArticle
}
return useMutation(deleteItem, {
onMutate: async (itemId: string) => {
await queryClient.cancelQueries('libraryItems')
updateItemStateInCache(queryClient, itemId, State.DELETED)
return { previousItems: queryClient.getQueryData('libraryItems') }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData('libraryItems', context.previousItems)
}
},
onSettled: () => {
console.log('settled')
queryClient.invalidateQueries('libraryItems')
},
})
}
export const useRestoreItem = () => {
const queryClient = useQueryClient()
const restoreItem = async (itemId: string) => {
const result = (await gqlFetcher(GQL_UPDATE_LIBRARY_ITEM, {
input: { pageId: itemId, state: State.SUCCEEDED },
})) as UpdateLibraryItemData
console.log('result: ', result)
if (result.updatePage.errorCodes?.length) {
throw new Error(result.updatePage.errorCodes[0])
}
return result.updateLibraryItem
}
return useMutation(restoreItem, {
onMutate: async (itemId: string) => {
await queryClient.cancelQueries('libraryItems')
updateItemStateInCache(queryClient, itemId, State.SUCCEEDED)
return { previousItems: queryClient.getQueryData('libraryItems') }
},
onError: (error, itemId, context) => {
if (context?.previousItems) {
queryClient.setQueryData('libraryItems', context.previousItems)
}
},
onSettled: () => {
console.log('settled')
queryClient.invalidateQueries('libraryItems')
},
})
}
// export const useRestoreItem = () => {
// const queryClient = useQueryClient()
// return useMutation(restoreItem, {
// onMutate: async (itemId) => {
// await queryClient.cancelQueries('libraryItems')
// const previousItems = queryClient.getQueryData<any>('libraryItems')
// updateItemStateInCache(queryClient, itemId, 'ACTIVE')
// return { previousItems }
// },
// onError: (error, itemId, context) => {
// if (context?.previousItems) {
// queryClient.setQueryData('libraryItems', context.previousItems)
// }
// },
// onSettled: () => {
// queryClient.invalidateQueries('libraryItems')
// },
// })
// }
export function useGetRawSearchItemsQuery(
{
limit,
searchQuery,
cursor,
includeContent = false,
}: LibraryItemsQueryInput,
shouldFetch = true
) {
// const query = gql`
// query Search(
// $after: String
// $first: Int
// $query: String
// $includeContent: Boolean
// ) {
// search(
// first: $first
// after: $after
// query: $query
// includeContent: $includeContent
// ) {
// ... on SearchSuccess {
// edges {
// cursor
// node {
// id
// title
// slug
// url
// folder
// createdAt
// author
// image
// description
// publishedAt
// originalArticleUrl
// siteName
// siteIcon
// subscription
// readAt
// savedAt
// wordsCount
// }
// }
// pageInfo {
// hasNextPage
// hasPreviousPage
// startCursor
// endCursor
// totalCount
// }
// }
// ... on SearchError {
// errorCodes
// }
// }
// }
// `
// const { data, error, isFetching, refetch } = useQuery(
// ['rawSearchItems', searchQuery, cursor],
// () =>
// makeGqlFetcher(query, {
// after: cursor,
// first: limit,
// query: searchQuery,
// includeContent,
// }),
// {
// enabled: shouldFetch,
// refetchOnWindowFocus: false,
// }
// )
// const responseData = data as LibraryItemsData | undefined
// if (responseData?.errorCodes) {
return {
isFetching: false,
items: [],
isLoading: false,
error: true,
}
// }
// return {
// isFetching,
// items: responseData?.search.edges.map((edge) => edge.node) ?? [],
// itemsDataError: error,
// isLoading: !error && !data,
// error: !!error,
// }
}

View file

@ -32,6 +32,7 @@
"@radix-ui/react-switch": "^1.0.1",
"@sentry/nextjs": "^7.42.0",
"@stitches/react": "^1.2.5",
"@tanstack/react-query": "^5.51.15",
"allotment": "^1.20.2",
"antd": "4.24.3",
"axios": "^1.2.0",

View file

@ -57,8 +57,6 @@ export default function Debug(): JSX.Element {
result.push({ name: 'savedAt', value: article.savedAt })
result.push({ name: 'createdAt', value: article.createdAt })
result.push({ name: 'publishedAt', value: article.publishedAt ?? 'null' })
result.push({ name: 'isArchived', value: article.isArchived.toString() })
result.push({ name: 'description', value: article.description ?? 'null' })
result.push({

View file

@ -1,10 +1,6 @@
import { PrimaryLayout } from '../../../components/templates/PrimaryLayout'
import { LoadingView } from '../../../components/patterns/LoadingView'
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
import {
removeItemFromCache,
useGetArticleQuery,
} from '../../../lib/networking/queries/useGetArticleQuery'
import { useRouter } from 'next/router'
import { VStack } from './../../../components/elements/LayoutPrimitives'
import {
@ -18,34 +14,31 @@ import { Toaster } from 'react-hot-toast'
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
import Script from 'next/script'
import { ArticleActionsMenu } from '../../../components/templates/article/ArticleActionsMenu'
import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { useSWRConfig } from 'swr'
import {
showErrorToast,
showSuccessToast,
showSuccessToastWithUndo,
} from '../../../lib/toastHelpers'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { SetLabelsModal } from '../../../components/templates/article/SetLabelsModal'
import { DisplaySettingsModal } from '../../../components/templates/article/DisplaySettingsModal'
import { useReaderSettings } from '../../../lib/hooks/useReaderSettings'
import { SkeletonArticleContainer } from '../../../components/templates/article/SkeletonArticleContainer'
import { useRegisterActions } from 'kbar'
import { deleteLinkMutation } from '../../../lib/networking/mutations/deleteLinkMutation'
import { ReaderHeader } from '../../../components/templates/reader/ReaderHeader'
import { EditArticleModal } from '../../../components/templates/homeFeed/EditItemModals'
import { VerticalArticleActionsMenu } from '../../../components/templates/article/VerticalArticleActions'
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'
import { posthog } from 'posthog-js'
import { PDFDisplaySettingsModal } from '../../../components/templates/article/PDFDisplaySettingsModal'
import {
ArticleReadingProgressMutationInput,
useArchiveItem,
useDeleteItem,
useGetLibraryItemContent,
useUpdateItemReadStatus,
} from '../../../lib/networking/library_items/useLibraryItems'
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
() => import('./../../../components/templates/article/PdfArticleContainer'),
@ -57,26 +50,28 @@ const EpubContainerNoSSR = dynamic<EpubContainerProps>(
{ ssr: false }
)
export default function Home(): JSX.Element {
export default function Reader(): JSX.Element {
const router = useRouter()
const { cache, mutate } = useSWRConfig()
const [showEditModal, setShowEditModal] = useState(false)
const [showHighlightsModal, setShowHighlightsModal] = useState(false)
const { viewerData } = useGetViewerQuery()
const readerSettings = useReaderSettings()
const archiveItem = useArchiveItem()
const deleteItem = useDeleteItem()
const updateItemReadStatus = useUpdateItemReadStatus()
const { articleData, articleFetchError } = useGetArticleQuery({
username: router.query.username as string,
slug: router.query.slug as string,
includeFriendsHighlights: false,
})
const article = articleData?.article.article
const { data: libraryItem, error: articleFetchError } =
useGetLibraryItemContent(
router.query.username as string,
router.query.slug as string
)
console.log('articleFetchError: ', articleFetchError)
useEffect(() => {
dispatchLabels({
type: 'RESET',
labels: article?.labels ?? [],
labels: libraryItem?.labels ?? [],
})
}, [articleData?.article.article])
}, [libraryItem])
const goNextOrHome = useCallback(() => {
// const listStr = localStorage.getItem('library-slug-list')
@ -97,7 +92,7 @@ export default function Home(): JSX.Element {
const query = window.sessionStorage.getItem('q')
router.push(`/home?${query}`)
}, [router, viewerData, article])
}, [router, viewerData, libraryItem])
const goPreviousOrHome = useCallback(() => {
// const listStr = localStorage.getItem('library-slug-list')
@ -113,78 +108,77 @@ export default function Home(): JSX.Element {
const query = window.sessionStorage.getItem('q')
router.push(`/home?${query}`)
// router.push(`/home`)
}, [router, viewerData, article])
}, [router, viewerData, libraryItem])
const actionHandler = useCallback(
async (action: string, arg?: unknown) => {
if (!libraryItem) {
return
}
switch (action) {
case 'unarchive':
if (article) {
removeItemFromCache(cache, mutate, article.id)
setLinkArchivedMutation({
linkId: article.id,
archived: false,
}).then((res) => {
if (res) {
showSuccessToast('Link unarchived', {
position: 'bottom-right',
})
} else {
showErrorToast('Error unarchiving link', {
position: 'bottom-right',
})
}
})
goNextOrHome()
}
break
case 'archive':
if (article) {
removeItemFromCache(cache, mutate, article.id)
await setLinkArchivedMutation({
linkId: article.id,
archived: true,
}).then((res) => {
if (!res) {
showErrorToast('Error archiving', {
position: 'bottom-right',
})
} else {
goNextOrHome()
showSuccessToast('Page archived', {
position: 'bottom-right',
})
}
case 'unarchive':
try {
await archiveItem.mutateAsync({
linkId: libraryItem.id,
archived: action == 'archive',
})
} catch {
showErrorToast(`Error ${action}ing item`, {
position: 'bottom-right',
})
return
}
showSuccessToast(`Item ${action}d`, {
position: 'bottom-right',
})
goNextOrHome()
break
case 'mark-read':
if (article) {
articleReadingProgressMutation({
id: article.id,
case 'mark-unread':
const desc = action == 'mark-read' ? 'read' : 'unread'
const values =
action == 'mark-read'
? {
readingProgressPercent: 100,
readingProgressTopPercent: 100,
readingProgressAnchorIndex: 0,
}
: {
readingProgressPercent: 0,
readingProgressTopPercent: 0,
readingProgressAnchorIndex: 0,
}
try {
await updateItemReadStatus.mutateAsync({
id: libraryItem.id,
force: true,
readingProgressPercent: 100,
readingProgressTopPercent: 100,
readingProgressAnchorIndex: 0,
}).then((res) => {
if (!res) {
// todo: revalidate or put back in cache?
showErrorToast('Error marking as read', {
position: 'bottom-right',
})
} else {
goNextOrHome()
}
...values,
})
} catch {
showErrorToast(`Error marking as ${desc}`, {
position: 'bottom-right',
})
return
}
goNextOrHome()
break
case 'delete':
await deleteCurrentItem()
try {
await deleteItem.mutateAsync(libraryItem.id)
} catch {
showErrorToast(`Error deleting item`, {
position: 'bottom-right',
})
return
}
showSuccessToast(`Item deleted`, {
position: 'bottom-right',
})
goNextOrHome()
break
case 'openOriginalArticle':
const url = article?.url
const url = libraryItem?.url
if (url) {
window.open(url, '_blank')
}
@ -206,7 +200,15 @@ export default function Home(): JSX.Element {
break
}
},
[article, viewerData, cache, mutate, router, readerSettings]
[
libraryItem,
viewerData,
router,
readerSettings,
archiveItem,
deleteItem,
updateItemReadStatus,
]
)
useEffect(() => {
@ -224,6 +226,10 @@ export default function Home(): JSX.Element {
actionHandler('mark-read')
}
const markUnread = () => {
actionHandler('mark-unread')
}
const showEditModal = () => {
actionHandler('showEditModal')
}
@ -231,6 +237,7 @@ export default function Home(): JSX.Element {
document.addEventListener('archive', archive)
document.addEventListener('delete', deletePage)
document.addEventListener('mark-read', markRead)
document.addEventListener('mark-unread', markUnread)
document.addEventListener('openOriginalArticle', openOriginalArticle)
document.addEventListener('showEditModal', showEditModal)
@ -240,6 +247,8 @@ export default function Home(): JSX.Element {
return () => {
document.removeEventListener('archive', archive)
document.removeEventListener('mark-read', markRead)
document.removeEventListener('mark-unread', markUnread)
document.removeEventListener('delete', deletePage)
document.removeEventListener('openOriginalArticle', openOriginalArticle)
document.removeEventListener('showEditModal', showEditModal)
@ -249,42 +258,15 @@ export default function Home(): JSX.Element {
}, [actionHandler, goNextOrHome, goPreviousOrHome])
useEffect(() => {
if (article && viewerData?.me) {
if (libraryItem && viewerData?.me) {
posthog.capture('link_read', {
link: article.id,
slug: article.slug,
reader: article.contentReader,
url: article.originalArticleUrl,
link: libraryItem.id,
slug: libraryItem.slug,
reader: libraryItem.contentReader,
url: libraryItem.originalArticleUrl,
})
}
}, [article, viewerData])
const deleteCurrentItem = useCallback(async () => {
if (article) {
const pageId = article.id
removeItemFromCache(cache, mutate, pageId)
await deleteLinkMutation(pageId).then((res) => {
if (res) {
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' })
}
})
goNextOrHome()
}
}, [article, cache, mutate, router])
}, [libraryItem, viewerData])
useRegisterActions(
[
@ -360,6 +342,15 @@ export default function Home(): JSX.Element {
document.dispatchEvent(new Event('mark-read'))
},
},
{
id: 'mark_unread',
section: 'Article',
name: 'Mark current item as unread',
shortcut: ['-'],
perform: () => {
document.dispatchEvent(new Event('mark-unread'))
},
},
{
id: 'full_screen',
section: 'Article',
@ -456,11 +447,14 @@ export default function Home(): JSX.Element {
[readerSettings, showHighlightsModal]
)
const [labels, dispatchLabels] = useSetPageLabels(
articleData?.article.article?.id
)
const [labels, dispatchLabels] = useSetPageLabels(libraryItem?.id)
if (articleFetchError && articleFetchError.indexOf('NOT_FOUND') > -1) {
new Error()
if (
articleFetchError &&
'message' in articleFetchError &&
articleFetchError['message'] === 'NOT_FOUND'
) {
router.push('/404')
return <LoadingView />
}
@ -470,18 +464,18 @@ export default function Home(): JSX.Element {
pageTestId="home-page-tag"
headerToolbarControl={
<ArticleActionsMenu
article={article}
article={libraryItem}
layout="top"
showReaderDisplaySettings={article?.contentReader != 'PDF'}
showReaderDisplaySettings={libraryItem?.contentReader != 'PDF'}
readerSettings={readerSettings}
articleActionHandler={actionHandler}
/>
}
alwaysDisplayToolbar={article?.contentReader == 'PDF'}
alwaysDisplayToolbar={libraryItem?.contentReader == 'PDF'}
pageMetaDataProps={{
title: article?.title ?? '',
title: libraryItem?.title ?? '',
path: router.pathname,
description: article?.description ?? '',
description: libraryItem?.description ?? '',
}}
>
<Script async src="/static/mathjax/mathJaxConfiguration.js" />
@ -497,17 +491,17 @@ export default function Home(): JSX.Element {
showDisplaySettingsModal={
readerSettings.setShowEditDisplaySettingsModal
}
alwaysDisplayToolbar={article?.contentReader == 'PDF'}
alwaysDisplayToolbar={libraryItem?.contentReader == 'PDF'}
>
<VerticalArticleActionsMenu
article={article}
article={libraryItem}
layout="top"
showReaderDisplaySettings={article?.contentReader != 'PDF'}
showReaderDisplaySettings={libraryItem?.contentReader != 'PDF'}
articleActionHandler={actionHandler}
/>
</ReaderHeader>
{article?.contentReader == 'PDF' && <PdfHeaderSpacer />}
{libraryItem?.contentReader == 'PDF' && <PdfHeaderSpacer />}
<VStack
distribution="between"
@ -523,9 +517,9 @@ export default function Home(): JSX.Element {
},
}}
>
{article?.contentReader !== 'PDF' ? (
{libraryItem?.contentReader !== 'PDF' ? (
<ArticleActionsMenu
article={article}
article={libraryItem}
layout="side"
readerSettings={readerSettings}
showReaderDisplaySettings={true}
@ -533,15 +527,15 @@ export default function Home(): JSX.Element {
/>
) : null}
</VStack>
{article && viewerData?.me && article.contentReader == 'PDF' && (
{libraryItem && viewerData?.me && libraryItem.contentReader == 'PDF' && (
<PdfArticleContainerNoSSR
article={article}
article={libraryItem}
showHighlightsModal={showHighlightsModal}
setShowHighlightsModal={setShowHighlightsModal}
viewer={viewerData.me}
/>
)}
{article && viewerData?.me && article.contentReader == 'WEB' && (
{libraryItem && viewerData?.me && libraryItem.contentReader == 'WEB' && (
<VStack
id="article-wrapper"
alignment="center"
@ -558,10 +552,10 @@ export default function Home(): JSX.Element {
},
}}
>
{article && viewerData?.me ? (
{libraryItem && viewerData?.me ? (
<ArticleContainer
viewer={viewerData.me}
article={article}
article={libraryItem}
isAppleAppEmbed={false}
highlightBarDisabled={false}
fontSize={readerSettings.fontSize}
@ -577,14 +571,23 @@ export default function Home(): JSX.Element {
readerSettings.highlightOnRelease ?? undefined
}
textDirection={
article.directionality ?? readerSettings.textDirection
libraryItem.directionality ?? readerSettings.textDirection
}
articleMutations={{
createHighlightMutation,
deleteHighlightMutation,
mergeHighlightMutation,
updateHighlightMutation,
articleReadingProgressMutation,
articleReadingProgressMutation: async (
input: ArticleReadingProgressMutationInput
) => {
try {
await updateItemReadStatus.mutateAsync(input)
} catch {
return false
}
return true
},
}}
/>
) : (
@ -597,7 +600,7 @@ export default function Home(): JSX.Element {
</VStack>
)}
{article && viewerData?.me && article.contentReader == 'EPUB' && (
{libraryItem && viewerData?.me && libraryItem.contentReader == 'EPUB' && (
<VStack
alignment="center"
distribution="start"
@ -610,9 +613,9 @@ export default function Home(): JSX.Element {
paddingTop: '80px',
}}
>
{article && viewerData?.me ? (
{libraryItem && viewerData?.me ? (
<EpubContainerNoSSR
article={article}
article={libraryItem}
showHighlightsModal={showHighlightsModal}
setShowHighlightsModal={setShowHighlightsModal}
viewer={viewerData.me}
@ -627,15 +630,15 @@ export default function Home(): JSX.Element {
</VStack>
)}
{article && readerSettings.showSetLabelsModal && (
{libraryItem && readerSettings.showSetLabelsModal && (
<SetLabelsModal
provider={article}
provider={libraryItem}
selectedLabels={labels.labels}
dispatchLabels={dispatchLabels}
onOpenChange={() => readerSettings.setShowSetLabelsModal(false)}
/>
)}
{article?.contentReader === 'PDF' &&
{libraryItem?.contentReader === 'PDF' &&
readerSettings.showEditDisplaySettingsModal && (
<PDFDisplaySettingsModal
centerX={true}
@ -645,7 +648,7 @@ export default function Home(): JSX.Element {
}}
/>
)}
{article?.contentReader !== 'PDF' &&
{libraryItem?.contentReader !== 'PDF' &&
readerSettings.showEditDisplaySettingsModal && (
<DisplaySettingsModal
centerX={true}
@ -655,16 +658,16 @@ export default function Home(): JSX.Element {
}}
/>
)}
{article && showEditModal && (
{libraryItem && showEditModal && (
<EditArticleModal
article={article}
article={libraryItem}
onOpenChange={() => setShowEditModal(false)}
updateArticle={(title, author, description, savedAt, publishedAt) => {
article.title = title
article.author = author
article.description = description
article.savedAt = savedAt
article.publishedAt = publishedAt
libraryItem.title = title
libraryItem.author = author
libraryItem.description = description
libraryItem.savedAt = savedAt
libraryItem.publishedAt = publishedAt
const titleEvent = new Event('updateTitle') as UpdateTitleEvent
titleEvent.title = title

View file

@ -23,8 +23,7 @@ import { updateTheme } from '../lib/themeUpdater'
import { ThemeId } from '../components/tokens/stitches.config'
import { posthog } from 'posthog-js'
import { GoogleReCaptchaProvider } from '@google-recaptcha/react'
import { SWRConfig } from 'swr'
import { DEFAULT_HOME_PATH } from '../lib/navigations'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
TopBarProgress.config({
barColors: {
@ -76,6 +75,14 @@ const ConditionalCaptchaProvider = (props: {
return <>{props.children}</>
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24,
},
},
})
export function OmnivoreApp({ Component, pageProps }: AppProps): JSX.Element {
const router = useRouter()
@ -91,19 +98,21 @@ export function OmnivoreApp({ Component, pageProps }: AppProps): JSX.Element {
return (
<ConditionalCaptchaProvider>
<KBarProvider actions={generateActions(router)}>
<KBarPortal>
<KBarPositioner style={{ zIndex: 100 }}>
<KBarAnimator style={animatorStyle}>
<KBarSearch style={searchStyle} />
<KBarResultsComponents />
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
<IdProvider>
<Component {...pageProps} />
</IdProvider>
</KBarProvider>
<QueryClientProvider client={queryClient}>
<KBarProvider actions={generateActions(router)}>
<KBarPortal>
<KBarPositioner style={{ zIndex: 100 }}>
<KBarAnimator style={animatorStyle}>
<KBarSearch style={searchStyle} />
<KBarResultsComponents />
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
<IdProvider>
<Component {...pageProps} />
</IdProvider>
</KBarProvider>
</QueryClientProvider>
</ConditionalCaptchaProvider>
)
}

View file

@ -1,32 +0,0 @@
import { PrimaryLayout } from '../components/templates/PrimaryLayout'
import { HomeFeedContainer } from '../components/templates/homeFeed/HomeFeedContainer'
import { VStack } from './../components/elements/LayoutPrimitives'
export default function Home(): JSX.Element {
return <LoadedContent />
}
function LoadedContent(): JSX.Element {
return (
<PrimaryLayout
pageMetaDataProps={{
title: 'Home - Omnivore',
path: '/home',
}}
pageTestId="home-page-tag"
>
<VStack
alignment="start"
distribution="center"
css={{
px: '70px',
backgroundColor: '$thLibraryBackground',
'@lgDown': { px: '20px' },
'@mdDown': { px: '10px' },
}}
>
<HomeFeedContainer />
</VStack>
</PrimaryLayout>
)
}

View file

@ -1,32 +0,0 @@
import { PrimaryLayout } from '../components/templates/PrimaryLayout'
import { HomeFeedContainer } from '../components/templates/homeFeed/HomeFeedContainer'
import { VStack } from './../components/elements/LayoutPrimitives'
export default function Home(): JSX.Element {
return <LoadedContent />
}
function LoadedContent(): JSX.Element {
return (
<PrimaryLayout
pageMetaDataProps={{
title: 'Home - Omnivore',
path: '/home',
}}
pageTestId="home-page-tag"
>
<VStack
alignment="start"
distribution="center"
css={{
px: '70px',
backgroundColor: '$thLibraryBackground',
'@lgDown': { px: '20px' },
'@mdDown': { px: '10px' },
}}
>
<HomeFeedContainer />
</VStack>
</PrimaryLayout>
)
}

View file

@ -10,6 +10,7 @@ import { useMemo } from 'react'
import { HighlightsContainer } from '../../components/nav-containers/HighlightsContainer'
import { usePersistedState } from '../../lib/hooks/usePersistedState'
import { isTouchScreenDevice } from '../../lib/deviceType'
import { State } from '../../lib/networking/fragments/articleFragment'
export default function Home(): JSX.Element {
const router = useRouter()
@ -51,8 +52,8 @@ export default function Home(): JSX.Element {
folder="inbox"
filterFunc={(item) => {
return (
item.state != 'DELETED' &&
!item.isArchived &&
item.state !== State.ARCHIVED &&
item.state !== State.DELETED &&
item.folder == 'inbox'
)
}}
@ -65,8 +66,8 @@ export default function Home(): JSX.Element {
folder="following"
filterFunc={(item) => {
return (
item.state != 'DELETED' &&
!item.isArchived &&
item.state !== State.ARCHIVED &&
item.state !== State.DELETED &&
item.folder == 'following'
)
}}
@ -78,7 +79,7 @@ export default function Home(): JSX.Element {
<LibraryContainer
folder="archive"
filterFunc={(item) => {
return item.state != 'DELETED' && item.isArchived
return item.state == 'ARCHIVED'
}}
showNavigationMenu={showNavigationMenu}
/>

View file

@ -23,7 +23,7 @@ import { emptyTrashMutation } from '../../lib/networking/mutations/emptyTrashMut
import { updateEmailMutation } from '../../lib/networking/mutations/updateEmailMutation'
import { updateUserMutation } from '../../lib/networking/mutations/updateUserMutation'
import { updateUserProfileMutation } from '../../lib/networking/mutations/updateUserProfileMutation'
import { useGetLibraryItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { useGetLibraryItems } from '../../lib/networking/library_items/useLibraryItems'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import { useValidateUsernameQuery } from '../../lib/networking/queries/useValidateUsernameQuery'
import { applyStoredTheme } from '../../lib/themeUpdater'
@ -99,15 +99,15 @@ export default function Account(): JSX.Element {
isUsernameValidationLoading,
])
const { itemsPages, isValidating } = useGetLibraryItemsQuery('', {
const { data: itemsPages, isLoading } = useGetLibraryItems('all', {
limit: 0,
searchQuery: 'in:all',
searchQuery: '',
sortDescending: false,
})
const libraryCount = useMemo(() => {
return itemsPages?.find(() => true)?.search.pageInfo.totalCount
}, [itemsPages, isValidating])
return itemsPages?.pages.find(() => true)?.pageInfo.totalCount
}, [itemsPages, isLoading])
useEffect(() => {
if (viewerData?.me?.profile.username) {
@ -415,7 +415,7 @@ export default function Account(): JSX.Element {
}}
>
<StyledLabel>Account Storage</StyledLabel>
{!isValidating && (
{!isLoading && (
<>
<ProgressBar
fillPercentage={((libraryCount ?? 0) / ACCOUNT_LIMIT) * 100}

View file

@ -14,7 +14,7 @@ import { theme } from '../../components/tokens/stitches.config'
import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
import { useRouter } from 'next/router'
import { useGetLibraryItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { useGetLibraryItems } from '../../lib/networking/library_items/useLibraryItems'
import {
BorderedFormInput,
FormLabel,
@ -34,21 +34,20 @@ export default function BulkPerformer(): JSX.Element {
const [errorMessage, setErrorMessage] = useState<string | undefined>()
const [runningState, setRunningState] = useState<RunningState>('none')
const { itemsPages, isValidating } = useGetLibraryItemsQuery('', {
const { data: itemsPages, isLoading } = useGetLibraryItems(undefined, {
searchQuery: query,
limit: 1,
sortDescending: false,
})
useEffect(() => {
console.log('itemsPages: ', itemsPages)
setExpectedCount(itemsPages?.find(() => true)?.search.pageInfo.totalCount)
setExpectedCount(itemsPages?.pages.find(() => true)?.pageInfo.totalCount)
}, [itemsPages])
const performAction = useCallback(() => {
;(async () => {
console.log('performing action: ', action)
if (isValidating) {
if (isLoading) {
showErrorToast('Query still being validated.')
return
}

View file

@ -1,6 +1,6 @@
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { EditLibraryItemModal } from '../components/templates/homeFeed/EditItemModals'
import { LibraryItem } from '../lib/networking/queries/useGetLibraryItemsQuery'
import { LibraryItem } from '../lib/networking/library_items/useLibraryItems'
export default {
title: 'Components/EditTitleModal',
@ -25,14 +25,15 @@ export default {
},
} as ComponentMeta<typeof EditLibraryItemModal>
export const EditTitleModalStory: ComponentStory<typeof EditLibraryItemModal> =
(args) => (
<EditLibraryItemModal
onOpenChange={() => {}}
item={{
cursor: '',
node: { title: '', description: '' } as LibraryItem['node'],
}}
updateItem={async () => console.log('update item')}
/>
)
export const EditTitleModalStory: ComponentStory<
typeof EditLibraryItemModal
> = (args) => (
<EditLibraryItemModal
onOpenChange={() => {}}
item={{
cursor: '',
node: { title: '', description: '' } as LibraryItem['node'],
}}
updateItem={async () => console.log('update item')}
/>
)

View file

@ -7575,6 +7575,18 @@
dependencies:
defer-to-connect "^1.0.1"
"@tanstack/query-core@5.51.15":
version "5.51.15"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.51.15.tgz#7aee6a2d5d3f64de3e54096607233b1132dc6afd"
integrity sha512-xyobHDJ0yhPE3+UkSQ2/4X1fLSg7ICJI5J1JyU9yf7F3deQfEwSImCDrB1WSRrauJkMtXW7YIEcC0oA6ZZWt5A==
"@tanstack/react-query@^5.51.15":
version "5.51.15"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.51.15.tgz#059bb2966f828263adb355de81410d107e22b5bc"
integrity sha512-UgFg23SrdIYrmfTSxAUn9g+J64VQy11pb9/EefoY/u2+zWuNMeqEOnvpJhf52XQy0yztQoyM9p6x8PFyTNaxXg==
dependencies:
"@tanstack/query-core" "5.51.15"
"@testing-library/cypress@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-8.0.2.tgz#b13f0ff2424dec4368b6670dfbfb7e43af8eefc9"
@ -29243,7 +29255,7 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -29269,15 +29281,6 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
@ -29432,7 +29435,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -29467,13 +29470,6 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@ -32152,7 +32148,7 @@ workerpool@6.2.1:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -32178,15 +32174,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"