diff --git a/packages/web/components/templates/NavigationLayout.tsx b/packages/web/components/templates/NavigationLayout.tsx index 0dfdc6a4c..15f1b921f 100644 --- a/packages/web/components/templates/NavigationLayout.tsx +++ b/packages/web/components/templates/NavigationLayout.tsx @@ -11,20 +11,21 @@ import { setupAnalytics } from '../../lib/analytics' import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts' import { logout } from '../../lib/logout' import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme' -import { updateTheme } from '../../lib/themeUpdater' -import { Priority, useRegisterActions } from 'kbar' -import { ThemeId, theme } from '../tokens/stitches.config' +import { useRegisterActions } from 'kbar' +import { theme } from '../tokens/stitches.config' import { NavigationMenu } from './navMenu/NavigationMenu' import { Button } from '../elements/Button' import { List } from '@phosphor-icons/react' import { LIBRARY_LEFT_MENU_WIDTH } from './navMenu/LibraryLegacyMenu' import { AddLinkModal } from './AddLinkModal' -import { saveUrlMutation } from '../../lib/networking/mutations/saveUrlMutation' +import { v4 as uuidv4 } from 'uuid' import { showErrorToast, showSuccessToastWithAction, } from '../../lib/toastHelpers' import useWindowDimensions from '../../lib/hooks/useGetWindowDimensions' +import { useAddItem } from '../../lib/networking/library_items/useLibraryItems' +import { useHandleAddUrl } from '../../lib/hooks/useHandleAddUrl' export type NavigationSection = | 'home' @@ -52,6 +53,7 @@ export function NavigationLayout(props: NavigationLayoutProps): JSX.Element { const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false) const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] = useState(false) + const addItem = useAddItem() useRegisterActions(navigationCommands(router)) @@ -84,22 +86,7 @@ export function NavigationLayout(props: NavigationLayoutProps): JSX.Element { const [showAddLinkModal, setShowAddLinkModal] = useState(false) - const handleLinkAdded = useCallback( - async (link: string, timezone: string, locale: string) => { - const result = await saveUrlMutation(link, timezone, locale) - if (result) { - showSuccessToastWithAction('Link saved', 'Read now', async () => { - window.location.href = `/article?url=${encodeURIComponent(link)}` - return Promise.resolve() - }) - // const id = result.url?.match(/[^/]+$/)?.[0] ?? '' - // performActionOnItem('refresh', undefined as unknown as any) - } else { - showErrorToast('Error saving link', { position: 'bottom-right' }) - } - }, - [] - ) + const handleLinkAdded = useHandleAddUrl() useEffect(() => { document.addEventListener('logout', showLogout) diff --git a/packages/web/components/templates/library/LibraryContainer.tsx b/packages/web/components/templates/library/LibraryContainer.tsx index 7a7fa2f93..05f357e98 100644 --- a/packages/web/components/templates/library/LibraryContainer.tsx +++ b/packages/web/components/templates/library/LibraryContainer.tsx @@ -58,6 +58,7 @@ 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' +import { useHandleAddUrl } from '../../../lib/hooks/useHandleAddUrl' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' @@ -851,24 +852,6 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element { [itemsPages, multiSelectMode, checkedItems] ) - const handleLinkSubmission = async ( - link: string, - timezone: string, - locale: string - ) => { - const result = await saveUrlMutation(link, timezone, locale) - if (result) { - showSuccessToastWithAction('Link saved', 'Read now', async () => { - window.location.href = `/article?url=${encodeURIComponent(link)}` - return Promise.resolve() - }) - const id = result.url?.match(/[^/]+$/)?.[0] ?? '' - // performActionOnItem('refresh', undefined as unknown as any) - } else { - showErrorToast('Error saving link', { position: 'bottom-right' }) - } - } - return ( { setQueryInputs({ ...queryInputs, @@ -959,12 +941,6 @@ export type HomeFeedContentProps = { item: LibraryItem | undefined ) => Promise - handleLinkSubmission: ( - link: string, - timezone: string, - locale: string - ) => Promise - showNavigationMenu: boolean setIsChecked: (itemId: string, set: boolean) => void @@ -1003,6 +979,8 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { return true }, [props]) + const addUrl = useHandleAddUrl() + return ( props.setShowAddLinkModal(false)} /> )} diff --git a/packages/web/lib/hooks/useHandleAddUrl.ts b/packages/web/lib/hooks/useHandleAddUrl.ts new file mode 100644 index 000000000..577f74f29 --- /dev/null +++ b/packages/web/lib/hooks/useHandleAddUrl.ts @@ -0,0 +1,26 @@ +import { useCallback } from 'react' +import { v4 as uuidv4 } from 'uuid' +import { useAddItem } from '../networking/library_items/useLibraryItems' +import { showErrorToast, showSuccessToastWithAction } from '../toastHelpers' + +export const useHandleAddUrl = () => { + const addItem = useAddItem() + return useCallback(async (url: string, timezone: string, locale: string) => { + const itemId = uuidv4() + const result = await addItem.mutateAsync({ + itemId, + url, + timezone, + locale, + }) + console.log('result: ', result) + if (result) { + showSuccessToastWithAction('Item saving', 'Read now', async () => { + window.location.href = `/article?url=${encodeURIComponent(url)}` + return Promise.resolve() + }) + } else { + showErrorToast('Error saving url', { position: 'bottom-right' }) + } + }, []) +} diff --git a/packages/web/lib/networking/library_items/gql.tsx b/packages/web/lib/networking/library_items/gql.tsx index d5d60300e..536503264 100644 --- a/packages/web/lib/networking/library_items/gql.tsx +++ b/packages/web/lib/networking/library_items/gql.tsx @@ -258,3 +258,18 @@ export const GQL_BULK_ACTION = gql` } } ` + +export const GQL_SAVE_URL = gql` + mutation SaveUrl($input: SaveUrlInput!) { + saveUrl(input: $input) { + ... on SaveSuccess { + url + clientRequestId + } + ... on SaveError { + errorCodes + message + } + } + } +` diff --git a/packages/web/lib/networking/library_items/useLibraryItems.tsx b/packages/web/lib/networking/library_items/useLibraryItems.tsx index 6dc365dd5..58cf6accc 100644 --- a/packages/web/lib/networking/library_items/useLibraryItems.tsx +++ b/packages/web/lib/networking/library_items/useLibraryItems.tsx @@ -1,6 +1,5 @@ -import { gql, GraphQLClient } from 'graphql-request' +import { GraphQLClient } from 'graphql-request' import { - InfiniteData, QueryClient, useInfiniteQuery, useMutation, @@ -8,8 +7,8 @@ import { useQueryClient, } from '@tanstack/react-query' import { ContentReader, PageType, State } from '../fragments/articleFragment' -import { Highlight, highlightFragment } from '../fragments/highlightFragment' -import { makeGqlFetcher, requestHeaders } from '../networkHelpers' +import { Highlight } from '../fragments/highlightFragment' +import { requestHeaders } from '../networkHelpers' import { Label } from '../fragments/labelFragment' import { GQL_BULK_ACTION, @@ -17,6 +16,7 @@ import { GQL_GET_LIBRARY_ITEM_CONTENT, GQL_MOVE_ITEM_TO_FOLDER, GQL_SAVE_ARTICLE_READING_PROGRESS, + GQL_SAVE_URL, GQL_SEARCH_QUERY, GQL_SET_LABELS, GQL_SET_LINK_ARCHIVED, @@ -165,12 +165,55 @@ const overwriteItemPropertiesInCache = ( } } +export const insertItemInCache = ( + queryClient: QueryClient, + itemId: string, + url: string +) => { + const keys = queryClient + .getQueryCache() + .findAll({ queryKey: ['libraryItems'] }) + console.log('keys: ', keys) + + keys.forEach((query) => { + queryClient.setQueryData(query.queryKey, (data: any) => { + console.log('data, data.pages', data) + if (!data) return data + if (data.pages.length > 0) { + const firstPage = data.pages[0] as LibraryItems + firstPage.edges = [ + ...firstPage.edges, + { + cursor: firstPage.pageInfo.endCursor, + node: { + id: itemId, + title: url, + url: url, + originalArticleUrl: url, + readingProgressPercent: 0, + readingProgressAnchorIndex: 0, + slug: url, + folder: 'inbox', + ownedByViewer: true, + state: State.PROCESSING, + pageType: PageType.UNKNOWN, + createdAt: new Date().toISOString(), + }, + }, + ] + data.pages[0] = firstPage + console.log('data: ', data) + return data + } + }) + }) +} + export function useGetLibraryItems( folder: string | undefined, { limit, searchQuery }: LibraryItemsQueryInput, enabled = true ) { - console.log('folder: ', folder) const fullQuery = folder ? (`in:${folder} use:folders ` + (searchQuery ?? '')).trim() : searchQuery ?? '' @@ -623,6 +666,56 @@ export const useSetItemLabels = () => { }) } +export const useAddItem = () => { + const queryClient = useQueryClient() + const addItem = async (variables: { + itemId: string + url: string + timezone: string | undefined + locale: string | undefined + }) => { + const result = (await gqlFetcher(GQL_SAVE_URL, { + input: { + clientRequestId: variables.itemId, + url: variables.url, + source: 'add-link', + timezone: variables.timezone, + locale: variables.locale, + }, + })) as SaveUrlData + if (result.saveUrl?.errorCodes?.length) { + throw new Error(result.saveUrl.errorCodes[0]) + } + return result.saveUrl?.clientRequestId + } + return useMutation({ + mutationFn: addItem, + onMutate: async (variables: { + itemId: string + url: string + timezone: string | undefined + locale: string | undefined + }) => { + await queryClient.cancelQueries({ queryKey: ['libraryItems'] }) + const previousState = { + previousItems: queryClient.getQueryData(['libraryItems']), + } + insertItemInCache(queryClient, variables.itemId, variables.url) + return previousState + }, + onError: (error, variables, context) => { + if (context?.previousItems) { + queryClient.setQueryData(['libraryItems'], context.previousItems) + } + }, + onSettled: async () => { + await queryClient.invalidateQueries({ + queryKey: ['libraryItems'], + }) + }, + }) +} + export const useBulkActions = () => { const queryClient = useQueryClient() const bulkAction = async (variables: { @@ -675,6 +768,18 @@ type BulkActionData = { bulkAction: BulkActionResult } +export type SaveUrlResult = { + id?: string + url?: string + slug?: string + clientRequestId?: string + errorCodes?: string[] +} + +export type SaveUrlData = { + saveUrl?: SaveUrlResult +} + type UpdateLibraryItemInput = { pageId: string title?: string @@ -811,16 +916,16 @@ export type LibraryItemNode = { 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 + description?: string + ownedByViewer: boolean + uploadFileId?: string + labels?: Label[] + pageId?: string + shortId?: string + quote?: string + annotation?: string siteName?: string siteIcon?: string subscription?: string