diff --git a/.changeset/honest-drinks-try.md b/.changeset/honest-drinks-try.md new file mode 100644 index 0000000000..2376fce03d --- /dev/null +++ b/.changeset/honest-drinks-try.md @@ -0,0 +1,7 @@ +--- +"@lens-protocol/api-bindings": minor +"@lens-protocol/react": minor +"@lens-protocol/react-web": minor +--- + +adds publication bookmarks diff --git a/examples/web/src/App.tsx b/examples/web/src/App.tsx index ac8896cdfd..ce5e8ab911 100644 --- a/examples/web/src/App.tsx +++ b/examples/web/src/App.tsx @@ -52,6 +52,7 @@ import { import { UseCreateComment } from './publications/UseCreateComment'; import { UseCreatePost } from './publications/UseCreatePost'; import { UseHidePublication } from './publications/UseHidePublication'; +import { UseMyBookmarks } from './publications/UseMyBookmarks'; const { publicClient, webSocketPublicClient } = configureChains( [polygonMumbai], @@ -103,6 +104,7 @@ export function App() { } /> } /> } /> + } /> diff --git a/examples/web/src/publications/PublicationsPage.tsx b/examples/web/src/publications/PublicationsPage.tsx index e50b3d0c43..5631ccf960 100644 --- a/examples/web/src/publications/PublicationsPage.tsx +++ b/examples/web/src/publications/PublicationsPage.tsx @@ -41,6 +41,11 @@ const publicationHooks = [ description: `Add or remove a reaction to a publication.`, path: '/publications/useReactionToggle', }, + { + label: 'useMyBookmarks', + description: `Fetch a list of publications that the current user has bookmarked.`, + path: '/publications/useMyBookmarks', + }, ]; export function PublicationsPage() { diff --git a/examples/web/src/publications/UseMyBookmarks.tsx b/examples/web/src/publications/UseMyBookmarks.tsx new file mode 100644 index 0000000000..a1959bc2a7 --- /dev/null +++ b/examples/web/src/publications/UseMyBookmarks.tsx @@ -0,0 +1,45 @@ +import { Profile, useMyBookmarks } from '@lens-protocol/react-web'; + +import { UnauthenticatedFallback, WhenLoggedIn } from '../components/auth'; +import { ErrorMessage } from '../components/error/ErrorMessage'; +import { Loading } from '../components/loading/Loading'; +import { useInfiniteScroll } from '../hooks/useInfiniteScroll'; +import { PublicationCard } from './components/PublicationCard'; + +export function MyBookmarks({ profile }: { profile: Profile }) { + const { + data: publications, + error, + loading, + hasMore, + observeRef, + } = useInfiniteScroll(useMyBookmarks({ where: {} })); + + if (loading) return ; + + if (error) return ; + + if (publications.length === 0) return No bookmarks yet.; + + return ( + + {publications.map((publication) => ( + + ))} + {hasMore && Loading more...} + + ); +} + +export function UseMyBookmarks() { + return ( + + + useMyBookmarks + + + {({ profile }) => } + + + ); +} diff --git a/packages/api-bindings/src/lens/__helpers__/queries/publication.ts b/packages/api-bindings/src/lens/__helpers__/queries/publication.ts index e6344cbd27..39d6a77ad2 100644 --- a/packages/api-bindings/src/lens/__helpers__/queries/publication.ts +++ b/packages/api-bindings/src/lens/__helpers__/queries/publication.ts @@ -1,5 +1,7 @@ import { PaginatedResultInfo, + PublicationBookmarksDocument, + PublicationBookmarksVariables, PublicationDocument, PublicationVariables, PublicationsDocument, @@ -62,3 +64,20 @@ export function mockSearchPublicationsResponse({ query: SearchPublicationsDocument, }); } + +export function mockProfileBookmarksResponse({ + variables, + items, + info = mockPaginatedResultInfo(), +}: { + variables: PublicationBookmarksVariables; + items: AnyPublication[]; + info?: PaginatedResultInfo; +}) { + return mockAnyPaginatedResponse({ + variables, + items, + info, + query: PublicationBookmarksDocument, + }); +} diff --git a/packages/api-bindings/src/lens/graphql/generated.ts b/packages/api-bindings/src/lens/graphql/generated.ts index 712a79111c..caf70ef054 100644 --- a/packages/api-bindings/src/lens/graphql/generated.ts +++ b/packages/api-bindings/src/lens/graphql/generated.ts @@ -611,6 +611,11 @@ export enum LensProfileManagerRelayErrorReasonType { RequiresSignature = 'REQUIRES_SIGNATURE', } +export enum LensProtocolVersion { + V1 = 'V1', + V2 = 'V2', +} + export enum LensTransactionFailureType { MetadataError = 'METADATA_ERROR', Reverted = 'REVERTED', @@ -4056,6 +4061,32 @@ export type RefreshPublicationMetadataData = { result: { result: RefreshPublicationMetadataResultType }; }; +export type PublicationBookmarksVariables = Exact<{ + request: PublicationBookmarksRequest; + imageSmallSize?: InputMaybe; + imageMediumSize?: InputMaybe; + profileCoverSize?: InputMaybe; + profilePictureSize?: InputMaybe; + activityOn?: InputMaybe | Scalars['AppId']>; + fxRateFor?: InputMaybe; +}>; + +export type PublicationBookmarksData = { + result: { items: Array; pageInfo: PaginatedResultInfo }; +} & InjectCommonQueryParams; + +export type AddPublicationBookmarkVariables = Exact<{ + request: PublicationBookmarkRequest; +}>; + +export type AddPublicationBookmarkData = { result: void | null }; + +export type RemovePublicationBookmarkVariables = Exact<{ + request: PublicationBookmarkRequest; +}>; + +export type RemovePublicationBookmarkData = { result: void | null }; + export type AddReactionVariables = Exact<{ request: ReactionRequest; }>; @@ -11862,6 +11893,188 @@ export type RefreshPublicationMetadataMutationOptions = Apollo.BaseMutationOptio RefreshPublicationMetadataData, RefreshPublicationMetadataVariables >; +export const PublicationBookmarksDocument = /*#__PURE__*/ gql` + query PublicationBookmarks( + $request: PublicationBookmarksRequest! + $imageSmallSize: ImageTransform = {} + $imageMediumSize: ImageTransform = {} + $profileCoverSize: ImageTransform = {} + $profilePictureSize: ImageTransform = {} + $activityOn: [AppId!] + $fxRateFor: SupportedFiatType = USD + ) { + ...InjectCommonQueryParams + result: publicationBookmarks(request: $request) { + items { + ... on Post { + ...Post + } + ... on Comment { + ...Comment + } + ... on Mirror { + ...Mirror + } + ... on Quote { + ...Quote + } + } + pageInfo { + ...PaginatedResultInfo + } + } + } + ${FragmentInjectCommonQueryParams} + ${FragmentPost} + ${FragmentComment} + ${FragmentMirror} + ${FragmentQuote} + ${FragmentPaginatedResultInfo} +`; + +/** + * __usePublicationBookmarks__ + * + * To run a query within a React component, call `usePublicationBookmarks` and pass it any options that fit your needs. + * When your component renders, `usePublicationBookmarks` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = usePublicationBookmarks({ + * variables: { + * request: // value for 'request' + * imageSmallSize: // value for 'imageSmallSize' + * imageMediumSize: // value for 'imageMediumSize' + * profileCoverSize: // value for 'profileCoverSize' + * profilePictureSize: // value for 'profilePictureSize' + * activityOn: // value for 'activityOn' + * fxRateFor: // value for 'fxRateFor' + * }, + * }); + */ +export function usePublicationBookmarks( + baseOptions: Apollo.QueryHookOptions, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery( + PublicationBookmarksDocument, + options, + ); +} +export function usePublicationBookmarksLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + PublicationBookmarksData, + PublicationBookmarksVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery( + PublicationBookmarksDocument, + options, + ); +} +export type PublicationBookmarksHookResult = ReturnType; +export type PublicationBookmarksLazyQueryHookResult = ReturnType< + typeof usePublicationBookmarksLazyQuery +>; +export type PublicationBookmarksQueryResult = Apollo.QueryResult< + PublicationBookmarksData, + PublicationBookmarksVariables +>; +export const AddPublicationBookmarkDocument = /*#__PURE__*/ gql` + mutation AddPublicationBookmark($request: PublicationBookmarkRequest!) { + result: addPublicationBookmark(request: $request) + } +`; +export type AddPublicationBookmarkMutationFn = Apollo.MutationFunction< + AddPublicationBookmarkData, + AddPublicationBookmarkVariables +>; + +/** + * __useAddPublicationBookmark__ + * + * To run a mutation, you first call `useAddPublicationBookmark` within a React component and pass it any options that fit your needs. + * When your component renders, `useAddPublicationBookmark` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [addPublicationBookmark, { data, loading, error }] = useAddPublicationBookmark({ + * variables: { + * request: // value for 'request' + * }, + * }); + */ +export function useAddPublicationBookmark( + baseOptions?: Apollo.MutationHookOptions< + AddPublicationBookmarkData, + AddPublicationBookmarkVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation( + AddPublicationBookmarkDocument, + options, + ); +} +export type AddPublicationBookmarkHookResult = ReturnType; +export type AddPublicationBookmarkMutationResult = + Apollo.MutationResult; +export type AddPublicationBookmarkMutationOptions = Apollo.BaseMutationOptions< + AddPublicationBookmarkData, + AddPublicationBookmarkVariables +>; +export const RemovePublicationBookmarkDocument = /*#__PURE__*/ gql` + mutation RemovePublicationBookmark($request: PublicationBookmarkRequest!) { + result: removePublicationBookmark(request: $request) + } +`; +export type RemovePublicationBookmarkMutationFn = Apollo.MutationFunction< + RemovePublicationBookmarkData, + RemovePublicationBookmarkVariables +>; + +/** + * __useRemovePublicationBookmark__ + * + * To run a mutation, you first call `useRemovePublicationBookmark` within a React component and pass it any options that fit your needs. + * When your component renders, `useRemovePublicationBookmark` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [removePublicationBookmark, { data, loading, error }] = useRemovePublicationBookmark({ + * variables: { + * request: // value for 'request' + * }, + * }); + */ +export function useRemovePublicationBookmark( + baseOptions?: Apollo.MutationHookOptions< + RemovePublicationBookmarkData, + RemovePublicationBookmarkVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation( + RemovePublicationBookmarkDocument, + options, + ); +} +export type RemovePublicationBookmarkHookResult = ReturnType; +export type RemovePublicationBookmarkMutationResult = + Apollo.MutationResult; +export type RemovePublicationBookmarkMutationOptions = Apollo.BaseMutationOptions< + RemovePublicationBookmarkData, + RemovePublicationBookmarkVariables +>; export const AddReactionDocument = /*#__PURE__*/ gql` mutation AddReaction($request: ReactionRequest!) { addReaction(request: $request) diff --git a/packages/api-bindings/src/lens/graphql/publication.graphql b/packages/api-bindings/src/lens/graphql/publication.graphql index b6c104a2fa..260fb90905 100644 --- a/packages/api-bindings/src/lens/graphql/publication.graphql +++ b/packages/api-bindings/src/lens/graphql/publication.graphql @@ -508,3 +508,42 @@ mutation RefreshPublicationMetadata($request: RefreshPublicationMetadataRequest! result } } + +query PublicationBookmarks( + $request: PublicationBookmarksRequest! + $imageSmallSize: ImageTransform = {} + $imageMediumSize: ImageTransform = {} + $profileCoverSize: ImageTransform = {} + $profilePictureSize: ImageTransform = {} + $activityOn: [AppId!] + $fxRateFor: SupportedFiatType = USD +) { + ...InjectCommonQueryParams + result: publicationBookmarks(request: $request) { + items { + ... on Post { + ...Post + } + ... on Comment { + ...Comment + } + ... on Mirror { + ...Mirror + } + ... on Quote { + ...Quote + } + } + pageInfo { + ...PaginatedResultInfo + } + } +} + +mutation AddPublicationBookmark($request: PublicationBookmarkRequest!) { + result: addPublicationBookmark(request: $request) +} + +mutation RemovePublicationBookmark($request: PublicationBookmarkRequest!) { + result: removePublicationBookmark(request: $request) +} diff --git a/packages/react/src/publication/__tests__/useMyBookmarks.spec.ts b/packages/react/src/publication/__tests__/useMyBookmarks.spec.ts new file mode 100644 index 0000000000..53a1086264 --- /dev/null +++ b/packages/react/src/publication/__tests__/useMyBookmarks.spec.ts @@ -0,0 +1,52 @@ +import { LimitType, SafeApolloClient } from '@lens-protocol/api-bindings'; +import { + mockLensApolloClient, + mockPostFragment, + mockProfileBookmarksResponse, +} from '@lens-protocol/api-bindings/mocks'; +import { RenderHookResult, waitFor } from '@testing-library/react'; + +import { renderHookWithMocks } from '../../__helpers__/testing-library'; +import { useMyBookmarks } from '../useMyBookmarks'; + +function setupTestScenario({ client }: { client: SafeApolloClient }) { + return { + renderHook( + callback: (props: TProps) => TResult, + ): RenderHookResult { + return renderHookWithMocks(callback, { + mocks: { + apolloClient: client, + }, + }); + }, + }; +} + +describe(`Given the ${useMyBookmarks.name} hook`, () => { + const publications = [mockPostFragment()]; + const expectations = publications.map(({ __typename, id }) => ({ __typename, id })); + + describe('when a profile is provided', () => { + const client = mockLensApolloClient([ + mockProfileBookmarksResponse({ + variables: { + request: { + where: {}, + limit: LimitType.Ten, + }, + }, + items: publications, + }), + ]); + + it('should settle with the bookmarked publications', async () => { + const { renderHook } = setupTestScenario({ client }); + + const { result } = renderHook(() => useMyBookmarks({ where: {}, limit: LimitType.Ten })); + + await waitFor(() => expect(result.current.loading).toBeFalsy()); + expect(result.current.data).toMatchObject(expectations); + }); + }); +}); diff --git a/packages/react/src/publication/index.ts b/packages/react/src/publication/index.ts index 9635d70837..3c804e96f3 100644 --- a/packages/react/src/publication/index.ts +++ b/packages/react/src/publication/index.ts @@ -7,6 +7,7 @@ export * from './useReactionToggle'; export * from './useWhoReactedToPublication'; export * from './useHidePublication'; export * from './useReportPublication'; +export * from './useMyBookmarks'; /** * Fragments diff --git a/packages/react/src/publication/useMyBookmarks.ts b/packages/react/src/publication/useMyBookmarks.ts new file mode 100644 index 0000000000..2f674f62ac --- /dev/null +++ b/packages/react/src/publication/useMyBookmarks.ts @@ -0,0 +1,35 @@ +import { + AnyPublication, + PublicationBookmarksRequest, + usePublicationBookmarks as useGetProfileBookmarks +} from '@lens-protocol/api-bindings'; + +import { useLensApolloClient } from '../helpers/arguments'; +import { PaginatedArgs, PaginatedReadResult, usePaginatedReadResult } from '../helpers/reads'; + +export type UseMyBookmarksArgs = PaginatedArgs; + +/** + * `useMyBookmarks` is a paginated hook that lets you fetch the bookmarks of a profile owned by the logged in wallet. + * + * You MUST be authenticated via {@link useWalletLogin} to use this hook. + * By default it will fetch the bookmarks of the Active Profile. + * + * @category Bookmarks + * @group Hooks + * @param args - {@link UseMyBookmarksArgs} + */ +export function useMyBookmarks({ + where, + limit, +}: UseMyBookmarksArgs): PaginatedReadResult { + return usePaginatedReadResult( + useGetProfileBookmarks( + useLensApolloClient({ + variables: { + request: { where, limit }, + }, + }), + ), + ); +}
No bookmarks yet.
Loading more...
useMyBookmarks