Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
35d7811
feat(daily): always-expanded feed card glass actions
tsahimatsliah Jun 21, 2026
456059b
fix(daily): full-bleed ad cover image matching glass cards
tsahimatsliah Jun 21, 2026
f570310
feat: add Google Cloud advertiser-takeover demo
tsahimatsliah Jun 22, 2026
c93b93b
feat: inject Google Cloud takeover into the main feed
tsahimatsliah Jun 22, 2026
2d8fe1b
fix: exclude Google Cloud takeover from the test env
tsahimatsliah Jun 22, 2026
17032f1
feat: refine Google Cloud takeover (real cards, compact bar, aligned …
tsahimatsliah Jun 22, 2026
adb8792
fix: small improvements around glass actions
rebelchris Jun 22, 2026
1f05be8
Merge branch 'claude/intelligent-elgamal-534329' of https://github.co…
tsahimatsliah Jun 22, 2026
292dd4d
feat: official GCP logo, ad in 3rd slot, bar outside feed box, #6225 …
tsahimatsliah Jun 22, 2026
130019a
fix: land GCP strip on a real row boundary in the live feed
tsahimatsliah Jun 22, 2026
8291668
feat: GCP ad card — distinct cover, hover CTA, left-aligned attribution
tsahimatsliah Jun 22, 2026
ea8f080
fix: glass action bar buttons overlapping/overflowing on narrow cards
tsahimatsliah Jun 22, 2026
086d82e
chore: remove em dashes from Google Cloud takeover copy
tsahimatsliah Jun 22, 2026
6136b59
fix: order ad attribution as Promoted then Advertise here
tsahimatsliah Jun 22, 2026
c678bc7
chore: shorten Google Cloud ad title to avoid truncation
tsahimatsliah Jun 22, 2026
f615f92
feat: center announcement-bar CTA and ad hover CTA
tsahimatsliah Jun 22, 2026
f895d90
feat: open Google Cloud blog post as a regular post modal
tsahimatsliah Jun 22, 2026
3b5aae3
fix: place ad CTA at top-right on the source line with equal insets
tsahimatsliah Jun 22, 2026
4087cad
feat: uniform feed row heights on the takeover home feed
tsahimatsliah Jun 22, 2026
bb44007
Revert "feat: uniform feed row heights on the takeover home feed"
tsahimatsliah Jun 22, 2026
d415e6f
fix: remove empty feed cells caused by the takeover injection
tsahimatsliah Jun 22, 2026
7b6dca3
feat: takeover engagement polish (no organic ads, fake discussion, ad…
tsahimatsliah Jun 22, 2026
f388d09
fix: blog "Read post" opens the real Google Cloud blog URL
tsahimatsliah Jun 22, 2026
ac4b5bd
Merge remote-tracking branch 'origin/main' into claude/hungry-jackson…
tsahimatsliah Jun 22, 2026
61bb9ac
fix: keep Google Cloud takeover bar visible while post modal is open
tsahimatsliah Jun 22, 2026
e5790a6
feat: add Google Cloud engagement-ad card + richer demo comments
tsahimatsliah Jun 22, 2026
3c529d1
fix: align Google Cloud ad 'Promoted' label flush-left with title
tsahimatsliah Jun 22, 2026
d43e688
fix: drop branded upvote animation from feed-card glass bar
tsahimatsliah Jun 22, 2026
6102c2e
feat: demo polish — in-app reader on blog card, ad tags, richer discu…
tsahimatsliah Jun 22, 2026
e6d3fea
chore: update Google Cloud ad card title
tsahimatsliah Jun 22, 2026
b9abf3b
feat: restore branded upvote animation on cards; new Google Cloud ad …
tsahimatsliah Jun 22, 2026
f2ab63e
feat: real ad cover image + takeover on tag feed page
tsahimatsliah Jun 22, 2026
ce2baa6
fix: blog card Read post opens new tab (Google blocks in-app reader e…
tsahimatsliah Jun 22, 2026
1c9ffc4
revert: use the original Google Cloud I/O header as the ad cover image
tsahimatsliah Jun 22, 2026
5e60c70
Merge branch 'main' into claude/hungry-jackson-735044
tsahimatsliah Jun 22, 2026
875bb67
fix: kill the action-pill spinner during the takeover demo
tsahimatsliah Jun 22, 2026
c2a1e09
feat: show takeover on the explore-tag feed too
tsahimatsliah Jun 22, 2026
47f1b1b
feat: surface Google Cloud placement at the top of the tag page
tsahimatsliah Jun 22, 2026
3a5d954
chore: shorten Google Cloud ad card title
tsahimatsliah Jun 22, 2026
1e41d67
feat: give the engagement card its own distinct discussion
tsahimatsliah Jun 22, 2026
dc7c3e6
feat: brand upvote animation in post modal; remove ad card CTA
tsahimatsliah Jun 22, 2026
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
97 changes: 87 additions & 10 deletions packages/shared/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ import ReadingReminderFeedHero from './marketing/banners/ReadingReminderFeedHero
import { useLayoutVariant } from '../hooks/layout/useLayoutVariant';
import { useReaderModalEligibility } from './post/reader/hooks/useReaderModalEligibility';
import { useQuestDashboard } from '../hooks/useQuestDashboard';
import { GoogleCloudBlogCard } from '../features/googleCloudTakeover/GoogleCloudBlogCard';
import { GoogleCloudEngagementCard } from '../features/googleCloudTakeover/GoogleCloudEngagementCard';
import { GoogleCloudHeadAd } from '../features/googleCloudTakeover/GoogleCloudHeadAd';
import { GoogleCloudStrip } from '../features/googleCloudTakeover/GoogleCloudStrip';
import {
googleCloudPrependedCards,
googleCloudStripRow,
googleCloudTakeoverEnabled,
} from '../features/googleCloudTakeover/config';
import { isTesting } from '../lib/constants';

const FeedErrorScreen = dynamic(
() => import(/* webpackChunkName: "feedErrorScreen" */ './FeedErrorScreen'),
Expand Down Expand Up @@ -203,6 +213,21 @@ export default function Feed<T>({
const isSquadFeed = feedName === OtherFeedPage.Squad;
const trackedFeedFinish = useRef(false);
const isMyFeed = feedName === SharedFeedPage.MyFeed;
// DEMO: render the Google Cloud takeover on the main home feeds only.
// Excluded from the test env so it doesn't distort existing feed specs;
// still live in dev and on the production preview deploy.
const showGoogleCloudTakeover =
googleCloudTakeoverEnabled &&
!isTesting &&
(feedName === SharedFeedPage.MyFeed ||
feedName === SharedFeedPage.Popular ||
// The advertiser takeover follows the user onto a tag feed: both the
// standard tag page (/tags/ai → OtherFeedPage.Tag) and the explore-tag
// feed reached via the feed tab bar (/explore/ai → ExploreTag) carry the
// Google Cloud engagement placements.
feedName === OtherFeedPage.Tag ||
feedName === OtherFeedPage.ExploreTag) &&
!isHorizontal;
const showAcquisitionForm =
isMyFeed &&
(routerQuery?.[acquisitionKey] as string)?.toLocaleLowerCase() === 'true' &&
Expand Down Expand Up @@ -292,7 +317,10 @@ export default function Feed<T>({
firstSlotOffset: Number(showFirstSlotCard),
disableTopHero: isV2,
settings: {
disableAds,
// DEMO: suppress the feed's organic ads during the takeover so the
// only ad is the injected Google Cloud slot (avoids extra ads that
// non-Plus users would otherwise see).
disableAds: disableAds || showGoogleCloudTakeover,
staticAd,
adPostLength: isSquadFeed ? 2 : undefined,
showAcquisitionForm,
Expand All @@ -309,6 +337,29 @@ export default function Feed<T>({
const { onMenuClick, postMenuIndex, postMenuLocation } = useFeedContextMenu();
const useList = isListMode && numCards > 1;
const virtualizedNumCards = useList ? 1 : numCards;
// Find the item index before which the strip should render so it starts on a
// whole grid row (no empty cells above it). The takeover forces every feed
// item to a single cell, so the cells rendered before item `i` are just the
// fixed injected cards (the prepended blog + engagement cards, plus the head
// ad inserted before item 0) plus `i`. Stop at the first column-0 boundary
// at/after the target row.
const googleCloudStripBeforeIndex = useMemo(() => {
if (!showGoogleCloudTakeover || virtualizedNumCards < 1) {
return -1;
}
const targetCells = googleCloudStripRow * virtualizedNumCards;
const injectedBeforeItems = googleCloudPrependedCards + 1; // + head ad
for (let i = 0; i < items.length; i += 1) {
const cellsBefore = injectedBeforeItems + i;
if (
cellsBefore >= targetCells &&
cellsBefore % virtualizedNumCards === 0
) {
return i;
}
}
return -1;
}, [showGoogleCloudTakeover, virtualizedNumCards, items]);

// Experiment: let the browser skip layout/paint for off-screen cards on long
// vertical feeds. Horizontal carousels are short and scroll on the other axis,
Expand Down Expand Up @@ -689,9 +740,20 @@ export default function Feed<T>({
}}
/>
)}
{showGoogleCloudTakeover && (
<>
<GoogleCloudBlogCard isList={shouldUseListFeedLayout} />
<GoogleCloudEngagementCard isList={shouldUseListFeedLayout} />
</>
)}
{items.map((item, index) => {
const placement = itemPlacements[index];
const { colSpan } = placement;
// DEMO: the takeover injects extra cells (blog card, ad), which
// shifts the grid and desyncs the feed's wide-card placements,
// leaving empty cells. Force single-column cards so every cell
// packs cleanly; the only full-row item is the GCP strip, which
// is positioned on a row boundary.
const colSpan = showGoogleCloudTakeover ? 1 : placement.colSpan;
const isWidened = colSpan > 1;
const wideColSpan =
isWidened && (colSpan === 2 || colSpan === 3 || colSpan === 4)
Expand Down Expand Up @@ -764,15 +826,30 @@ export default function Feed<T>({
: undefined,
}}
>
{showPromoBanner && index === indexWhenShowingPromoBanner && (
<BriefBannerFeed
style={{
gridColumn: !shouldUseListFeedLayout
? `span ${virtualizedNumCards}`
: undefined,
}}
/>
{showPromoBanner &&
!showGoogleCloudTakeover &&
index === indexWhenShowingPromoBanner && (
<BriefBannerFeed
style={{
gridColumn: !shouldUseListFeedLayout
? `span ${virtualizedNumCards}`
: undefined,
}}
/>
)}
{showGoogleCloudTakeover && index === 0 && (
<GoogleCloudHeadAd isList={shouldUseListFeedLayout} />
)}
{showGoogleCloudTakeover &&
index === googleCloudStripBeforeIndex && (
<GoogleCloudStrip
style={{
gridColumn: !shouldUseListFeedLayout
? `span ${virtualizedNumCards}`
: undefined,
}}
/>
)}
{renderedItem}
</FeedCardContext.Provider>
);
Expand Down
21 changes: 19 additions & 2 deletions packages/shared/src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { PromptElement } from './modals/Prompt';
import { useNotificationParams } from '../hooks/useNotificationParams';
import { useAuthContext } from '../contexts/AuthContext';
import { SharedFeedPage } from './utilities';
import { isTesting, onboardingUrl } from '../lib/constants';
import { isTesting, onboardingUrl, webappUrl } from '../lib/constants';
import { useBanner } from '../hooks/useBanner';
import { useGrowthBookContext } from './GrowthBookProvider';
import {
Expand All @@ -32,6 +32,8 @@ import { useFeedName } from '../hooks/feed/useFeedName';
import { AuthTriggers } from '../lib/auth';
import PlusMobileEntryBanner from './marketing/banners/PlusMobileEntryBanner';
import usePlusEntry from '../hooks/usePlusEntry';
import { GoogleCloudAnnouncementBar } from '../features/googleCloudTakeover/GoogleCloudAnnouncementBar';
import { googleCloudTakeoverEnabled } from '../features/googleCloudTakeover/config';
import { SearchProvider } from '../contexts/search/SearchContext';
import { SpotlightProvider } from './spotlight/SpotlightContext';
import { SpotlightHost } from './spotlight/SpotlightHost';
Expand Down Expand Up @@ -123,6 +125,13 @@ function MainLayoutComponent({
feedName: currentFeedName,
});
const { plusEntryAnnouncementBar } = usePlusEntry();
// DEMO: Google Cloud takeover bar, rendered at the app layout level so it
// sits above/outside the feed's floating-card box on the home feed. Gate on
// `router.pathname` (the underlying page route) rather than `asPath`. When a
// post modal opens it only changes `asPath`/query to `/posts/...` while the
// home route stays `/`, so the bar stays put and doesn't cause a layout shift.
const showGoogleCloudBar =
googleCloudTakeoverEnabled && !isTesting && router?.pathname === webappUrl;
const isLaptop = useViewSize(ViewSize.Laptop);
const isLaptopXL = useViewSize(ViewSize.LaptopXL);
const { screenCenteredOnMobileLayout } = useFeedLayout();
Expand Down Expand Up @@ -349,6 +358,9 @@ function MainLayoutComponent({
)}
{sidebarOwnsHeader ? (
<div className="flex min-h-0 flex-1 flex-col laptop:my-3 laptop:ml-1 laptop:mr-3">
{showGoogleCloudBar && (
<GoogleCloudAnnouncementBar className="mx-4 mb-3 laptop:mx-0" />
)}
{showHomepageTopBanners && (
<HomepageTopBanners className="mx-4 mb-3 laptop:mx-0" />
)}
Expand All @@ -373,7 +385,12 @@ function MainLayoutComponent({
</div>
</div>
) : (
children
<>
{showGoogleCloudBar && (
<GoogleCloudAnnouncementBar className="mx-4 mb-3 laptop:mx-0" />
)}
{children}
</>
)}
</main>
{!hideFeedbackWidget && !sidebarOwnsHeader && <FeedbackWidget />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
import React from 'react';
import React, { useMemo } from 'react';
import classNames from 'classnames';
import type { ActionButtonsProps } from './ActionButtons';
import { UpvoteButtonIcon } from './UpvoteButtonIcon';
Expand All @@ -17,6 +17,7 @@ import { IconSize } from '../../Icon';
import { Tooltip } from '../../tooltip/Tooltip';
import { useFeedPreviewMode } from '../../../hooks/useFeedPreviewMode';
import { useCardActions } from '../../../hooks/cards/useCardActions';
import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship';

// Full-bleed cover: drop side padding/bottom margin and round only the bottom
// corners so the image meets the card edges. Height/crop are untouched.
Expand All @@ -29,16 +30,19 @@ const outerClasses = 'pointer-events-none absolute inset-x-2 bottom-2 z-1';
// hover/pressed colors are left to each `btn-tertiary-*` class so icons keep
// their brand tint on hover, matching the standard ActionButtons.
const pillClasses = classNames(
'pointer-events-auto flex h-10 w-full items-center overflow-hidden px-1',
'pointer-events-auto flex h-10 w-full items-center justify-between gap-0.5 overflow-hidden px-1',
'rounded-12 border border-border-subtlest-tertiary',
'bg-blur-bg text-text-primary backdrop-blur-xl backdrop-saturate-150',
'[&_.btn-quaternary]:[--button-default-color:var(--theme-text-primary)]',
'[&_.btn]:[--button-default-color:var(--theme-text-primary)]',
);

// Every action gets an equal-width centered slot so the icons stay evenly spaced
// across the pill regardless of upvote/comment counts widening a button.
const slotClasses = 'flex min-w-0 flex-1 items-center justify-center';
// Each action sizes to its content (so the count inside the upvote/comment
// button isn't clipped) and the row spreads them with `justify-between`. An
// equal-width `flex-1` layout forced every slot to 1/N of the pill, which is
// narrower than a count-bearing button on tight (e.g. 5-column) cards — so the
// buttons overflowed their slots and overlapped.
const slotClasses = 'flex min-w-0 shrink items-center justify-center';

// Dark glow behind the pill so it stays readable over busy cover images. Fixed
// pepper tint in both themes; inline gradient since it's a one-off scrim.
Expand All @@ -59,13 +63,13 @@ export function FeedCardGlassActions({
coverScrim = false,
}: ActionButtonsProps & { coverScrim?: boolean }): ReactElement | null {
const isFeedPreview = useFeedPreviewMode();
const { getUpvoteAnimation } = useBrandSponsorship();
const {
isUpvoteActive,
isDownvoteActive,
onToggleUpvote,
onToggleDownvote,
onToggleBookmark,
onCopyLink,
} = useCardActions({
post,
onUpvoteClick,
Expand All @@ -74,6 +78,24 @@ export function FeedCardGlassActions({
onCopyLinkClick,
});

// Branded upvote animation (icon swaps to the advertiser logo) when the post
// has a sponsored tag (engagement ad).
const brandAnimation = useMemo(() => {
const animationResult = getUpvoteAnimation(post.tags || []);
if (
!animationResult.shouldAnimate ||
!animationResult.colors ||
!animationResult.config
) {
return null;
}
return {
colors: animationResult.colors,
config: animationResult.config,
brandLogo: animationResult.brandLogo,
};
}, [getUpvoteAnimation, post.tags]);

if (isFeedPreview) {
return null;
}
Expand Down Expand Up @@ -110,6 +132,7 @@ export function FeedCardGlassActions({
<UpvoteButtonIcon
secondary={isUpvoteActive}
size={IconSize.XSmall}
brandAnimation={brandAnimation}
/>
}
>
Expand Down Expand Up @@ -193,11 +216,20 @@ export function FeedCardGlassActions({
</div>
<div className={slotClasses}>
<Tooltip content="Copy link" side="bottom">
{/* DEMO: static copy button. Does a plain synchronous clipboard
write with no interaction tracking or async state, so it can
never render a loading spinner in the action pill. */}
<QuaternaryButton
id="copy-post-btn"
size={ButtonSize.Small}
icon={<LinkIcon size={IconSize.XSmall} />}
onClick={onCopyLink}
onClick={() => {
if (typeof navigator !== 'undefined') {
navigator.clipboard?.writeText(
post.commentsPermalink || post.permalink || '',
);
}
}}
variant={ButtonVariant.Tertiary}
color={ButtonColor.Cabbage}
className="pointer-events-auto"
Expand Down
28 changes: 25 additions & 3 deletions packages/shared/src/components/post/focus/FocusCardActionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { ReactElement } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import type { Post } from '../../../graphql/posts';
import { UserVote } from '../../../graphql/posts';
import { useViewSize, useVotePost, ViewSize } from '../../../hooks';
import { useBookmarkPost } from '../../../hooks/useBookmarkPost';
import { useBlockPostPanel } from '../../../hooks/post/useBlockPostPanel';
import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship';
import { useCanAwardUser } from '../../../hooks/useCoresFeature';
import { useLazyModal } from '../../../hooks/useLazyModal';
import { LazyModal } from '../../modals/common/types';
Expand Down Expand Up @@ -66,6 +67,25 @@ export const FocusCardActionBar = ({
sendingUser: user,
receivingUser: post.author as LoggedUser | undefined,
});
const { getUpvoteAnimation } = useBrandSponsorship();

// Branded upvote animation (icon swaps to the advertiser logo) when the post
// has a sponsored tag (engagement ad) — same as the feed card.
const brandAnimation = useMemo(() => {
const animationResult = getUpvoteAnimation(post.tags || []);
if (
!animationResult.shouldAnimate ||
!animationResult.colors ||
!animationResult.config
) {
return null;
}
return {
colors: animationResult.colors,
config: animationResult.config,
brandLogo: animationResult.brandLogo,
};
}, [getUpvoteAnimation, post.tags]);

// Track whether the bar is pinned, and at which edge. The sentinel sits just
// above the bar: when it scrolls above the viewport top the bar is pinned at
Expand Down Expand Up @@ -233,8 +253,10 @@ export const FocusCardActionBar = ({
id="upvote-post-btn"
label="Upvote"
color={ButtonColor.Avocado}
icon={<UpvoteButtonIcon />}
iconPressed={<UpvoteButtonIcon secondary />}
icon={<UpvoteButtonIcon brandAnimation={brandAnimation} />}
iconPressed={
<UpvoteButtonIcon secondary brandAnimation={brandAnimation} />
}
count={isPinned ? upvotes : undefined}
pressed={isUpvoteActive}
onClick={onToggleUpvote}
Expand Down
12 changes: 11 additions & 1 deletion packages/shared/src/components/tags/TagTopicPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ import EntityCardSkeleton from '../cards/entity/EntityCardSkeleton';
import { TagPageNavbar } from './TagPageNavbar';
import { PublicPageSignupBanner } from '../auth/PublicPageSignupBanner';
import { largeNumberFormat } from '../../lib/numberFormat';
import { webappUrl } from '../../lib/constants';
import { isTesting, webappUrl } from '../../lib/constants';
import { GoogleCloudStrip } from '../../features/googleCloudTakeover/GoogleCloudStrip';
import { googleCloudTakeoverEnabled } from '../../features/googleCloudTakeover/config';
import {
Typography,
TypographyColor,
Expand Down Expand Up @@ -461,6 +463,14 @@ export const TagTopicPage = ({

<div className="mb-2 h-px w-full bg-border-subtlest-tertiary" />

{/* DEMO: the tag page's post feed ("All posts about …") is the last
section, so the in-feed takeover lands far below the fold. Surface
the Google Cloud placement prominently at the top of the tag page
too, so it's visible without scrolling. */}
{googleCloudTakeoverEnabled && !isTesting && (
<GoogleCloudStrip className="mb-10 w-full" />
)}

{showRoadmap && initialData?.flags?.roadmap && (
<section className="mb-10">
<SectionHeading>Roadmaps</SectionHeading>
Expand Down
Loading
Loading