Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 62 additions & 47 deletions packages/shared/src/components/post/PostComments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ interface PostCommentsProps {
canReply?: MainCommentProps['canReply'];
onReplyBlocked?: MainCommentProps['onReplyBlocked'];
/**
* Renders between top-level comments, every `interleaveEvery` of them. Used
* by the ad template to break a long thread up; never after the last comment,
* where whatever follows the thread already sits. Both props are required
* together, and without them the list keeps its original markup.
* Renders after the top-level comment at which the running total of
* comments — replies included, every comment counts — crosses a multiple
* of `interleaveEvery`. Used by the ad template to break a long thread up;
* never after the last top-level comment, where whatever follows the
* thread already sits. Both props are required together, and without them
* the list keeps its original markup.
*/
interleaveEvery?: number;
renderInterleaved?: (occurrence: number) => ReactNode;
Expand Down Expand Up @@ -134,50 +136,63 @@ export function PostComments({
}
ref={container}
>
{comments!.postComments.edges.map((e, index) => {
const isLast = index === comments!.postComments.edges.length - 1;
// Never after the last comment, where whatever follows the thread
// already sits.
const shouldInterleave =
!!interleaveEvery &&
!!renderInterleaved &&
!isLast &&
(index + 1) % interleaveEvery === 0;
{(() => {
// Replies count too: the interval is over everything the reader
// scrolls past, not just top-level rows (loaded replies — collapsed
// pagination beyond the first page is not on screen and not counted),
// and the boundary can only sit after a top-level block. One prefix
// pass instead of two reductions per row.
const totals: number[] = [0];
comments!.postComments.edges.forEach((edge, i) => {
totals.push(totals[i] + 1 + (edge.node.children?.edges?.length ?? 0));
});
return comments!.postComments.edges.map((e, index, edges) => {
const isLast = index === edges.length - 1;
const seen = totals[index + 1];
const shouldInterleave =
!!interleaveEvery &&
!!renderInterleaved &&
!isLast &&
Math.floor(seen / interleaveEvery) >
Math.floor(totals[index] / interleaveEvery);

// Always the Fragment, even rows that interleave nothing: the type at
// a given key must not flip as the boundary moves (a new comment
// landing shifts every index), or React remounts that comment's
// subtree and open reply boxes lose their state.
return (
<Fragment key={e.node.id}>
<MainComment
isModalThread={isModalThread}
className={{ commentBox: className }}
post={post}
origin={origin}
commentHash={commentHash ?? undefined}
commentRef={commentRef as React.MutableRefObject<HTMLElement>}
comment={e.node}
onShare={onShare ?? noopShare}
onDelete={(comment, parentId) =>
deleteComment(comment.id, parentId ?? null, post)
}
onShowUpvotes={onClickUpvote ?? noopShowUpvotes}
postAuthorId={post.author?.id ?? null}
postScoutId={post.scout?.id ?? null}
appendTooltipTo={getAppendTooltipParent}
permissionNotificationCommentId={permissionNotificationCommentId}
joinNotificationCommentId={joinNotificationCommentId}
onCommented={onCommented}
lazy={!commentHash && index >= lazyCommentThreshold}
canReply={canReply}
onReplyBlocked={onReplyBlocked}
/>
{shouldInterleave &&
renderInterleaved((index + 1) / interleaveEvery)}
</Fragment>
);
})}
// Always the Fragment, even rows that interleave nothing: the type at
// a given key must not flip as the boundary moves (a new comment
// landing shifts every index), or React remounts that comment's
// subtree and open reply boxes lose their state.
return (
<Fragment key={e.node.id}>
<MainComment
isModalThread={isModalThread}
className={{ commentBox: className }}
post={post}
origin={origin}
commentHash={commentHash ?? undefined}
commentRef={commentRef as React.MutableRefObject<HTMLElement>}
comment={e.node}
onShare={onShare ?? noopShare}
onDelete={(comment, parentId) =>
deleteComment(comment.id, parentId ?? null, post)
}
onShowUpvotes={onClickUpvote ?? noopShowUpvotes}
postAuthorId={post.author?.id ?? null}
postScoutId={post.scout?.id ?? null}
appendTooltipTo={getAppendTooltipParent}
permissionNotificationCommentId={
permissionNotificationCommentId
}
joinNotificationCommentId={joinNotificationCommentId}
onCommented={onCommented}
lazy={!commentHash && index >= lazyCommentThreshold}
canReply={canReply}
onReplyBlocked={onReplyBlocked}
/>
{shouldInterleave &&
renderInterleaved(Math.floor(seen / interleaveEvery))}
</Fragment>
);
});
})()}
</div>
);
}
8 changes: 7 additions & 1 deletion packages/shared/src/components/post/PostEngagements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ interface PostEngagementsProps {
onCopyLinkClick?: (post?: Post) => void;
/** Ad templates break a long thread up — see PostComments. */
interleaveEvery?: number;
/**
* Drops the internal AdAsComment. The programmatic template carries its own
* comment-thread units, and two ad systems in one thread double the density.
*/
hideInternalAd?: boolean;
renderInterleaved?: (occurrence: number) => ReactNode;
}

