Skip to content

Commit 9dfcb3b

Browse files
committed
feat: infinite profiles
1 parent d3ec082 commit 9dfcb3b

File tree

6 files changed

+121
-69
lines changed

6 files changed

+121
-69
lines changed

src/components/PostCard.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ export function PostCard({ post, context, className, onClick }: PostCardProps) {
4949
const { t } = useTranslation(['app', 'post']);
5050
const like = useLike();
5151
const repost = useRepost();
52-
// const { data: reply } = usePostThread({ uri: post?.record.reply?.parent.uri });
5352
const { isAuthenticated } = useAuth();
5453
const { experiments } = useSettings();
5554

@@ -61,9 +60,7 @@ export function PostCard({ post, context, className, onClick }: PostCardProps) {
6160
repost.mutate({ uri, cid });
6261
};
6362

64-
if (!post) {
65-
return <div className={cn('bg-white dark:bg-neutral-900 p-4 rounded-lg shadow', className)}>{t('post:notFound')}</div>;
66-
}
63+
if (!post) return null;
6764

6865
return (
6966
<div

src/components/PostEmbed.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,17 @@ export const PostEmbed = ({ embed }: { embed?: BSkyPostEmbed | null }) => {
164164
return (
165165
<>
166166
<div className={cn((embed.record.record.embeds ?? [])?.length >= 2 && 'grid grid-cols-2', 'gap-2 mb-3')}>
167-
<Image
168-
type="post"
169-
key={embed.media.external.uri}
170-
src={embed.media.external.uri ?? embed.media.external.thumb}
171-
alt={embed.media.external.description}
172-
classNames={{
173-
image: 'rounded-lg w-full object-cover',
174-
}}
175-
/>
167+
{embed.media.external && (
168+
<Image
169+
type="post"
170+
key={embed.media.external.uri}
171+
src={embed.media.external.uri ?? embed.media.external.thumb}
172+
alt={embed.media.external.description}
173+
classNames={{
174+
image: 'rounded-lg w-full object-cover',
175+
}}
176+
/>
177+
)}
176178
</div>
177179
</>
178180
);

src/components/ui/Image.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
55

66
type ImageProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'className'> & {
77
type: 'avatar' | 'post' | 'banner';
8-
classNames: {
8+
classNames?: {
99
wrapper?: string;
1010
image?: string;
1111
};
@@ -41,7 +41,7 @@ export const Image = ({ src, alt, type, classNames, ...props }: ImageProps) => {
4141

4242
return (
4343
<>
44-
<div className={cn('relative', classNames.wrapper)}>
44+
<div className={cn('relative', classNames?.wrapper)}>
4545
{alt &&
4646
type === 'post' &&
4747
(showAltText ? (
@@ -62,7 +62,7 @@ export const Image = ({ src, alt, type, classNames, ...props }: ImageProps) => {
6262
alt={alt}
6363
{...props}
6464
onClick={imageOnClick}
65-
className={cn(classNames.image, experiments.streamerMode && 'filter blur-md')}
65+
className={cn(classNames?.image, experiments.streamerMode && 'filter blur-md')}
6666
/>
6767
</div>
6868
{isFullscreen && (
@@ -75,7 +75,7 @@ export const Image = ({ src, alt, type, classNames, ...props }: ImageProps) => {
7575
src={src}
7676
alt={alt}
7777
{...props}
78-
className={cn(classNames.image, 'h-full w-full', experiments.streamerMode && 'filter blur-md')}
78+
className={cn(classNames?.image, 'h-full w-full', experiments.streamerMode && 'filter blur-md')}
7979
/>
8080
</div>
8181
</div>
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1-
import { useQuery } from '@tanstack/react-query';
1+
import { useInfiniteQuery } from '@tanstack/react-query';
22
import { useBlueskyStore } from '../store';
3+
import { AppBskyFeedDefs } from '@atproto/api';
4+
5+
type AuthorFeed = {
6+
cursor: string;
7+
feed: AppBskyFeedDefs.FeedViewPost[];
8+
};
39

410
export function useAuthorFeed({ handle }: { handle: string }) {
511
const { agent } = useBlueskyStore();
612

7-
return useQuery({
13+
return useInfiniteQuery<AuthorFeed>({
814
queryKey: ['author-feed', handle],
9-
queryFn: async () => {
10-
if (!agent) {
11-
throw new Error('Not authenticated');
12-
}
13-
const response = await agent.api.app.bsky.feed.getAuthorFeed({ actor: handle });
14-
return response.data.feed;
15+
queryFn: async ({ pageParam: cursor }) => {
16+
if (!agent) throw new Error('Not authenticated');
17+
18+
const response = await agent.api.app.bsky.feed.getAuthorFeed({ actor: handle, cursor: cursor as string });
19+
return response.data as AuthorFeed;
1520
},
21+
getNextPageParam: (lastPage) => lastPage.cursor,
22+
initialPageParam: undefined,
1623
enabled: !!agent,
24+
retry: 1,
1725
});
1826
}

src/lib/bluesky/types/BSkyPostEmbed.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,14 @@ export const BSkyPostEmbed = Type.Recursive((Self) => {
131131
$type: Type.Literal('app.bsky.embed.recordWithMedia#view'),
132132
media: Type.Object({
133133
$type: Type.Literal('app.bsky.embed.external#view'),
134-
external: Type.Object({
135-
uri: Type.String(),
136-
title: Type.String(),
137-
description: Type.String(),
138-
thumb: Type.String(),
139-
}),
134+
external: Type.Optional(
135+
Type.Object({
136+
uri: Type.String(),
137+
title: Type.String(),
138+
description: Type.String(),
139+
thumb: Type.String(),
140+
}),
141+
),
140142
}),
141143
record: Type.Object({
142144
record: Type.Object({

src/routes/profile/$handle/index.tsx

Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,93 +24,136 @@ export const Route = createFileRoute('/profile/$handle/')({
2424
function All() {
2525
const { t } = useTranslation('app');
2626
const { handle } = Route.useParams();
27-
const { data: feed, isLoading } = useAuthorFeed({ handle });
27+
const { data, isLoading, fetchNextPage } = useAuthorFeed({ handle });
28+
const feed = data?.pages.flatMap((page) => page.feed);
2829

2930
if (isLoading) return t('loading');
31+
if (!feed) return null;
3032

3133
return (
32-
<div className="flex flex-col gap-2">{feed?.map(({ post }) => <PostCard key={post.uri} post={post as BSkyPost} />)}</div>
34+
<Virtuoso
35+
useWindowScroll
36+
totalCount={feed.length}
37+
endReached={() => fetchNextPage()}
38+
components={{
39+
List: forwardRef((props, ref) => <div ref={ref} {...props} className="flex flex-col gap-2" />),
40+
}}
41+
itemContent={(index: number) => <PostCard key={feed[index]?.post.uri} post={feed[index]?.post as BSkyPost} />}
42+
/>
3343
);
3444
}
3545

3646
function Posts() {
3747
const { t } = useTranslation('app');
3848
const { handle } = Route.useParams();
39-
const { data: posts, isLoading } = useAuthorFeed({ handle });
49+
const { data, isLoading, fetchNextPage } = useAuthorFeed({ handle });
50+
const feed = data?.pages.flatMap((page) => page.feed);
4051

4152
if (isLoading) return t('loading');
42-
if (!posts) return null;
53+
if (!feed) return null;
4354

44-
const filteredPosts = posts
55+
const filteredPosts = feed
4556
// Filter out replies
4657
?.filter(({ post }) => !(post.record as BSkyPost['record']).reply)
4758
// Filter out reposts of other users
4859
?.filter(({ post }) => post.author.handle === handle);
4960

5061
return (
51-
<div className="w-[550px]">
52-
<Virtuoso
53-
useWindowScroll
54-
totalCount={filteredPosts.length}
55-
components={{
56-
List: forwardRef((props, ref) => <div ref={ref} {...props} className="flex flex-col gap-2" />),
57-
}}
58-
itemContent={(index: number) => (
59-
<PostCard key={filteredPosts[index]?.post.uri} post={filteredPosts[index]?.post as BSkyPost} />
60-
)}
61-
/>
62-
</div>
62+
<Virtuoso
63+
useWindowScroll
64+
totalCount={feed.length}
65+
endReached={() => fetchNextPage()}
66+
components={{
67+
List: forwardRef((props, ref) => <div ref={ref} {...props} className="flex flex-col gap-2" />),
68+
}}
69+
itemContent={(index: number) => (
70+
<PostCard key={filteredPosts[index]?.post.uri} post={filteredPosts[index]?.post as BSkyPost} />
71+
)}
72+
/>
6373
);
6474
}
6575

6676
function Reposts() {
6777
const { t } = useTranslation('app');
6878
const { handle } = Route.useParams();
69-
const { data: feed, isLoading } = useAuthorFeed({ handle });
79+
const { data, isLoading, fetchNextPage } = useAuthorFeed({ handle });
80+
const feed = data?.pages.flatMap((page) => page.feed);
7081

7182
if (isLoading) return t('loading');
83+
if (!feed) return null;
84+
85+
const filteredPosts = feed
86+
// Filter only reposts
87+
?.filter(({ post }) => post.author.handle !== handle);
7288

7389
return (
74-
<div className="flex flex-col gap-2">
75-
{feed
76-
// Filter only reposts
77-
?.filter(({ post }) => post.author.handle !== handle)
78-
?.map(({ post }) => <PostCard key={post.uri} post={post as BSkyPost} />)}
79-
</div>
90+
<Virtuoso
91+
useWindowScroll
92+
totalCount={feed.length}
93+
endReached={() => fetchNextPage()}
94+
components={{
95+
List: forwardRef((props, ref) => <div ref={ref} {...props} className="flex flex-col gap-2" />),
96+
}}
97+
itemContent={(index: number) => (
98+
<PostCard key={filteredPosts[index]?.post.uri} post={filteredPosts[index]?.post as BSkyPost} />
99+
)}
100+
/>
80101
);
81102
}
82103

83104
function Replies() {
84105
const { t } = useTranslation('app');
85106
const { handle } = Route.useParams();
86-
const { data: feed, isLoading } = useAuthorFeed({ handle });
107+
const { data, isLoading, fetchNextPage } = useAuthorFeed({ handle });
108+
const feed = data?.pages.flatMap((page) => page.feed);
87109

88110
if (isLoading) return t('loading');
111+
if (!feed) return null;
112+
113+
const filteredPosts = feed
114+
// Filter to only replies
115+
?.filter(({ post }) => (post.record as BSkyPost['record']).reply);
89116

90117
return (
91-
<div className="flex flex-col gap-2">
92-
{feed
93-
// Filter to only replies
94-
?.filter(({ post }) => (post.record as BSkyPost['record']).reply)
95-
?.map(({ post }) => <PostCard key={post.uri} post={post as BSkyPost} />)}
96-
</div>
118+
<Virtuoso
119+
useWindowScroll
120+
totalCount={feed.length}
121+
endReached={() => fetchNextPage()}
122+
components={{
123+
List: forwardRef((props, ref) => <div ref={ref} {...props} className="flex flex-col gap-2" />),
124+
}}
125+
itemContent={(index: number) => (
126+
<PostCard key={filteredPosts[index]?.post.uri} post={filteredPosts[index]?.post as BSkyPost} />
127+
)}
128+
/>
97129
);
98130
}
99131

100132
function Media() {
101133
const { t } = useTranslation('app');
102134
const { handle } = Route.useParams();
103-
const { data: feed, isLoading } = useAuthorFeed({ handle });
135+
const { data, isLoading, fetchNextPage } = useAuthorFeed({ handle });
136+
const feed = data?.pages.flatMap((page) => page.feed);
104137

105138
if (isLoading) return t('loading');
139+
if (!feed) return null;
140+
141+
const filteredPosts = feed
142+
// Filter to only media
143+
?.filter(({ post }) => (post.record as BSkyPost['record']).embed?.$type === 'app.bsky.embed.images');
106144

107145
return (
108-
<div className="flex flex-col gap-2">
109-
{feed
110-
// Filter to only media
111-
?.filter(({ post }) => (post.record as BSkyPost['record']).embed?.$type === 'app.bsky.embed.images')
112-
?.map(({ post }) => <PostCard key={post.uri} post={post as BSkyPost} />)}
113-
</div>
146+
<Virtuoso
147+
useWindowScroll
148+
totalCount={feed.length}
149+
endReached={() => fetchNextPage()}
150+
components={{
151+
List: forwardRef((props, ref) => <div ref={ref} {...props} className="flex flex-col gap-2" />),
152+
}}
153+
itemContent={(index: number) => (
154+
<PostCard key={filteredPosts[index]?.post.uri} post={filteredPosts[index]?.post as BSkyPost} />
155+
)}
156+
/>
114157
);
115158
}
116159

0 commit comments

Comments
 (0)