diff --git a/__tests__/unit/components/repost-renders-as-a-post.test.tsx b/__tests__/unit/components/repost-renders-as-a-post.test.tsx new file mode 100644 index 000000000..bcf4e19b5 --- /dev/null +++ b/__tests__/unit/components/repost-renders-as-a-post.test.tsx @@ -0,0 +1,92 @@ +/** + * A repost is shown as the post it is; a quote repost shows two posts. + * + * A simple repost used to suppress its own content and render the original + * inside a bordered panel instead — a panel repeating the original author's + * avatar and handle, which the post header directly above was ALREADY showing + * (PostCard swaps the reposter for the original author on a simple repost). + * One repost drew the same person twice, two lines apart, with the actual text + * boxed off underneath. + * + * The distinction these tests hold: a SIMPLE repost has one author and one + * body, so it renders flat. A QUOTE repost genuinely has two of each, so it + * keeps the nested panel. Collapsing both cases the same way would be the + * opposite bug. + */ + +import { render, screen } from '@testing-library/react'; +import { PostContent } from '@/components/timeline/PostContent'; +import type { TimelineDisplayEvent } from '@/types/timeline'; + +jest.mock('@/utils/markdown', () => ({ + renderMarkdownToReact: (text: string) => text, +})); + +const base = { + id: 'e1', + actor: { id: 'a1', name: 'Reposter', username: 'reposter', type: 'user' }, + description: '', + metadata: {}, +} as unknown as TimelineDisplayEvent; + +function simpleRepost(): TimelineDisplayEvent { + return { + ...base, + description: '', + metadata: { + is_repost: true, + original_event_id: 'orig-1', + original_actor_name: 'Original Author', + original_actor_username: 'original', + original_description: 'The original words.', + }, + } as unknown as TimelineDisplayEvent; +} + +function quoteRepost(): TimelineDisplayEvent { + return { + ...base, + description: 'My take on this.', + metadata: { + is_repost: true, + is_quote_repost: true, + original_event_id: 'orig-1', + original_actor_name: 'Original Author', + original_actor_username: 'original', + original_description: 'The original words.', + }, + } as unknown as TimelineDisplayEvent; +} + +describe('a simple repost', () => { + it('shows the original text as the post body', () => { + render(); + + expect(screen.getByText('The original words.')).toBeInTheDocument(); + }); + + it('does not repeat the original author, who is already in the header', () => { + render(); + + // PostCard renders the original author in the post header for a simple + // repost. PostContent must not draw them a second time. + expect(screen.queryByText('Original Author')).not.toBeInTheDocument(); + expect(screen.queryByText('@original')).not.toBeInTheDocument(); + }); +}); + +describe('a quote repost', () => { + it('shows the quoter’s own words as the post body', () => { + render(); + + expect(screen.getByText('My take on this.')).toBeInTheDocument(); + }); + + it('keeps the quoted original in its own panel, author and all', () => { + render(); + + // Two posts, two authors: here the panel earns its place. + expect(screen.getByText('Original Author')).toBeInTheDocument(); + expect(screen.getByText('The original words.')).toBeInTheDocument(); + }); +}); diff --git a/src/components/timeline/PostContent.tsx b/src/components/timeline/PostContent.tsx index 150b6173b..83fc8789b 100644 --- a/src/components/timeline/PostContent.tsx +++ b/src/components/timeline/PostContent.tsx @@ -107,8 +107,23 @@ export function PostContent({ event }: PostContentProps) { )} - {/* Event Description/Content */} - {!articleSlug && displayContent && (!isRepost || isQuoteRepost) && ( + {/* + A reposted post is shown as the post it is. + + A simple repost used to suppress its own content here and render the + original inside a bordered panel below instead — a panel that repeated + the original author's avatar and handle, which the post header directly + above was ALREADY showing (PostCard swaps the reposter for the original + author on a simple repost). So one repost drew the same person twice, + two lines apart, with the actual text boxed off underneath. That is + what "reposts look ugly" was. + + `getDisplayContent` already returns the original's text for a simple + repost, so it renders here like any other post. The nested panel is + kept for QUOTE reposts, where there genuinely are two posts and two + authors to tell apart. + */} + {!articleSlug && displayContent && (
{renderMarkdownToReact(displayContent)}
@@ -206,40 +221,6 @@ export function PostContent({ event }: PostContentProps) { )} {/* Simple Repost: show original post inside a quoted card for consistency */} - {isRepost && !isQuoteRepost && event.metadata?.original_event_id && ( -
-
-
- - {/* eslint-disable-next-line @next/next/no-img-element -- avatar_url is a free-form user URL (any host); next/image would throw for hosts outside images.remotePatterns */} - {originalAuthor.name} - -
-
- - {originalAuthor.name} - - {originalAuthor.username && ( - @{originalAuthor.username} - )} -
-
-
- {originalDescription && ( -
- {renderMarkdownToReact(originalDescription)} -
- )} -
-
- )} {/* Attached image — plain : Openverse hosts aren't in next/image remotePatterns */} {postImage && !isRepost && ( diff --git a/src/components/timeline/TimelineComponent.tsx b/src/components/timeline/TimelineComponent.tsx index 12c872c22..8a90501a0 100644 --- a/src/components/timeline/TimelineComponent.tsx +++ b/src/components/timeline/TimelineComponent.tsx @@ -11,6 +11,7 @@ import { usePostSelection } from '@/hooks/usePostSelection'; import EmptyState from '@/components/ui/EmptyState'; import { BulkActionsToolbar } from './BulkActionsToolbar'; import { BulkDeleteConfirmDialog } from './BulkDeleteConfirmDialog'; +import { TIMELINE_SURFACE } from '@/config/timeline'; interface TimelineComponentProps { feed: TimelineFeedResponse; @@ -186,16 +187,31 @@ export const TimelineComponent: React.FC = ({ {enableMultiSelect && ( <> {!isSelectionMode ? ( - // Entry point to selection mode - small button -
+ /* + The way IN to selection mode is not itself worth a banner. + + This used to be a full-width bar with its own border, background + and `sticky top-16` — so a control for bulk-deleting old posts + followed you down the entire feed, above every post you came to + read. It was the fourth separate bordered band before the first + post. + + Managing posts is a rare, deliberate task; reading them is the + reason the page exists. So the entry point is a quiet inline + control, and everything it opens — the full toolbar with counts, + select-all and the destructive actions — is unchanged, because + once you ARE selecting, that toolbar is the thing you need and + it earns being sticky. + */ +
) : (