Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿

Refactor GroupFeed, Highlights, Timeline, and Posts components to remove unnecessary type assertions, improve null handling, and utilize the reusable PostFeed component for consistent rendering and state management

yoginth.com e95f827e fa7926af

verified
+29 -68
+4 -10
apps/web/src/components/Group/GroupFeed.tsx
··· 1 1 import SinglePost from "@/components/Post/SinglePost"; 2 2 import PostFeed from "@/components/Shared/Post/PostFeed"; 3 3 import { ChatBubbleBottomCenterIcon } from "@heroicons/react/24/outline"; 4 - import { 5 - PageSize, 6 - type PostFragment, 7 - type PostsRequest, 8 - usePostsQuery 9 - } from "@hey/indexer"; 4 + import { PageSize, type PostsRequest, usePostsQuery } from "@hey/indexer"; 10 5 11 6 interface GroupFeedProps { 12 7 feed: string; ··· 23 18 variables: { request } 24 19 }); 25 20 26 - const posts = data?.posts?.items as PostFragment[]; 21 + const posts = data?.posts?.items; 27 22 const pageInfo = data?.posts?.pageInfo; 28 23 const hasMore = pageInfo?.next; 29 24 ··· 35 30 } 36 31 }; 37 32 38 - const filteredPosts = posts.filter( 33 + const filteredPosts = (posts ?? []).filter( 39 34 (post) => 40 35 !post.author.operations?.hasBlockedMe && 41 - !post.author.operations?.isBlockedByMe && 42 - !post.operations?.hasReported 36 + !post.author.operations?.isBlockedByMe 43 37 ); 44 38 45 39 return (
+2 -3
apps/web/src/components/Home/Highlights.tsx
··· 3 3 import { LightBulbIcon } from "@heroicons/react/24/outline"; 4 4 import { 5 5 PageSize, 6 - type PostFragment, 7 6 type TimelineHighlightsRequest, 8 7 useTimelineHighlightsQuery 9 8 } from "@hey/indexer"; ··· 21 20 variables: { request } 22 21 }); 23 22 24 - const posts = data?.timelineHighlights.items as PostFragment[]; 23 + const posts = data?.timelineHighlights.items; 25 24 const pageInfo = data?.timelineHighlights.pageInfo; 26 25 const hasMore = pageInfo?.next; 27 26 ··· 33 32 } 34 33 }; 35 34 36 - const filteredPosts = posts.filter( 35 + const filteredPosts = (posts ?? []).filter( 37 36 (post) => 38 37 !post.author.operations?.hasBlockedMe && 39 38 !post.author.operations?.isBlockedByMe &&
+20 -47
apps/web/src/components/Home/Timeline/index.tsx
··· 1 1 import SinglePost from "@/components/Post/SinglePost"; 2 - import PostsShimmer from "@/components/Shared/Shimmer/PostsShimmer"; 3 - import { Card, EmptyState, ErrorMessage } from "@/components/Shared/UI"; 2 + import PostFeed from "@/components/Shared/Post/PostFeed"; 4 3 import { useAccountStore } from "@/store/persisted/useAccountStore"; 5 4 import { UserGroupIcon } from "@heroicons/react/24/outline"; 6 5 import { ··· 8 7 type TimelineRequest, 9 8 useTimelineQuery 10 9 } from "@hey/indexer"; 11 - import { useIntersectionObserver } from "@uidotdev/usehooks"; 12 - import { memo, useEffect } from "react"; 13 - import { WindowVirtualizer } from "virtua"; 10 + import { memo } from "react"; 14 11 15 12 const Timeline = () => { 16 13 const { currentAccount } = useAccountStore(); 17 - const [ref, entry] = useIntersectionObserver({ 18 - threshold: 0, 19 - root: null, 20 - rootMargin: "0px" 21 - }); 22 - 23 14 const request: TimelineRequest = { 24 15 account: currentAccount?.address, 25 16 filter: { ··· 47 38 } 48 39 }; 49 40 50 - useEffect(() => { 51 - if (entry?.isIntersecting) { 52 - onEndReached(); 53 - } 54 - }, [entry?.isIntersecting]); 55 - 56 - if (loading) { 57 - return <PostsShimmer />; 58 - } 59 - 60 - if (!feed?.length) { 61 - return ( 62 - <EmptyState 63 - icon={<UserGroupIcon className="size-8" />} 64 - message="No posts yet!" 65 - /> 66 - ); 67 - } 68 - 69 - if (error) { 70 - return <ErrorMessage error={error} title="Failed to load timeline" />; 71 - } 72 - 73 - const filteredPosts = feed.filter( 41 + const filteredPosts = (feed ?? []).filter( 74 42 (timelineItem) => 75 43 !timelineItem.primary.author.operations?.hasBlockedMe && 76 44 !timelineItem.primary.author.operations?.isBlockedByMe && ··· 78 46 ); 79 47 80 48 return ( 81 - <Card className="virtual-divider-list-window"> 82 - <WindowVirtualizer> 83 - {filteredPosts.map((timelineItem) => ( 84 - <SinglePost 85 - key={timelineItem.id} 86 - timelineItem={timelineItem} 87 - post={timelineItem.primary} 88 - /> 89 - ))} 90 - {hasMore && <span ref={ref} />} 91 - </WindowVirtualizer> 92 - </Card> 49 + <PostFeed 50 + items={filteredPosts} 51 + loading={loading} 52 + error={error} 53 + hasMore={hasMore} 54 + onEndReached={onEndReached} 55 + emptyIcon={<UserGroupIcon className="size-8" />} 56 + emptyMessage="No posts yet!" 57 + errorTitle="Failed to load timeline" 58 + renderItem={(timelineItem) => ( 59 + <SinglePost 60 + key={timelineItem.id} 61 + timelineItem={timelineItem} 62 + post={timelineItem.primary} 63 + /> 64 + )} 65 + /> 93 66 ); 94 67 }; 95 68
+3 -8
apps/web/src/components/Search/Posts.tsx
··· 1 1 import SinglePost from "@/components/Post/SinglePost"; 2 2 import { ChatBubbleBottomCenterIcon } from "@heroicons/react/24/outline"; 3 - import { 4 - PageSize, 5 - type PostFragment, 6 - type PostsRequest, 7 - usePostsQuery 8 - } from "@hey/indexer"; 3 + import { PageSize, type PostsRequest, usePostsQuery } from "@hey/indexer"; 9 4 import PostFeed from "../Shared/Post/PostFeed"; 10 5 11 6 interface PostsProps { ··· 22 17 variables: { request } 23 18 }); 24 19 25 - const posts = data?.posts?.items as PostFragment[]; 20 + const posts = data?.posts?.items; 26 21 const pageInfo = data?.posts?.pageInfo; 27 22 const hasMore = pageInfo?.next; 28 23 ··· 36 31 37 32 return ( 38 33 <PostFeed 39 - items={posts} 34 + items={posts ?? []} 40 35 loading={loading} 41 36 error={error} 42 37 hasMore={hasMore}