mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
More work on react-query switchover
This commit is contained in:
parent
06af855621
commit
457d1d9de9
51 changed files with 1264 additions and 3013 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -145,6 +145,9 @@ struct ProfileView: View {
|
|||
NavigationLink(destination: TextToSpeechView()) {
|
||||
Text(LocalText.textToSpeechGeneric)
|
||||
}
|
||||
NavigationLink(destination: SetupLoginsView()) {
|
||||
Text("Login to sites")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
35
packages/web/components/elements/icons/UntrashIcon.tsx
Normal file
35
packages/web/components/elements/icons/UntrashIcon.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
`
|
||||
515
packages/web/lib/networking/library_items/useLibraryItems.tsx
Normal file
515
packages/web/lib/networking/library_items/useLibraryItems.tsx
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
// }
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
43
yarn.lock
43
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue