- {media.map((item) =>
- item.type === 'photo' ? (
-

- ) : (
+ {media.map((item) => {
+ if (item.type === 'photo' || !item.videoUrl) {
+ return (
+

+ )
+ }
+
+ return (
// biome-ignore lint/a11y/useMediaCaption: Bird does not provide caption track URLs.
- ),
- )}
+ )
+ })}
)
}
diff --git a/src/features/posts/post-service.test.ts b/src/features/posts/post-service.test.ts
index 5e386a9..49948d7 100644
--- a/src/features/posts/post-service.test.ts
+++ b/src/features/posts/post-service.test.ts
@@ -168,6 +168,64 @@ describe('loadThreadPage', () => {
})
})
+ it('does not expose a continuation when the focal post has no replies', async () => {
+ const client = reader()
+ vi.mocked(client.getTweet).mockResolvedValue({
+ success: true,
+ tweet: {
+ id: '123',
+ text: 'focal',
+ conversationId: '100',
+ replyCount: 0,
+ author: { username: 'focus', name: 'Focus' },
+ },
+ })
+
+ const result = await loadThreadPage(client, { tweetId: '123' })
+
+ expect(result).toMatchObject({
+ ok: true,
+ page: { nextCursor: undefined },
+ })
+ })
+
+ it.each([
+ {
+ name: 'an empty final page',
+ tweets: [],
+ returnedCursor: 'thread-final',
+ },
+ {
+ name: 'the same cursor again',
+ tweets: [
+ {
+ id: '101',
+ text: 'last reply',
+ author: { username: 'reply', name: 'Reply' },
+ },
+ ],
+ returnedCursor: 'thread-next',
+ },
+ ])('stops after $name', async ({ tweets, returnedCursor }) => {
+ const client = reader()
+ vi.mocked(client.getThreadPaged).mockResolvedValue({
+ success: true,
+ tweets,
+ nextCursor: returnedCursor,
+ })
+
+ const result = await loadThreadPage(client, {
+ tweetId: '123',
+ conversationId: '100',
+ cursor: 'thread-next',
+ })
+
+ expect(result).toMatchObject({
+ ok: true,
+ page: { nextCursor: undefined },
+ })
+ })
+
it('removes raw data from focal, conversation, and quotes', async () => {
const client = reader()
vi.mocked(client.getTweet).mockResolvedValue({
diff --git a/src/features/posts/post-service.ts b/src/features/posts/post-service.ts
index af36f11..9a0205f 100644
--- a/src/features/posts/post-service.ts
+++ b/src/features/posts/post-service.ts
@@ -215,14 +215,22 @@ export async function loadThreadPage(
if (!result.success) {
return upstreamFailure(result.error, 'post')
}
+ const tweets = result.tweets ?? []
+ const nextCursor =
+ result.nextCursor &&
+ tweets.length > 0 &&
+ result.nextCursor !== input.cursor &&
+ focalPost?.replyCount !== 0
+ ? result.nextCursor
+ : undefined
return {
ok: true,
page: {
- tweets: (result.tweets ?? []).map(publicPost),
+ tweets: tweets.map(publicPost),
focalPost,
conversationId,
- nextCursor: result.nextCursor,
+ nextCursor,
},
}
} catch (error) {
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 1593d6b..f11c82c 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -16,6 +16,7 @@ export const Route = createRootRouteWithContext