feat: virtual list support for infinite list (#439)

This commit is contained in:
Innei 2023-04-26 21:59:39 +08:00 committed by GitHub
parent b811993b35
commit d078d8986d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 141 additions and 108 deletions

View File

@ -101,12 +101,12 @@
"react-hook-form": "7.43.9",
"react-hot-toast": "2.4.0",
"react-i18next": "^12.2.2",
"react-infinite-scroller": "1.2.6",
"react-parallax-tilt": "^1.7.131",
"react-popper": "^2.3.0",
"react-scroll": "1.8.9",
"react-sortablejs": "6.1.4",
"react-string-replace": "^1.1.0",
"react-virtuoso": "4.3.1",
"refractor": "4.8.1",
"rehype-autolink-headings": "6.1.1",
"rehype-infer-description-meta": "^1.1.0",

View File

@ -221,9 +221,6 @@ dependencies:
react-i18next:
specifier: ^12.2.2
version: 12.2.2(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0)
react-infinite-scroller:
specifier: 1.2.6
version: 1.2.6(react@18.2.0)
react-parallax-tilt:
specifier: ^1.7.131
version: 1.7.131(react-dom@18.2.0)(react@18.2.0)
@ -239,6 +236,9 @@ dependencies:
react-string-replace:
specifier: ^1.1.0
version: 1.1.0
react-virtuoso:
specifier: 4.3.1
version: 4.3.1(react-dom@18.2.0)(react@18.2.0)
refractor:
specifier: 4.8.1
version: 4.8.1
@ -12131,15 +12131,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-infinite-scroller@1.2.6(react@18.2.0):
resolution: {integrity: sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==}
peerDependencies:
react: ^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
prop-types: 15.8.1
react: 18.2.0
dev: false
/react-intersection-observer@9.4.3(react@18.2.0):
resolution: {integrity: sha512-WNRqMQvKpupr6MzecAQI0Pj0+JQong307knLP4g/nBex7kYfIaZsPpXaIhKHR+oV8z+goUbH9e10j6lGRnTzlQ==}
peerDependencies:
@ -12311,6 +12302,17 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-virtuoso@4.3.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-2+V0bvA1fASO+etlBG6YB0uj+StizxP3ecDJXgGW/r2z9AH067ehpJy2TSRiEIGQtDTmJAcmZnZzYVnk7AUmbw==}
engines: {node: '>=10'}
peerDependencies:
react: '>=16 || >=17 || >= 18'
react-dom: '>=16 || >=17 || >= 18'
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}

View File

