feat: add VirtualList support for feed events

This commit is contained in:
moonrailgun 2024-07-03 00:12:50 +08:00
parent 1b859e3176
commit caf7e9ca72
3 changed files with 90 additions and 63 deletions

View File

@ -5,13 +5,21 @@ import dayjs from 'dayjs';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import { MarkdownViewer } from '../MarkdownEditor'; import { MarkdownViewer } from '../MarkdownEditor';
import { FeedIcon } from './FeedIcon'; import { FeedIcon } from './FeedIcon';
import { cn } from '@/utils/style';
type FeedEventItemType = AppRouterOutput['feed']['events'][number]; type FeedEventItemType = AppRouterOutput['feed']['events'][number];
export const FeedEventItem: React.FC<{ event: FeedEventItemType }> = React.memo( export const FeedEventItem: React.FC<{
({ event }) => { className?: string;
event: FeedEventItemType;
}> = React.memo(({ className, event }) => {
return ( return (
<div className="border-muted flex items-center rounded-lg border px-4 py-2"> <div
className={cn(
'border-muted flex items-center rounded-lg border px-4 py-2',
className
)}
>
<div className="flex-1 gap-2"> <div className="flex-1 gap-2">
<div className="mb-2 flex items-center gap-2 text-sm"> <div className="mb-2 flex items-center gap-2 text-sm">
<div className="border-muted rounded-lg border p-2"> <div className="border-muted rounded-lg border p-2">
@ -43,6 +51,5 @@ export const FeedEventItem: React.FC<{ event: FeedEventItemType }> = React.memo(
</Tooltip> </Tooltip>
</div> </div>
); );
} });
);
FeedEventItem.displayName = 'FeedEventItem'; FeedEventItem.displayName = 'FeedEventItem';

View File

@ -1,7 +1,6 @@
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc'; import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
@ -16,6 +15,7 @@ import { FeedIntegration } from '@/components/feed/FeedIntegration';
import { DialogWrapper } from '@/components/DialogWrapper'; import { DialogWrapper } from '@/components/DialogWrapper';
import { useSocketSubscribeList } from '@/api/socketio'; import { useSocketSubscribeList } from '@/api/socketio';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SimpleVirtualList } from '@/components/SimpleVirtualList';
export const Route = createFileRoute('/feed/$channelId/')({ export const Route = createFileRoute('/feed/$channelId/')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -30,10 +30,18 @@ function PageComponent() {
workspaceId, workspaceId,
channelId, channelId,
}); });
const { data: events = [] } = trpc.feed.events.useQuery({
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
trpc.feed.fetchEventsByCursor.useInfiniteQuery(
{
workspaceId, workspaceId,
channelId, channelId,
}); },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
const deleteMutation = trpc.feed.deleteChannel.useMutation({ const deleteMutation = trpc.feed.deleteChannel.useMutation({
onSuccess: defaultSuccessHandler, onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler, onError: defaultErrorHandler,
@ -55,8 +63,8 @@ function PageComponent() {
}); });
const fullEvents = useMemo( const fullEvents = useMemo(
() => [...realtimeEvents, ...events], () => [...realtimeEvents, ...(data?.pages.flatMap((p) => p.items) ?? [])],
[realtimeEvents, events] [realtimeEvents, data]
); );
return ( return (
@ -102,19 +110,21 @@ function PageComponent() {
/> />
} }
> >
{fullEvents && fullEvents.length === 0 ? ( <div className="h-full w-full overflow-hidden p-4">
<SimpleVirtualList
allData={fullEvents}
estimateSize={100}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onFetchNextPage={fetchNextPage}
renderItem={(item) => <FeedEventItem className="mb-2" event={item} />}
renderEmpty={() => (
<div className="w-full overflow-hidden p-4"> <div className="w-full overflow-hidden p-4">
<FeedApiGuide channelId={channelId} /> <FeedApiGuide channelId={channelId} />
</div> </div>
) : (
<ScrollArea className="h-full overflow-hidden p-4">
<div className="space-y-2">
{(fullEvents ?? []).map((event) => (
<FeedEventItem key={event.id} event={event} />
))}
</div>
</ScrollArea>
)} )}
/>
</div>
</CommonWrapper> </CommonWrapper>
); );
} }

View File

@ -15,6 +15,7 @@ import {
import { prisma } from '../../../model/_client'; import { prisma } from '../../../model/_client';
import _ from 'lodash'; import _ from 'lodash';
import { buildFeedPublicOpenapi, feedIntegrationRouter } from './integration'; import { buildFeedPublicOpenapi, feedIntegrationRouter } from './integration';
import { fetchDataByCursor } from '../../../utils/prisma';
export const feedRouter = router({ export const feedRouter = router({
channels: workspaceProcedure channels: workspaceProcedure
@ -113,33 +114,42 @@ export const feedRouter = router({
return channel; return channel;
}), }),
events: workspaceProcedure fetchEventsByCursor: workspaceProcedure
.meta( .meta(
buildFeedOpenapi({ buildFeedOpenapi({
method: 'GET', method: 'GET',
path: '/{channelId}/events', path: '/{channelId}/fetchEventsByCursor',
description: 'Fetch workspace feed channel events',
}) })
) )
.input( .input(
z.object({ z.object({
channelId: z.string(), channelId: z.string(),
limit: z.number().min(1).max(100).default(50),
cursor: z.string().optional(),
})
)
.output(
z.object({
items: z.array(FeedEventModelSchema),
nextCursor: z.string().optional(),
}) })
) )
.output(z.array(FeedEventModelSchema))
.query(async ({ input }) => { .query(async ({ input }) => {
const { channelId } = input; const { channelId, cursor, limit } = input;
const events = await prisma.feedEvent.findMany({ const { items, nextCursor } = await fetchDataByCursor(prisma.feedEvent, {
where: { where: {
channelId: channelId, channelId,
},
take: 50,
orderBy: {
createdAt: 'desc',
}, },
limit,
cursor,
}); });
return events; return {
items,
nextCursor,
};
}), }),
createChannel: workspaceOwnerProcedure createChannel: workspaceOwnerProcedure
.meta( .meta(