From b98be1a2c92b1f799d133d22dcc2a3585dbb9c95 Mon Sep 17 00:00:00 2001 From: Horacio Herrera Date: Wed, 27 Mar 2024 16:00:49 +0100 Subject: [PATCH] implementing pagination in the frontend WIP --- frontend/packages/app/models/accounts.ts | 30 ++++++-- frontend/packages/app/models/contacts.ts | 14 +--- frontend/packages/app/models/documents.ts | 65 ++++++++++++++--- frontend/packages/app/models/groups.ts | 20 +++-- frontend/packages/app/pages/contacts-page.tsx | 6 ++ frontend/packages/app/pages/feed.tsx | 19 +++-- frontend/packages/app/pages/groups.tsx | 1 + .../app/pages/publication-list-page.tsx | 21 +++++- frontend/packages/shared/package.json | 3 +- .../shared/src/publication-content.tsx | 4 +- frontend/packages/ui/src/list.tsx | 73 +++---------------- yarn.lock | 1 + 12 files changed, 145 insertions(+), 112 deletions(-) diff --git a/frontend/packages/app/models/accounts.ts b/frontend/packages/app/models/accounts.ts index 18011f792..2d832d2bd 100644 --- a/frontend/packages/app/models/accounts.ts +++ b/frontend/packages/app/models/accounts.ts @@ -6,6 +6,7 @@ import {queryKeys} from '@mintter/app/models/query-keys' import {Account, GRPCClient, Profile} from '@mintter/shared' import { UseMutationOptions, + useInfiniteQuery, useMutation, useQueries, useQuery, @@ -41,13 +42,19 @@ function getAccountQuery(grpcClient: GRPCClient, accountId?: string) { export function useAllAccounts(filterSites?: boolean) { // let isDaemonReady = useDaemonReady() const grpcClient = useGRPCClient() - const contacts = useQuery<{accounts: Array}, ConnectError>({ - // enabled: !!isDaemonReady, + const accountsQuery = useInfiniteQuery< + {accounts: Array}, + ConnectError + >({ queryKey: [queryKeys.GET_ALL_ACCOUNTS, filterSites], - queryFn: async () => { - const listed = await grpcClient.accounts.listAccounts({}) + queryFn: async (context) => { + const listed = await grpcClient.accounts.listAccounts({ + pageToken: context.pageParam, + pageSize: 20, + }) if (filterSites) { return { + ...listed, accounts: listed.accounts.filter( (account) => account.profile?.bio !== 'Hypermedia Site. Powered by Mintter.', @@ -59,8 +66,21 @@ export function useAllAccounts(filterSites?: boolean) { onError: (err) => { appError(`useAllAccounts Error ${err.code}: ${err.message}`, err.metadata) }, + getNextPageParam: (lastPage) => { + return lastPage.nextPageToken || undefined + }, }) - return contacts + + const allAccounts = + accountsQuery.data?.pages.flatMap((page) => page.accounts) ?? [] + console.log(`== ~ useAllAccounts ~ accountsQuery:`, accountsQuery) + return { + ...accountsQuery, + data: { + ...accountsQuery.data, + accounts: allAccounts, + }, + } } export function useSetTrusted( diff --git a/frontend/packages/app/models/contacts.ts b/frontend/packages/app/models/contacts.ts index b123a8f4e..8e08e3ab8 100644 --- a/frontend/packages/app/models/contacts.ts +++ b/frontend/packages/app/models/contacts.ts @@ -1,6 +1,6 @@ import {queryKeys} from '@mintter/app/models/query-keys' import {Device} from '@mintter/shared' -import {UseMutationOptions, useMutation, useQuery} from '@tanstack/react-query' +import {UseMutationOptions, useMutation} from '@tanstack/react-query' import {decompressFromEncodedURIComponent} from 'lz-string' import {useGRPCClient, useQueryInvalidator} from '../app-context' import appError from '../errors' @@ -8,18 +8,6 @@ import {useAccount} from './accounts' import {useConnectedPeers} from './networking' import {fullInvalidate} from './query-keys' -export function useContactsList() { - const grpcClient = useGRPCClient() - const contacts = useQuery({ - queryKey: [queryKeys.GET_ALL_ACCOUNTS], - queryFn: async () => { - return await grpcClient.accounts.listAccounts({}) - }, - refetchInterval: 20_000, - }) - return contacts -} - export function useConnectionSummary() { const peerInfo = useConnectedPeers({ refetchInterval: 15_000, diff --git a/frontend/packages/app/models/documents.ts b/frontend/packages/app/models/documents.ts index fcbc49685..13da6df22 100644 --- a/frontend/packages/app/models/documents.ts +++ b/frontend/packages/app/models/documents.ts @@ -36,8 +36,10 @@ import { import {UpdateDraftResponse} from '@mintter/shared/src/client/.generated/documents/v1alpha/documents_pb' import { FetchQueryOptions, + UseInfiniteQueryOptions, UseMutationOptions, UseQueryOptions, + useInfiniteQuery, useMutation, useQueries, useQuery, @@ -61,38 +63,58 @@ import {useGroupContent, useGroups} from './groups' import {queryKeys} from './query-keys' export function usePublicationList( - opts?: UseQueryOptions & {trustedOnly: boolean}, + opts?: UseInfiniteQueryOptions & { + trustedOnly: boolean + }, ) { const {trustedOnly, ...queryOpts} = opts || {} const grpcClient = useGRPCClient() - return useQuery({ + const pubListQuery = useInfiniteQuery({ ...queryOpts, queryKey: [ queryKeys.GET_PUBLICATION_LIST, trustedOnly ? 'trusted' : 'global', ], refetchOnMount: true, - queryFn: async () => { + queryFn: async (context) => { const result = await grpcClient.publications.listPublications({ trustedOnly: trustedOnly, + pageSize: 50, + pageToken: context.pageParam, }) let publications = result.publications.sort((a, b) => sortDocuments(a.document?.updateTime, b.document?.updateTime), ) || [] - publications = publications.filter((pub) => { - return pub.document?.title !== '(HIDDEN) Group Navigation' - }) + // publications = publications.filter((pub) => { + // return pub.document?.title !== '(HIDDEN) Group Navigation' + // }) return { ...result, publications, } }, + getNextPageParam: (lastPage) => { + return lastPage.nextPageToken + }, }) + + const allPublications = + pubListQuery.data?.pages.flatMap((page) => page.publications) || [] + console.log(`== ~ allPublications:`, allPublications) + return { + ...pubListQuery, + data: { + ...pubListQuery.data, + publications: allPublications, + }, + } } export function usePublicationFullList( - opts?: UseQueryOptions & {trustedOnly: boolean}, + opts?: UseInfiniteQueryOptions & { + trustedOnly: boolean + }, ) { const pubList = usePublicationList(opts) const accounts = useAllAccounts() @@ -114,14 +136,15 @@ export function usePublicationFullList( export function useDraftList() { const grpcClient = useGRPCClient() - return useQuery({ + const draftListQuery = useInfiniteQuery({ queryKey: [queryKeys.GET_DRAFT_LIST], refetchOnMount: true, - queryFn: async () => { + queryFn: async (context) => { const result = await grpcClient.drafts.listDrafts({ - pageSize: undefined, - pageToken: undefined, + pageToken: context.pageParam, }) + + console.log(`== ~ queryFn: ~ result:`, result) const documents = result.documents.sort((a, b) => sortDocuments(a.updateTime, b.updateTime), @@ -131,7 +154,27 @@ export function useDraftList() { documents, } }, + getNextPageParam: (lastPage) => { + return lastPage.nextPageToken || undefined + }, }) + + const allDrafts = + draftListQuery.data?.pages.flatMap((page) => page.documents) || [] + + console.log( + `== ~ useDraftList ~ draftListQuery:`, + draftListQuery.data, + allDrafts, + ) + + return { + ...draftListQuery, + data: { + ...draftListQuery.data, + documents: allDrafts, + }, + } } export function useDeleteDraft( diff --git a/frontend/packages/app/models/groups.ts b/frontend/packages/app/models/groups.ts index 3d126cf9c..f2aa3d404 100644 --- a/frontend/packages/app/models/groups.ts +++ b/frontend/packages/app/models/groups.ts @@ -13,8 +13,10 @@ import { } from '@mintter/shared' import {ListDocumentGroupsResponse_Item} from '@mintter/shared/src/client/.generated/groups/v1alpha/groups_pb' import { + UseInfiniteQueryOptions, UseMutationOptions, UseQueryOptions, + useInfiniteQuery, useMutation, useQueries, useQuery, @@ -35,23 +37,31 @@ import { } from '@mintter/shared' import {queryKeys} from './query-keys' -export function useAllGroups(opts?: UseQueryOptions) { +export function useAllGroups( + opts?: UseInfiniteQueryOptions, +) { const grpcClient = useGRPCClient() - const groupsQuery = useQuery({ + const groupsQuery = useInfiniteQuery({ ...opts, queryKey: [queryKeys.GET_GROUPS], - queryFn: async () => { - return await grpcClient.groups.listGroups({}) + queryFn: async (context) => { + return await grpcClient.groups.listGroups({ + pageSize: 50, + pageToken: context.pageParam, + }) }, + getNextPageParam: (lastPage) => lastPage?.nextPageToken ?? undefined, }) + const allGroups = groupsQuery.data?.pages.flatMap((page) => page.groups) || [] + return useMemo(() => { return { ...groupsQuery, data: { ...groupsQuery.data, groups: - groupsQuery.data?.groups?.sort((a, b) => + allGroups?.sort((a, b) => sortDocuments(a.updateTime, b.updateTime), ) || [], }, diff --git a/frontend/packages/app/pages/contacts-page.tsx b/frontend/packages/app/pages/contacts-page.tsx index 559e4275d..873ae80ae 100644 --- a/frontend/packages/app/pages/contacts-page.tsx +++ b/frontend/packages/app/pages/contacts-page.tsx @@ -105,6 +105,8 @@ function ErrorPage({}: {error: any}) { export default function ContactsPage() { const contacts = useAllAccounts(true) + + console.log(`== ~ ContactsPage ~ contacts:`, contacts) const myAccount = useMyAccount() const allAccounts = contacts.data?.accounts || [] const trustedAccounts = allAccounts.filter( @@ -147,6 +149,7 @@ export default function ContactsPage() { { return ( ) }} + onEndReached={() => { + contacts.fetchNextPage() + }} /> {copyDialogContent} diff --git a/frontend/packages/app/pages/feed.tsx b/frontend/packages/app/pages/feed.tsx index 2f8db16dd..8a61717c1 100644 --- a/frontend/packages/app/pages/feed.tsx +++ b/frontend/packages/app/pages/feed.tsx @@ -18,13 +18,13 @@ import { import { Button, ButtonText, - FeedList, - FeedListHandle, Globe, + List, PageContainer, RadioButtons, SizableText, Spinner, + TextProps, Theme, UIAvatar, View, @@ -33,7 +33,7 @@ import { toast, } from '@mintter/ui' import {ArrowRight, ChevronUp, Verified} from '@tamagui/lucide-icons' -import React, {PropsWithChildren, ReactNode, useRef} from 'react' +import React, {PropsWithChildren, ReactNode} from 'react' import Footer from '../components/footer' import {MainWrapperNoScroll} from '../components/main-wrapper' import {useAccount} from '../models/accounts' @@ -162,13 +162,15 @@ type CommentFeedItemProps = { function EntityLink({ id, children, -}: { + ...props +}: TextProps & { id: UnpackedHypermediaId children: ReactNode }) { const navigate = useNavigate('push') return ( { e.stopPropagation() @@ -181,6 +183,7 @@ function EntityLink({ }} numberOfLines={1} textOverflow="ellipsis" // not working. long titles don't look great + {...props} > {children} @@ -762,12 +765,10 @@ const Feed = React.memo(function Feed({tab}: {tab: 'trusted' | 'all'}) { const feed = useFeedWithLatest(tab === 'trusted') const route = useNavRoute() const replace = useNavigate('replace') - const scrollRef = useRef(null) if (route.key !== 'feed') throw new Error('invalid route') return ( - @@ -837,11 +838,9 @@ export const ResourceFeed = React.memo(function ResourceFeed({ id: string }) { const feed = useResourceFeed(id) - const scrollRef = useRef(null) return ( - } footer={ feed.data?.pages?.length && ( diff --git a/frontend/packages/app/pages/groups.tsx b/frontend/packages/app/pages/groups.tsx index 08ad75f94..6d68c9177 100644 --- a/frontend/packages/app/pages/groups.tsx +++ b/frontend/packages/app/pages/groups.tsx @@ -191,6 +191,7 @@ export default function GroupsPage() { ) : groups.length > 0 ? ( { if (!item.group) return null diff --git a/frontend/packages/app/pages/publication-list-page.tsx b/frontend/packages/app/pages/publication-list-page.tsx index bcee81aec..e642109a4 100644 --- a/frontend/packages/app/pages/publication-list-page.tsx +++ b/frontend/packages/app/pages/publication-list-page.tsx @@ -42,8 +42,9 @@ import { } from '../models/documents' import {useGatewayUrl} from '../models/gateway-settings' import {useWaitForPublication} from '../models/web-links' -import {DraftRoute, useNavRoute} from '../utils/navigation' +import {useNavRoute} from '../utils/navigation' import {useOpenDraft} from '../utils/open-draft' +import {DraftRoute} from '../utils/routes' import {useClickNavigate, useNavigate} from '../utils/useNavigate' export const PublicationListPage = memo(PublicationListPageUnmemo) @@ -253,6 +254,10 @@ export function PublicationsList({ key={trustedOnly ? 'trusted' : 'all'} items={items} header={header} + fixedItemHeight={52} + onEndReached={() => { + publications.fetchNextPage() + }} renderItem={({item}) => { const {publication, author, editors} = item if (!publication.document) return null @@ -326,6 +331,7 @@ function DraftsList() { if (drafts.data?.documents.length === 0) { return ( } items={['You have no current Drafts.']} renderItem={({item}) => { @@ -345,9 +351,14 @@ function DraftsList() { } items={drafts.data.documents} + fixedItemHeight={52} renderItem={({item}) => { return }} + onEndReached={() => { + console.log('== ~ DraftsList ~ onEndReached') + drafts.fetchNextPage() + }} /> ) } @@ -362,9 +373,13 @@ const DraftListItem = React.memo(function DraftListItem({ const navigate = useClickNavigate() const {queryClient, grpcClient} = useAppContext() if (!draft.id) throw new Error('DraftListItem requires an id') - const draftRoute: DraftRoute = {key: 'draft', draftId: draft.id} + const draftRoute: DraftRoute = { + key: 'draft', + draftId: draft.id, + variant: null, + } const goToItem = (e: any) => { - navigate(draftRoute, e) + navigate(draftRoute as DraftRoute, e) } return ( <> diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json index 179a68948..cdafd2bd5 100644 --- a/frontend/packages/shared/package.json +++ b/frontend/packages/shared/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@bufbuild/protobuf": "1.4.1", - "@connectrpc/connect-web": "1.1.3" + "@connectrpc/connect-web": "1.1.3", + "react-tweet": "^3.2.0" } } diff --git a/frontend/packages/shared/src/publication-content.tsx b/frontend/packages/shared/src/publication-content.tsx index 43aaa0106..199d5bd03 100644 --- a/frontend/packages/shared/src/publication-content.tsx +++ b/frontend/packages/shared/src/publication-content.tsx @@ -640,7 +640,7 @@ function BlockContent(props: BlockContentProps) { } } - if (props.block.type == 'web-embed') { + if (props.block.type == 'web-embed' && props.block.ref) { return } @@ -1575,7 +1575,7 @@ export function BlockContentNostr({block, ...props}: BlockContentProps) { export function BlockContentTwitter({block, ...props}: BlockContentProps) { const {layoutUnit, onLinkClick} = usePublicationContentContext() - const urlArray = block.ref.split('/') + const urlArray = block.ref?.split('/') ?? [] const tweetId = urlArray[urlArray.length - 1].split('?')[0] const {data, error, isLoading} = useTweet(tweetId) diff --git a/frontend/packages/ui/src/list.tsx b/frontend/packages/ui/src/list.tsx index 73ec6f29d..96dc33918 100644 --- a/frontend/packages/ui/src/list.tsx +++ b/frontend/packages/ui/src/list.tsx @@ -1,80 +1,24 @@ -import {ReactNode, forwardRef, useRef, useState} from 'react' +import {ReactNode, forwardRef, useState} from 'react' import {Virtuoso, VirtuosoHandle} from 'react-virtuoso' import {View, XStack, YStack} from 'tamagui' -export function List({ - items, - renderItem, - header, - footer, -}: { - items: Item[] - renderItem: (row: {item: Item; containerWidth: number}) => ReactNode - header?: ReactNode | null - footer?: ReactNode | null -}) { - const virtuoso = useRef(null) - const [containerWidth, setContainerWidth] = useState(0) - const [containerHeight, setContainerHeight] = useState(0) - return ( - { - setContainerHeight(e.nativeEvent.layout.height) - setContainerWidth(e.nativeEvent.layout.width) - }} - > - header || null, - Footer: () => footer || , - }} - className="main-scroll-wrapper" - totalCount={items?.length || 0} - itemContent={(index) => { - const item = items?.[index] - if (!item) return null - return ( - - {renderItem({item, containerWidth})} - - ) - }} - /> - - ) -} - -export type FeedListHandle = VirtuosoHandle +export type ListHandle = VirtuosoHandle -export const FeedList = forwardRef(function FeedListComponent( +export const List = forwardRef(function ListComponent( { items, renderItem, header, footer, onEndReached, + fixedItemHeight, }: { items: Item[] renderItem: (row: {item: Item; containerWidth: number}) => ReactNode header?: ReactNode | null footer?: ReactNode | null onEndReached?: () => void + fixedItemHeight?: number }, ref: React.Ref, ) { @@ -92,6 +36,7 @@ export const FeedList = forwardRef(function FeedListComponent( > { onEndReached?.() }} @@ -115,7 +60,11 @@ export const FeedList = forwardRef(function FeedListComponent( const item = items?.[index] if (!item) return null return ( - + {renderItem({item, containerWidth})} ) diff --git a/yarn.lock b/yarn.lock index 5f89f8f9f..49629f7d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5156,6 +5156,7 @@ __metadata: "@bufbuild/protobuf": 1.4.1 "@connectrpc/connect-web": 1.1.3 "@mintter/prettier": "*" + react-tweet: ^3.2.0 typescript: 5.1.6 vitest: 0.34.2 languageName: unknown