@ -1,6 +1,6 @@
import { useTranslation } from "next-i18next"
import React from "react"
import InfiniteScroll from "react-infinite-scroller"
import { Virtuoso } from "react-virtuoso"
import { Modal } from "~/components/ui/Modal"
@ -18,40 +18,38 @@ export const CharacterList: React.FC<{
title: string
}> = ({ open, setOpen, hasMore, loadMore, list, title }) => {
const { t } = useTranslation("common")
const flattenList = list?.reduce(
(acc, cur) => acc.concat(cur?.list || []),
[] as any[],
) as any[]
return (
<Modal open={open} setOpen={setOpen} title={title} zIndex={20}>
<div className="px-5 overflow-auto flex-1">
<InfiniteScroll
loadMore={loadMore}
hasMore={hasMore}
loader={
<div className="text-sm py-3 text-center" key={0}>
{t("Loading")} ...
</div>
}
useWindow={false}
>
{list?.length ? (
list.map((page) =>
page?.list?.map((sub: any, index) => {
const character = sub?.character || sub?.fromCharacter
return (
<CharacterListItem
key={index}
sub={sub}
character={character}
/>
)
}),
{list?.length ? (
<Virtuoso
overscan={10}
style={{ height: Infinity }}
className="h-screen"
endReached={() => loadMore()}
components={{
Footer: hasMore ? Loader : undefined,
}}
data={flattenList}
itemContent={(index, sub) => {
const character = sub?.character || sub?.fromCharacter
return (
<CharacterListItem key={index} sub={sub} character={character} />
)
) : (
<div className="py-3 text-center text-zinc-300">
{t("No Content Yet.")}
</div>
)}
</InfiniteScroll>
</div>
}}
></Virtuoso>
) : (
<div className="px-5 overflow-auto flex-1">
<div className="py-3 text-center text-zinc-300">
{t("No Content Yet.")}
</div>
</div>
)}
<div className="h-16 border-t flex items-center px-5 py-4">
<Button isBlock onClick={() => setOpen(false)}>
{t("Close")}
@ -60,3 +58,12 @@ export const CharacterList: React.FC<{
</Modal>
)
}
const Loader = () => {
const { t } = useTranslation("common")
return (
<div className="text-sm py-3 text-center" key={0}>
{t("Loading")} ...
</div>
)
}

View File

@ -9,12 +9,13 @@ import { Avatar } from "../ui/Avatar"
import { UniLink } from "../ui/UniLink"
import { FollowingButton } from "./FollowingButton"
const noopArr = [] as any[]
const CharacterListItem: React.FC<{
character: any
sub: any
}> = ({ character, sub }) => {
return (
<div className="py-3 flex items-center justify-between space-x-2 text-sm">
<div className="py-3 flex items-center justify-between space-x-2 text-sm px-5">
<div className="flex flex-1 overflow-hidden space-x-2">
<UniLink
href={getSiteLink({
@ -25,7 +26,7 @@ const CharacterListItem: React.FC<{
<CharacterFloatCard siteId={character?.handle}>
<Avatar
className="align-middle border-2 border-white"
images={character?.metadata?.content?.avatars || []}
images={character?.metadata?.content?.avatars || noopArr}
name={character?.metadata?.content?.name || character?.handle}
size={40}
/>

View File

@ -1,5 +1,5 @@
import { useTranslation } from "next-i18next"
import InfiniteScroll from "react-infinite-scroller"
import { Virtuoso } from "react-virtuoso"
import { CommentInput } from "~/components/common/CommentInput"
import { CommentItem } from "~/components/common/CommentItem"
@ -38,36 +38,44 @@ export const Comment: React.FC<{
</span>
</div>
<CommentInput pageId={page?.id} />
<InfiniteScroll
className="xlog-comment-list space-y-6 pt-6"
loadMore={comments.fetchNextPage as any}
hasMore={comments.hasNextPage}
loader={
<div
className="relative mt-4 w-full text-sm text-center py-4"
key={"loading"}
>
{t("Loading")}...
</div>
<Virtuoso
className="xlog-comment-list mt-6"
useWindowScroll
data={comments.data?.pages}
endReached={() => comments.fetchNextPage()}
components={{
Footer: comments.isLoading ? Loader : undefined,
}}
itemContent={(_, p) =>
p?.list?.map((comment, idx) => (
<CommentItem
className={idx === 0 ? "" : "mt-6"}
originalId={page?.id}
comment={comment}
key={comment.transactionHash}
depth={0}
/>
))
}
>
{comments.isLoading ? (
<div className="relative mt-4 w-full text-sm text-center py-4">
{t("Loading")}...
</div>
) : (
comments.data?.pages?.map((p) =>
p?.list?.map((comment) => (
<CommentItem
originalId={page?.id}
comment={comment}
key={comment.transactionHash}
depth={0}
/>
)),
)
)}
</InfiniteScroll>
></Virtuoso>
{comments.isLoading && (
<div className="relative mt-4 w-full text-sm text-center py-4">
{t("Loading")}...
</div>
)}
</div>
)
}
const Loader = () => {
const { t } = useTranslation("common")
return (
<div
className="relative mt-4 w-full text-sm text-center py-4"
key={"loading"}
>
{t("Loading")}...
</div>
)
}

View File

@ -16,6 +16,7 @@ import { UniLink } from "~/components/ui/UniLink"
import { useDate } from "~/hooks/useDate"
import { CSB_SCAN } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
import { cn } from "~/lib/utils"
export const CommentItem: React.FC<{
comment: NoteEntity & {
@ -23,7 +24,8 @@ export const CommentItem: React.FC<{
}
originalId?: string
depth: number
}> = ({ comment, originalId, depth }) => {
className?: string
}> = ({ comment, originalId, depth, className }) => {
const [replyOpen, setReplyOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
@ -37,7 +39,9 @@ export const CommentItem: React.FC<{
}
return (
<div className={depth > 0 ? "" : "border-b border-dashed pb-6"}>
<div
className={cn(depth > 0 ? "" : "border-b border-dashed pb-6", className)}
>
<div className="flex group">
<div>
<CharacterFloatCard siteId={comment?.character?.handle}>

View File

@ -1,9 +1,9 @@
import { useTranslation } from "next-i18next"
import Link from "next/link"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import InfiniteScroll from "react-infinite-scroller"
import { memo, useEffect, useState } from "react"
import reactStringReplace from "react-string-replace"
import { Virtuoso } from "react-virtuoso"
import { useAccountState } from "@crossbell/connect-kit"
import { Switch } from "@headlessui/react"
@ -43,7 +43,7 @@ const Post = ({
}
return (
<div>
<div className="mt-8">
<div className="flex items-center space-x-2">
<CharacterFloatCard siteId={post.character?.handle}>
<Link
@ -157,6 +157,8 @@ const Post = ({
)
}
const MemoedPost = memo(Post)
export const MainFeed: React.FC<{
type?: FeedType
noteIds?: string[]
@ -226,19 +228,7 @@ export const MainFeed: React.FC<{
return (
<>
<InfiniteScroll
loadMore={feed.fetchNextPage as any}
hasMore={feed.hasNextPage}
loader={
<div
className="relative mt-4 w-full text-sm text-center py-4"
key={"loading"}
>
{t("Loading")} ...
</div>
}
className="space-y-10"
>
<div className="space-y-10">
{hasFiltering && (
<div className="flex items-center text-zinc-500">
<i className="i-mingcute:android-2-line mr-2 text-lg" />
@ -279,27 +269,48 @@ export const MainFeed: React.FC<{
{type === "search" && (
<Tabs items={searchTabs} className="border-none text-sm -my-4"></Tabs>
)}
{feed.isLoading ? (
<div className="text-center text-zinc-600">{t("Loading")}...</div>
) : !feed.data?.pages[0]?.count ? (
<EmptyState />
) : (
<div className="xlog-posts space-y-8 overflow-x-hidden">
{feed.data?.pages.map((posts) =>
posts?.list.map((post) => {
return (
<Post
key={`${post.characterId}-${post.noteId}`}
post={post}
filtering={aiFiltering ? 60 : 0}
keyword={keyword}
/>
)
}),
)}
<div className="xlog-posts !-mt-0">
<Virtuoso
overscan={5}
endReached={() => feed.fetchNextPage()}
useWindowScroll
data={feed.data?.pages}
itemContent={(_, posts) =>
posts?.list.map((post) => {
return (
<MemoedPost
key={`${post.characterId}-${post.noteId}`}
post={post}
filtering={aiFiltering ? 60 : 0}
keyword={keyword}
/>
)
})
}
></Virtuoso>
{feed.isFetching && feed.hasNextPage && <Loader />}
</div>
)}
</InfiniteScroll>
</div>
</>
)
}
const Loader = () => {
const { t } = useTranslation("common")
return (
<div
className="relative w-full text-sm text-center py-4 mt-12"
key={"loading"}
>
{t("Loading")} ...
</div>
)
}