Skip to content

Commit

Permalink
feat(frontend): use infinite scroll in discussion detail page
Browse files Browse the repository at this point in the history
  • Loading branch information
SundayChen committed Aug 27, 2024
1 parent 4e46fbf commit 1fadee9
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 106 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react-dom": "^18",
"react-i18next": "^14.1.3",
"react-icons": "^5.2.1",
"react-infinite-scroller": "^1.2.6",
"react-markdown": "^9.0.1",
"react-spinners": "^0.14.1",
"rehype-highlight": "^7.0.0",
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"drawer": {
"reply": "Add Comment",
"editComment": "Edit Comment"
}
},
"loading": "Loading..."
},
"Enums": {
"chakra-colors": {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"drawer": {
"reply": "添加评论",
"editComment": "编辑评论"
}
},
"loading": "加载中..."
},
"Enums": {
"chakra-colors": {
Expand Down
251 changes: 152 additions & 99 deletions frontend/src/pages/organizations/[id]/discussion/[local_id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {
Button,
Text,
VStack,
Flex,
Box,
Skeleton,
useDisclosure,
HStack,
Spinner,
Flex,
Skeleton,
SkeletonCircle,
SkeletonText,
} from "@chakra-ui/react";
Expand All @@ -35,6 +36,7 @@ import {
editComment
} from "@/services/discussion";
import { shareContent } from "@/utils/share";
import InfiniteScroll from "react-infinite-scroller";

const DiscussionTopicPage = () => {
const router = useRouter();
Expand All @@ -48,8 +50,9 @@ const DiscussionTopicPage = () => {
const [comments, setComments] = useState<DiscussionComment[]>([]);
const [newComment, setNewComment] = useState<string>("");
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(20);
const [commentCount, setCommentCount] = useState<number>(0);
const [pageSize, setPageSize] = useState<number>(6);
const [hasMore, setHasMore] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isTitleVisible, setIsTitleVisible] = useState(true);
const titleRef = useRef(null);

Expand Down Expand Up @@ -98,8 +101,15 @@ const DiscussionTopicPage = () => {

useEffect(() => {
getTopic();
getCommentsList(page, pageSize);
}, [page, pageSize]);
initializeComments();
}, []);

const initializeComments = async () => {
setIsLoading(true);
const res = await getCommentsList(1, pageSize);
setComments(res.results);
setIsLoading(false);
};

const getTopic = async () => {
try {
Expand All @@ -116,16 +126,16 @@ const DiscussionTopicPage = () => {
});
}
setTopic(null);
router.push(`/organizations/${org_id}/discussion/`)
router.push(`/organizations/${org_id}/discussion/`);
}
};

const getCommentsList = async (page: number = 1, pageSize: number = 20) => {
try {
const res = await listComments(org_id, page, pageSize, topic_local_id);
console.warn(res);
setComments(res.results);
setCommentCount(res.count);
if (res.count <= page * pageSize) setHasMore(false);
console.log("Comments list:", res);
return res;
} catch (error) {
console.error("Failed to get comment list:", error);
if (error.request && error.request.status === 403) {
Expand All @@ -136,11 +146,20 @@ const DiscussionTopicPage = () => {
status: "error",
});
}
setComments([]);
setCommentCount(0);
return {};
}
};

const loadMoreComments = async () => {
if (isLoading) return;
setIsLoading(true);
console.log("Load more comments");
const res = await getCommentsList(page + 1, pageSize);
setComments([...comments, ...res.results]);
setPage(page + 1);
setIsLoading(false);
};

const handleSubmission = async () => {
try {
await createComment(org_id, topic_local_id, newComment);
Expand All @@ -161,7 +180,8 @@ const DiscussionTopicPage = () => {
});
setNewComment("");
onClose();
getCommentsList(page, pageSize);

// TODO: Operations after submission
};

const handleTopicDelete = async () => {
Expand Down Expand Up @@ -203,10 +223,18 @@ const DiscussionTopicPage = () => {
});
}
}
getCommentsList(page, pageSize);
if (hasMore) {
const res = await getCommentsList(page, pageSize);
setComments([...comments.filter((c) => c.local_id !== comment.local_id), res.results[res.results.length - 1]]);
}
else
setComments(comments.filter((c) => c.local_id !== comment.local_id));
};

const handleCommentEdit = async (comment: DiscussionComment, newContent: string) => {
const handleCommentEdit = async (
comment: DiscussionComment,
newContent: string
) => {
if (newContent === comment.content) return;
try {
await editComment(org_id, topic_local_id, comment.local_id, newContent);
Expand All @@ -225,99 +253,124 @@ const DiscussionTopicPage = () => {
});
}
}
getCommentsList(page, pageSize);
setComments(
comments.map((c) =>
c.local_id === comment.local_id ? { ...c, content: newContent } : c
)
);
};


