From 3d30607068de865d9df8c48fb2c005329f5cdebe Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 25 Jul 2024 14:40:06 +0800 Subject: [PATCH 01/42] Better background colour for card dropdown menu --- packages/web/components/patterns/CardMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/components/patterns/CardMenu.tsx b/packages/web/components/patterns/CardMenu.tsx index 23fb6e7bc..f35892e47 100644 --- a/packages/web/components/patterns/CardMenu.tsx +++ b/packages/web/components/patterns/CardMenu.tsx @@ -28,6 +28,7 @@ export function CardMenu(props: CardMenuProps): JSX.Element { {!props.item.isArchived ? ( Date: Thu, 25 Jul 2024 14:40:26 +0800 Subject: [PATCH 02/42] Try to prevent center text aligned lists in articles --- packages/web/styles/articleInnerStyling.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/styles/articleInnerStyling.css b/packages/web/styles/articleInnerStyling.css index dcd048cd3..de0672a31 100644 --- a/packages/web/styles/articleInnerStyling.css +++ b/packages/web/styles/articleInnerStyling.css @@ -273,6 +273,7 @@ ul { margin: 0; } + text-align: left; } .article-inner-css sup, From e12d892f2bd991319226ae8138ffd7a13a6d47d5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 25 Jul 2024 14:40:41 +0800 Subject: [PATCH 03/42] Color fixes --- packages/web/components/tokens/stitches.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index af8052eba..098dcf61c 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -453,10 +453,10 @@ const sepiaThemeSpec = { readerFontHighContrast: '#0A0806', readerTableHeader: '#FFFFFF', - thLeftMenuBackground: '#EEE8D5', - thNavMenuFooter: '#DDD6C1', + thLeftMenuBackground: '#F8F1E0', + thNavMenuFooter: '#EEE8D5', - thLibrarySelectionColor: '#DDD6C1', + thLibrarySelectionColor: '#EEE8D5', thLabelChipBackground: '#EEE8D5', thBackground4: '#DDD6C166', // used on hover of menu items thBorderColor: '#DDD6C1', From c529e529360fcc60fe9463dda73eab53f1e57a02 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 25 Jul 2024 15:12:31 +0800 Subject: [PATCH 04/42] Allow mutate to re-render, fixes issues with filter funcs --- .../lib/networking/queries/useGetLibraryItemsQuery.tsx | 2 +- packages/web/pages/l/[section].tsx | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 820b36d58..760edbfdb 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -319,7 +319,7 @@ export function useGetLibraryItemsQuery( break } } - mutate(responsePages, false) + mutate(responsePages) } switch (action) { diff --git a/packages/web/pages/l/[section].tsx b/packages/web/pages/l/[section].tsx index 43c8187c0..49565561d 100644 --- a/packages/web/pages/l/[section].tsx +++ b/packages/web/pages/l/[section].tsx @@ -78,11 +78,6 @@ export default function Home(): JSX.Element { { - console.log( - 'running archive filter: ', - item.title, - item.isArchived - ) return item.state != 'DELETED' && item.isArchived }} showNavigationMenu={showNavigationMenu} @@ -92,7 +87,9 @@ export default function Home(): JSX.Element { return ( item.state == 'DELETED'} + filterFunc={(item) => { + return item.state == 'DELETED' + }} showNavigationMenu={showNavigationMenu} /> ) From 06af855621150ec688f369d591a0904f39b966a0 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 26 Jul 2024 23:44:36 +0800 Subject: [PATCH 05/42] WIP: move item queries to react-query to better handle mutations --- .../lib/networking/queries/gql-queries.tsx | 155 +++++++++ packages/web/lib/networking/queries/types.tsx | 138 ++++++++ .../networking/queries/useLibraryItems.tsx | 322 ++++++++++++++++++ 3 files changed, 615 insertions(+) create mode 100644 packages/web/lib/networking/queries/gql-queries.tsx create mode 100644 packages/web/lib/networking/queries/types.tsx create mode 100644 packages/web/lib/networking/queries/useLibraryItems.tsx diff --git a/packages/web/lib/networking/queries/gql-queries.tsx b/packages/web/lib/networking/queries/gql-queries.tsx new file mode 100644 index 000000000..30d2a549f --- /dev/null +++ b/packages/web/lib/networking/queries/gql-queries.tsx @@ -0,0 +1,155 @@ +import { gql } from 'graphql-request' +import { highlightFragment } from '../fragments/highlightFragment' + +export const recommendationFragment = gql` + fragment RecommendationFields on Recommendation { + id + name + note + user { + userId + name + username + profileImageURL + } + recommendedAt + } +` + +export const GQL_SEARCH_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} +` + +export const GQL_SET_LINK_ARCHIVED = gql` + mutation SetLinkArchived($input: ArchiveLinkInput!) { + setLinkArchived(input: $input) { + ... on ArchiveLinkSuccess { + linkId + message + } + ... on ArchiveLinkError { + message + errorCodes + } + } + } +` + +export const GQL_DELETE_LIBRARY_ITEM = gql` + mutation SetBookmarkArticle($input: SetBookmarkArticleInput!) { + setBookmarkArticle(input: $input) { + ... on SetBookmarkArticleSuccess { + bookmarkedArticle { + id + } + } + ... on SetBookmarkArticleError { + errorCodes + } + } + } +` + +export const GQL_UPDATE_LIBRARY_ITEM = gql` + mutation UpdatePage($input: UpdatePageInput!) { + updatePage(input: $input) { + ... on UpdatePageSuccess { + updatedPage { + id + title + url + createdAt + author + image + description + savedAt + publishedAt + } + } + ... on UpdatePageError { + errorCodes + } + } + } +` diff --git a/packages/web/lib/networking/queries/types.tsx b/packages/web/lib/networking/queries/types.tsx new file mode 100644 index 000000000..c028c7417 --- /dev/null +++ b/packages/web/lib/networking/queries/types.tsx @@ -0,0 +1,138 @@ +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 +} diff --git a/packages/web/lib/networking/queries/useLibraryItems.tsx b/packages/web/lib/networking/queries/useLibraryItems.tsx new file mode 100644 index 000000000..c86951662 --- /dev/null +++ b/packages/web/lib/networking/queries/useLibraryItems.tsx @@ -0,0 +1,322 @@ +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 { + // 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('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, + // } +} From 457d1d9de9d9bf725eae166148986c6341096e4a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sun, 28 Jul 2024 20:41:16 +0800 Subject: [PATCH 06/42] More work on react-query switchover --- .../Share/ShareExtensionViewModel.swift | 2 + .../Share/Views/ShareExtensionView.swift | 4 + .../App/Views/Profile/ProfileView.swift | 3 + .../App/Views/WebReader/WebReader.swift | 14 + .../Views/Article/WebViewManager.swift | 2 + .../components/elements/icons/UntrashIcon.tsx | 35 + .../nav-containers/HomeContainer.tsx | 56 +- packages/web/components/patterns/CardMenu.tsx | 28 +- .../web/components/patterns/HighlightView.tsx | 2 +- .../patterns/LibraryCards/CardTypes.tsx | 3 +- .../LibraryCards/LibraryCardStyles.tsx | 6 +- .../LibraryCards/LibraryHighlightGridCard.tsx | 2 +- .../LibraryCards/LibraryHoverActions.tsx | 50 +- .../patterns/ReaderDropdownMenu.tsx | 21 +- .../templates/article/ArticleActionsMenu.tsx | 3 +- .../templates/article/ArticleContainer.tsx | 27 +- .../templates/article/HighlightViewItem.tsx | 2 +- .../templates/article/HighlightsLayer.tsx | 2 +- .../components/templates/article/Notebook.tsx | 2 +- .../templates/article/NotebookHeader.tsx | 2 +- .../templates/article/NotebookModal.tsx | 2 +- .../templates/article/NotebookPresenter.tsx | 2 +- .../article/VerticalArticleActions.tsx | 7 +- .../templates/homeFeed/EditItemModals.tsx | 2 +- .../templates/homeFeed/HighlightItem.tsx | 2 +- .../templates/homeFeed/HighlightsLayout.tsx | 2 +- .../templates/homeFeed/HomeFeedContainer.tsx | 1302 +---------------- .../templates/homeFeed/TLDRLayout.tsx | 2 +- .../templates/library/LibraryContainer.tsx | 389 +++-- .../templates/library/LibrarySideBar.tsx | 3 +- .../networking/fragments/articleFragment.ts | 2 - .../networking/fragments/highlightFragment.ts | 2 +- .../gql-queries.tsx => library_items/gql.tsx} | 36 +- .../library_items/useLibraryItems.tsx | 515 +++++++ .../web/lib/networking/queries/search.tsx | 178 +-- packages/web/lib/networking/queries/types.tsx | 138 -- .../networking/queries/useGetArticleQuery.tsx | 8 +- .../networking/queries/useGetHighlights.tsx | 2 +- .../queries/useGetLibraryItemsQuery.tsx | 582 -------- .../networking/queries/useLibraryItems.tsx | 322 ---- packages/web/package.json | 1 + .../web/pages/[username]/[slug]/debug.tsx | 2 - .../web/pages/[username]/[slug]/index.tsx | 307 ++-- packages/web/pages/_app.tsx | 39 +- packages/web/pages/home-old.tsx | 32 - packages/web/pages/home.tsx | 32 - packages/web/pages/l/[section].tsx | 11 +- packages/web/pages/settings/account.tsx | 12 +- packages/web/pages/tools/bulk.tsx | 9 +- .../web/stories/EditTitleModal.stories.tsx | 25 +- yarn.lock | 43 +- 51 files changed, 1264 insertions(+), 3013 deletions(-) create mode 100644 packages/web/components/elements/icons/UntrashIcon.tsx rename packages/web/lib/networking/{queries/gql-queries.tsx => library_items/gql.tsx} (78%) create mode 100644 packages/web/lib/networking/library_items/useLibraryItems.tsx delete mode 100644 packages/web/lib/networking/queries/types.tsx delete mode 100644 packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx delete mode 100644 packages/web/lib/networking/queries/useLibraryItems.tsx delete mode 100644 packages/web/pages/home-old.tsx delete mode 100644 packages/web/pages/home.tsx diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index a315f1e8f..15e62ee10 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index 8a1e1dc21..f5b57b01d 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -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 diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index d6de5c14c..291e20f47 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -145,6 +145,9 @@ struct ProfileView: View { NavigationLink(destination: TextToSpeechView()) { Text(LocalText.textToSpeechGeneric) } + NavigationLink(destination: SetupLoginsView()) { + Text("Login to sites") + } } #endif diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index beaaa9cc7..9b9fc5bc4 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebViewManager.swift b/apple/OmnivoreKit/Sources/Views/Article/WebViewManager.swift index 022b1bce1..d27a5cdf4 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebViewManager.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebViewManager.swift @@ -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 } diff --git a/packages/web/components/elements/icons/UntrashIcon.tsx b/packages/web/components/elements/icons/UntrashIcon.tsx new file mode 100644 index 000000000..d5952fd36 --- /dev/null +++ b/packages/web/components/elements/icons/UntrashIcon.tsx @@ -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 { + render() { + const size = (this.props.size || 26).toString() + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + + + + + + ) + } +} diff --git a/packages/web/components/nav-containers/HomeContainer.tsx b/packages/web/components/nav-containers/HomeContainer.tsx index 79498322c..230a0e043 100644 --- a/packages/web/components/nav-containers/HomeContainer.tsx +++ b/packages/web/components/nav-containers/HomeContainer.tsx @@ -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 } -const NavigationContext = - createContext(undefined) +const NavigationContext = createContext( + 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 ( diff --git a/packages/web/components/patterns/CardMenu.tsx b/packages/web/components/patterns/CardMenu.tsx index f35892e47..73446847f 100644 --- a/packages/web/components/patterns/CardMenu.tsx +++ b/packages/web/components/patterns/CardMenu.tsx @@ -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 ( - {!props.item.isArchived ? ( + {props.item.state != State.ARCHIVED ? ( props.actionHandler('archive')} title="Archive" @@ -63,15 +67,23 @@ export function CardMenu(props: CardMenuProps): JSX.Element { /> {props.item.readingProgressPercent < 98 ? ( { - props.actionHandler('mark-read') + onSelect={async () => { + await updateItemReadStatus.mutateAsync({ + id: props.item.id, + readingProgressPercent: 100, + force: true, + }) }} title="Mark read" /> ) : ( { - props.actionHandler('mark-unread') + onSelect={async () => { + await updateItemReadStatus.mutateAsync({ + id: props.item.id, + readingProgressPercent: 0, + force: true, + }) }} title="Mark unread" /> diff --git a/packages/web/components/patterns/HighlightView.tsx b/packages/web/components/patterns/HighlightView.tsx index 435b51c23..ba7909ddb 100644 --- a/packages/web/components/patterns/HighlightView.tsx +++ b/packages/web/components/patterns/HighlightView.tsx @@ -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, diff --git a/packages/web/components/patterns/LibraryCards/CardTypes.tsx b/packages/web/components/patterns/LibraryCards/CardTypes.tsx index b0ab5e221..211bf1066 100644 --- a/packages/web/components/patterns/LibraryCards/CardTypes.tsx +++ b/packages/web/components/patterns/LibraryCards/CardTypes.tsx @@ -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 diff --git a/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx b/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx index cc9c18815..19ee4e029 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryCardStyles.tsx @@ -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 ( {props.children} diff --git a/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx index 704fa6237..828935989 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryHighlightGridCard.tsx @@ -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' diff --git a/packages/web/components/patterns/LibraryCards/LibraryHoverActions.tsx b/packages/web/components/patterns/LibraryCards/LibraryHoverActions.tsx index a63f7aea3..08ff50770 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryHoverActions.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryHoverActions.tsx @@ -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 ( { ) : ( )} - {!props.article?.isArchived ? ( + {props.article?.state !== State.ARCHIVED ? ( - {!props.article?.isArchived ? ( + {props.article?.state !== State.ARCHIVED ? ( - ) : ( - - )} - - - - {props.showEditTitleModal && ( - - props.actionHandler('update-item', item) - } - onOpenChange={() => { - props.setShowEditTitleModal(false) - props.setLinkToEdit(undefined) - }} - item={props.linkToEdit as LibraryItem} - /> - )} - {showUnsubscribeConfirmation && ( - setShowUnsubscribeConfirmation(false)} - /> - )} - {props.labelsTarget?.node.id && ( - { - if (props.labelsTarget) { - const activate = props.labelsTarget - props.setActiveItem(activate) - props.setLabelsTarget(undefined) - } - }} - /> - )} - {props.viewer && props.notebookTarget?.node.id && ( - { - // onClose={(highlights: Highlight[]) => { - // if (props.notebookTarget?.node.highlights) { - // props.notebookTarget.node.highlights = highlights - // } - props.setNotebookTarget(open ? props.notebookTarget : undefined) - }} - /> - )} - {showUploadModal && ( - setShowUploadModal(false)} /> - )} - - ) -} - -type LibraryItemsProps = { - items: LibraryItem[] - layout: LayoutType - viewer: UserBasicData | undefined - - gridContainerRef: React.RefObject - - setShowEditTitleModal: (show: boolean) => void - setLinkToEdit: (set: LibraryItem | undefined) => void - setShowUnsubscribeConfirmation: (show: true) => void - setLinkToUnsubscribe: (set: LibraryItem | undefined) => void - - isChecked: (itemId: string) => boolean - setIsChecked: (itemId: string, set: boolean) => void - multiSelectMode: MultiSelectMode - - actionHandler: ( - action: LinkedItemCardAction, - item: LibraryItem | undefined - ) => Promise -} - -function LibraryItems(props: LibraryItemsProps): JSX.Element { - return ( - - {props.items.map((linkedItem) => ( - div': { - bg: '$thLeftMenuBackground', - // bg: '$thLibraryBackground', - }, - '&:focus': { - outline: 'none', - '> div': { - outline: 'none', - bg: '$thBackgroundActive', - }, - }, - '&:hover': { - '> div': { - bg: '$thBackgroundActive', - boxShadow: '$cardBoxShadow', - }, - '> a': { - bg: '$thBackgroundActive', - }, - }, - }} - > - {props.viewer && ( - { - if (action === 'editTitle') { - props.setShowEditTitleModal(true) - props.setLinkToEdit(linkedItem) - } else if (action == 'unsubscribe') { - props.setShowUnsubscribeConfirmation(true) - props.setLinkToUnsubscribe(linkedItem) - } else { - props.actionHandler(action, linkedItem) - } - document.body.style.removeProperty('pointer-events') - }} - /> - )} - - ))} - - ) -} diff --git a/packages/web/components/templates/homeFeed/TLDRLayout.tsx b/packages/web/components/templates/homeFeed/TLDRLayout.tsx index 13d8881f1..7274c8f31 100644 --- a/packages/web/components/templates/homeFeed/TLDRLayout.tsx +++ b/packages/web/components/templates/homeFeed/TLDRLayout.tsx @@ -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' diff --git a/packages/web/components/templates/library/LibraryContainer.tsx b/packages/web/components/templates/library/LibraryContainer.tsx index e9f0128c7..ee8ae35fc 100644 --- a/packages/web/components/templates/library/LibraryContainer.tsx +++ b/packages/web/components/templates/library/LibraryContainer.tsx @@ -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(null) - const [labelsTarget, setLabelsTarget] = - useState(undefined) + const [labelsTarget, setLabelsTarget] = useState( + undefined + ) - const [notebookTarget, setNotebookTarget] = - useState(undefined) + const [notebookTarget, setNotebookTarget] = useState( + undefined + ) const [showAddLinkModal, setShowAddLinkModal] = useState(false) const [showEditTitleModal, setShowEditTitleModal] = useState(false) @@ -114,27 +114,25 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element { useState(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 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%' }} > - Promise } -function LibraryItems(props: LibraryItemsProps): JSX.Element { +function LibraryItemsList(props: LibraryItemsProps): JSX.Element { return ( { + // 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 { +// 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 +} diff --git a/packages/web/lib/networking/queries/search.tsx b/packages/web/lib/networking/queries/search.tsx index cd19944ee..c7993d2e2 100644 --- a/packages/web/lib/networking/queries/search.tsx +++ b/packages/web/lib/networking/queries/search.tsx @@ -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 { - 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 { +// 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 +// } +// } diff --git a/packages/web/lib/networking/queries/types.tsx b/packages/web/lib/networking/queries/types.tsx deleted file mode 100644 index c028c7417..000000000 --- a/packages/web/lib/networking/queries/types.tsx +++ /dev/null @@ -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 -} diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index 282cff9e1..48139d510 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -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 diff --git a/packages/web/lib/networking/queries/useGetHighlights.tsx b/packages/web/lib/networking/queries/useGetHighlights.tsx index 09edb9a33..69a9505d8 100644 --- a/packages/web/lib/networking/queries/useGetHighlights.tsx +++ b/packages/web/lib/networking/queries/useGetHighlights.tsx @@ -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 diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx deleted file mode 100644 index 760edbfdb..000000000 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ /dev/null @@ -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 - 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, - } -} diff --git a/packages/web/lib/networking/queries/useLibraryItems.tsx b/packages/web/lib/networking/queries/useLibraryItems.tsx deleted file mode 100644 index c86951662..000000000 --- a/packages/web/lib/networking/queries/useLibraryItems.tsx +++ /dev/null @@ -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 { - // 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('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, - // } -} diff --git a/packages/web/package.json b/packages/web/package.json index e6d50b316..c4ff22b35 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -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", diff --git a/packages/web/pages/[username]/[slug]/debug.tsx b/packages/web/pages/[username]/[slug]/debug.tsx index 0691eb37b..f933e2abc 100644 --- a/packages/web/pages/[username]/[slug]/debug.tsx +++ b/packages/web/pages/[username]/[slug]/debug.tsx @@ -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({ diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 559f4f8d0..c48ed2859 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -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( () => import('./../../../components/templates/article/PdfArticleContainer'), @@ -57,26 +50,28 @@ const EpubContainerNoSSR = dynamic( { 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 } @@ -470,18 +464,18 @@ export default function Home(): JSX.Element { pageTestId="home-page-tag" headerToolbarControl={ } - alwaysDisplayToolbar={article?.contentReader == 'PDF'} + alwaysDisplayToolbar={libraryItem?.contentReader == 'PDF'} pageMetaDataProps={{ - title: article?.title ?? '', + title: libraryItem?.title ?? '', path: router.pathname, - description: article?.description ?? '', + description: libraryItem?.description ?? '', }} >