Expand All @@ -60,6 +65,7 @@ function PostEngagements({
onCopyLinkClick,
logOrigin,
shouldOnboardAuthor,
hideInternalAd,
interleaveEvery,
renderInterleaved,
}: PostEngagementsProps): ReactElement {
Expand Down Expand Up @@ -172,7 +178,7 @@ function PostEngagements({
shouldHandleCommentQuery
CommentInputOrModal={CommentInputOrModal}
/>
{!isPlus && <AdAsComment postId={post.id} />}
{!isPlus && !hideInternalAd && <AdAsComment postId={post.id} />}
<PostComments
post={post}
sortBy={sortBy}
Expand Down
13 changes: 9 additions & 4 deletions packages/shared/src/components/post/PostWidgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export type PostWidgetsProps = Omit<PostHeaderActionsProps, 'contextMenuId'> &
getRailAd?: (position: PostWidgetPosition) => ReactNode;
/** Rendered last, below the footer links. */
trailing?: ReactNode;
/** Drops the internal sidebar ad — for templates carrying their own. */
hideAdWidget?: boolean;
};

/**
Expand Down Expand Up @@ -100,6 +102,7 @@ export function PostWidgets({
hideToc = false,
getRailAd,
trailing,
hideAdWidget,
}: PostWidgetsProps): ReactElement {
const { tokenRefreshed } = useContext(AuthContext);
const { source } = post;
Expand Down Expand Up @@ -158,10 +161,12 @@ export function PostWidgets({
/>
),
)}
<PostSidebarAdWidget
postId={post.id}
className={{ container: cardClasses }}
/>
{!hideAdWidget && (
<PostSidebarAdWidget
postId={post.id}
className={{ container: cardClasses }}
/>
)}
<MentionedToolsWidget postTags={post.tags || []} />
{withAd(
PostWidgetPosition.Share,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ export interface ArbitrageAdSlotProps {
/** Marks slots wired to a declared 30-60s in-view refresh once on Ad Manager. */
refreshes?: boolean;
/**
* Drops the slot below the tablet breakpoint. The Better Ads Standards cap
* mobile ad density at 30% of page height, and a scraped post carries little
* body text to dilute it — running every slot on a phone measured 56%, which
* is what gets a site's ads filtered by Chrome. The unit is hidden rather
* than skipped so it also never requests: the ad only pushes on intersection,
* Drops the slot below the tablet breakpoint — the Better Ads Standards cap
* mobile ad density at 30% of page height, and Chrome's filter for a
* violation applies to the whole domain. The unit is hidden rather than
* skipped so it also never requests: the ad only pushes on intersection,
* and a display:none box never intersects.
*/
hideOnPhone?: boolean;
Expand All @@ -49,6 +48,8 @@ export interface ArbitrageAdSlotProps {
* arrived, so eager pushes ride its very first processing pass.
*/
eager?: boolean;
/** Per-instance extra for repeated placements — see ProgrammaticAd. */
logExtra?: Record<string, unknown>;
}

function MappedAdSlot({
Expand All @@ -58,6 +59,7 @@ function MappedAdSlot({
refreshes,
hideOnPhone,
eager,
logExtra,
slots,
surface,
allowPlaceholder = false,
Expand Down Expand Up @@ -88,6 +90,7 @@ function MappedAdSlot({
refreshes={refreshes}
hideOnPhone={hideOnPhone}
eager={eager}
logExtra={logExtra}
/>
);
}
Expand Down
Loading
Loading