return (
<>
<Head>
<meta name="headerTitle" content={orgCtx.basicInfo?.display_name} />
<meta name="headerBreadcrumbs" content="" />
</Head>
<Grid templateColumns="repeat(4, 1fr)" gap={16}>
<GridItem colSpan={{ base: 4, md: 3 }}>
<VStack spacing={6} align="stretch">
<Skeleton isLoaded={topic!==null}>
<Heading as="h3" size="lg" wordBreak="break-all" ref={titleRef} px={1}>
{topic?.title}
<Text
as="span"
fontWeight="normal"
color="gray.400"
ml={2}
>{`#${topic?.local_id}`}</Text>
</Heading>
</Skeleton>
{comments && comments.length > 0 ? (
<CommentList
items={comments}
onCommentDelete={handleCommentDelete}
onCommentEdit={handleCommentEdit}
onTopicDelete={handleTopicDelete}
topic_op={topic?.user}
/>
) : (
<Flex justify="space-between" alignItems="flex-start">
<SkeletonCircle size="10" />
<SkeletonText
ml={{ base: "2", md: "4" }}
noOfLines={4}
spacing="4"
skeletonHeight="4"
flex="1"
<Box overflow='auto' height='80vh' id='scrollableDiv'>
<Grid templateColumns="repeat(4, 1fr)" gap={16}>
<GridItem colSpan={{ base: 4, md: 3 }}>
<VStack spacing={6} align="stretch">
<Skeleton isLoaded={topic !== null}>
<Heading as="h3" size="lg" wordBreak="break-all" ref={titleRef} px={1}>
{topic?.title}
<Text
as="span"
fontWeight="normal"
color="gray.400"
ml={2}
>{`#${topic?.local_id}`}</Text>
</Heading>
</Skeleton>
{comments && comments.length > 0 ? (
<InfiniteScroll
loadMore={loadMoreComments}
hasMore={hasMore}
useWindow={false}
initialLoad={false}
getScrollParent={() => document.getElementById('scrollableDiv')}
loader={
<HStack justifyContent='center' mt='5'>
<Spinner
thickness="4px"
speed="0.65s"
emptyColor="gray.200"
color="blue.500"
size="lg"
/>
<Text>{t("DiscussionTopicPage.loading")}</Text>
</HStack>}
>
<CommentList
items={comments}
onCommentDelete={handleCommentDelete}
onCommentEdit={handleCommentEdit}
onTopicDelete={handleTopicDelete}
topic_op={topic?.user}
/>
</InfiniteScroll>
) : (
<Flex justify="space-between" alignItems="flex-start">
<SkeletonCircle size="10" />
<SkeletonText
ml={{ base: "2", md: "4" }}
noOfLines={4}
spacing="4"
skeletonHeight="4"
flex="1"
/>
</Flex>
)}
<HStack spacing={2}>
<Button
colorScheme="blue"
leftIcon={<FaReply />}
onClick={() => {
onOpen();
}}
>
{t("DiscussionTopicPage.button.reply")}
</Button>
<Button
leftIcon={<FiShare2 />}
onClick={() => {
shareContent(
topic.title,
`Discussion on ${orgCtx.basicInfo?.display_name} #${topic.local_id}`,
window.location.href,
toast, t
)
}}
>
{t("DiscussionTopicPage.button.share")}
</Button>
</HStack>
</VStack>
</GridItem>
<GridItem
colSpan={{ base: 0, md: 1 }}
display={{ base: "none", md: "block" }}
>
<Box position="sticky" top="2">
<HStack spacing={2}>
<IconButton
aria-label="Add Comment"
icon={<FaReply />}
onClick={() => {
onOpen();
}}
/>
<IconButton
aria-label="Scroll to Top"
icon={<LuArrowUpToLine />}
onClick={() => {
titleRef.current.scrollIntoView({ behavior: "smooth" });
}}
/>
</Flex>
)}
<HStack spacing={2}>
<Button
colorScheme="blue"
leftIcon={<FaReply />}
onClick={() => {
onOpen();
}}
>
{t("DiscussionTopicPage.button.reply")}
</Button>
<Button
leftIcon={<FiShare2 />}
onClick={() => {
shareContent(
topic.title,
`Discussion on ${orgCtx.basicInfo?.display_name} #${topic.local_id}`,
window.location.href,
toast, t
)}}
>
{t("DiscussionTopicPage.button.share")}
</Button>
</HStack>
</VStack>
</GridItem>
<GridItem
colSpan={{ base: 0, md: 1 }}
display={{ base: "none", md: "block" }}
>
<Box position="sticky" top="2">
<HStack spacing={2}>
<IconButton
aria-label="Add Comment"
icon={<FaReply />}
onClick={() => {
onOpen();
}}
/>
<IconButton
aria-label="Scroll to Top"
icon={<LuArrowUpToLine />}
onClick={() => {
titleRef.current.scrollIntoView({ behavior: "smooth" });
}}
/>
</HStack>
</Box>
</GridItem>
</Grid>
</HStack>
</Box>
</GridItem>
</Grid>
</Box>

<NewDiscussionDrawer
isOpen={isOpen}
Expand Down
Loading

0 comments on commit 1fadee9

Please sign in to comment.