Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3291921
feat(webapp): add squad notification toast and polish CTA layout
tomeredlich Mar 10, 2026
f963959
fix(shared): polish reminder options and notification toasts
tomeredlich Mar 10, 2026
f19b1cc
fix(webapp): restore production-like appearance and feed sizing behavior
tomeredlich Mar 10, 2026
3ddf86e
fix(shared): reuse source-follow notification bubble in streak recove…
tomeredlich Mar 12, 2026
3b06e1c
feat(shared): add contextual notification prompts for engagement actions
tomeredlich Mar 12, 2026
794cb95
fix(shared): align reading reminder modal hero layout and copy sizing
tomeredlich Mar 12, 2026
2d2dd0d
feat(shared): add floating style variant for desktop reading reminder…
tomeredlich Mar 12, 2026
60751dc
feat(shared): force show reading reminder hero on mobile and desktop
tomeredlich Mar 12, 2026
b8b65ca
feat(shared): add centered sidebar reminder prompt CTA
tomeredlich Mar 16, 2026
247b06c
fix(shared): simplify notification CTA behavior and clean feed chrome
tomeredlich Mar 16, 2026
899e714
feat(shared): show reminder top hero inline after feed scroll
tomeredlich Mar 16, 2026
2c5807f
feat(shared): add dedicated force flags for reminder surfaces
tomeredlich Mar 16, 2026
2e2ef83
feat(shared): add contextual notification CTAs across engagement surf…
tomeredlich Mar 17, 2026
a6fdcf5
fix(shared): address CI lint errors and Claude review feedback
tomeredlich Mar 17, 2026
1c35d5a
refactor: centralize notification CTA rollout
idoshamun Mar 18, 2026
3964908
chore: merge origin/main into notification CTA rollout
idoshamun Mar 18, 2026
335084c
fix: wire notification CTA rollout correctly
idoshamun Mar 18, 2026
5be30cc
fix: align popup notification enable flows
idoshamun Mar 18, 2026
faaff5f
fix: standardize notification CTA analytics
idoshamun Mar 18, 2026
266b4c0
fix: restore reading reminder fallback behavior
idoshamun Mar 18, 2026
f971f82
perf: defer notification CTA feature evaluation
idoshamun Mar 18, 2026
93004fa
refactor: simplify notification CTA rollout wiring
idoshamun Mar 18, 2026
4faa1a1
refactor: extract feed reminder rollout hook
idoshamun Mar 18, 2026
b0e17d7
Subscribe sources from notification CTA
idoshamun Mar 18, 2026
67610dc
fix: stabilize reading reminder hero layout across feed breakpoints
tomeredlich Mar 18, 2026
42d92d1
revert: remove tag follow notification CTA surfaces
tomeredlich Mar 18, 2026
7cef7a6
fix: remove upvote notification CTAs and restore squad toast prompt
tomeredlich Mar 18, 2026
ae5dc41
fix: address review feedback on notification CTA branch
tomeredlich Mar 18, 2026
9f959db
Remove notification CTA test overrides
idoshamun Mar 19, 2026
ab315e2
Merge origin/main into feat/notification-cta-polish
idoshamun Mar 19, 2026
cef46bd
Fix strict CI regressions after merge
idoshamun Mar 19, 2026
c8896f5
fix: guard notification listener during SSR
idoshamun Mar 19, 2026
c3dfc92
docs: restore AGENTS and note conventional commits
idoshamun Mar 19, 2026
098dd40
feat: add reading reminder variation flag
idoshamun Mar 19, 2026
925322d
fix: address notification cta review feedback
idoshamun Mar 19, 2026
8a93ebb
Merge branch 'main' into feat/notification-cta-polish
idoshamun Mar 22, 2026
9732556
fix(shared): resolve source CTA strict regressions
idoshamun Mar 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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ const handleClick = useCallback((key: string) => {

Keep PR descriptions concise and to the point. Reviewers should not be exhausted by lengthy explanations.

Use conventional commit messages for all commits, for example `fix: ...`, `feat: ...`, or `chore: ...`.

Before opening a PR, run `git diff --name-only origin/main...HEAD` and confirm every changed file belongs to the current task. If unrelated files appear (for example from reverted or merged commits), clean the branch history first.

## Code Review Guidelines
Expand Down
127 changes: 95 additions & 32 deletions packages/shared/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ import { useLogContext } from '../contexts/LogContext';
import { feedLogExtra, postLogEvent } from '../lib/feed';
import { usePostModalNavigation } from '../hooks/usePostModalNavigation';
import { useSharePost } from '../hooks/useSharePost';
import { LogEvent, Origin, TargetId } from '../lib/log';
import {
LogEvent,
NotificationCtaPlacement,
Origin,
TargetId,
} from '../lib/log';
import { SharedFeedPage } from './utilities';
import type { FeedContainerProps } from './feeds/FeedContainer';
import { FeedContainer } from './feeds/FeedContainer';
Expand Down Expand Up @@ -60,13 +65,16 @@ import { FeedCardContext } from '../features/posts/FeedCardContext';
import {
briefCardFeedFeature,
briefFeedEntrypointPage,
featureFeedAdTemplate,
featureFeedLayoutV2,
} from '../lib/featureManagement';
import type { AwardProps } from '../graphql/njord';
import { getProductsQueryOptions } from '../graphql/njord';
import { useUpdateQuery } from '../hooks/useUpdateQuery';
import { BriefBannerFeed } from './cards/brief/BriefBanner/BriefBannerFeed';
import { ActionType } from '../graphql/actions';
import { TopHero } from './banners/HeroBottomBanner';
import { useReadingReminderFeedHero } from '../hooks/notifications/useReadingReminderFeedHero';

const FeedErrorScreen = dynamic(
() => import(/* webpackChunkName: "feedErrorScreen" */ './FeedErrorScreen'),
Expand Down Expand Up @@ -251,6 +259,8 @@ export default function Feed<T>({
});
const showBriefCard = shouldEvaluateBriefCard && briefCardFeatureValue;
const [getProducts] = useUpdateQuery(getProductsQueryOptions());
const adTemplate = currentSettings.adTemplate ??
featureFeedAdTemplate.defaultValue?.default ?? { adStart: 1 };

const { value: briefBannerPage } = useConditionalFeature({
feature: briefFeedEntrypointPage,
Expand All @@ -272,10 +282,10 @@ export default function Feed<T>({
pageSize ?? currentSettings.pageSize,
isSquadFeed || shouldUseListFeedLayout
? {
...currentSettings.adTemplate,
...adTemplate,
adStart: 2, // always make adStart 2 for squads due to welcome and pinned posts
}
: currentSettings.adTemplate,
: adTemplate,
numCards,
{
onEmptyFeed,
Expand All @@ -294,7 +304,9 @@ export default function Feed<T>({
},
);
const canFetchMore = allowFetchMore ?? queryCanFetchMore;
const [postModalIndex, setPostModalIndex] = useState<PostLocation>(null);
const [postModalIndex, setPostModalIndex] = useState<PostLocation | null>(
null,
);
const { onMenuClick, postMenuIndex, postMenuLocation } = useFeedContextMenu();
const useList = isListMode && numCards > 1;
const virtualizedNumCards = useList ? 1 : numCards;
Expand All @@ -313,6 +325,19 @@ export default function Feed<T>({
canFetchMore,
feedName,
});
const {
heroInsertIndex,
shouldShowTopHero,
shouldShowInFeedHero,
title: readingReminderTitle,
subtitle: readingReminderSubtitle,
shouldShowDismiss: shouldShowReadingReminderDismiss,
onEnableHero,
onDismissHero,
} = useReadingReminderFeedHero({
itemCount: items.length,
itemsPerRow: virtualizedNumCards,
});

useMutationSubscription({
matcher: ({ mutation }) => {
Expand All @@ -326,19 +351,24 @@ export default function Feed<T>({

if (type === 'POST') {
const postItem = items.find(
(item: PostItem) => item.post.id === entityId && item.type === 'post',
) as PostItem;

const currentPost = postItem?.post;
(item): item is PostItem =>
item.type === 'post' && item.post.id === entityId,
);

if (!!currentPost && entityId !== currentPost.id) {
if (!postItem) {
return;
}

const currentPost = postItem.post;

const awardProduct = getProducts()?.edges.find(
(item) => item.node.id === productId,
)?.node;

if (!currentPost.userState || awardProduct?.value === undefined) {
return;
}

updatePost(postItem.page, postItem.index, {
...currentPost,
userState: {
Expand All @@ -359,13 +389,14 @@ export default function Feed<T>({
});

const logOpts = useMemo(() => {
const modalRow = postModalIndex?.row;
const modalColumn = postModalIndex?.column;

return {
columns: virtualizedNumCards,
row: !isNullOrUndefined(postModalIndex?.row)
? postModalIndex.row
: postMenuLocation?.row,
column: !isNullOrUndefined(postModalIndex?.column)
? postModalIndex.column
row: !isNullOrUndefined(modalRow) ? modalRow : postMenuLocation?.row,
column: !isNullOrUndefined(modalColumn)
? modalColumn
: postMenuLocation?.column,
is_ad: selectedPostIsAd ? true : undefined,
};
Expand Down Expand Up @@ -399,7 +430,7 @@ export default function Feed<T>({

const { toggleUpvote, toggleDownvote } = useFeedVotePost({
feedName,
ranking,
ranking: ranking ?? '',
items,
updatePost,
feedQueryKey,
Expand All @@ -408,7 +439,7 @@ export default function Feed<T>({
const { toggleBookmark } = useFeedBookmarkPost({
feedName,
feedQueryKey,
ranking,
ranking: ranking ?? '',
items,
updatePost,
});
Expand Down Expand Up @@ -541,7 +572,7 @@ export default function Feed<T>({
}
};

const PostModal = PostModalMap[selectedPost?.type];
const PostModal = selectedPost ? PostModalMap[selectedPost.type] : undefined;

if (isError) {
return <FeedErrorScreen error={feedError} />;
Expand All @@ -555,12 +586,31 @@ export default function Feed<T>({
feedName as SharedFeedPage,
);

const currentPageSize = pageSize ?? currentSettings.pageSize;
const showPromoBanner = !!briefBannerPage;
const columnsDiffWithPage = currentPageSize % virtualizedNumCards;
const showFirstSlotCard = showProfileCompletionCard || showBriefCard;
const indexWhenShowingPromoBanner =
currentPageSize * Number(briefBannerPage) - // number of items at that page
columnsDiffWithPage * Number(briefBannerPage) - // cards let out of rows * page number
Number(showFirstSlotCard);

const FeedWrapperComponent = isSearchPageLaptop
? SearchResultsLayout
: FeedContainer;
const containerProps = isSearchPageLaptop
? {}
: {
topContent: shouldShowTopHero ? (
<TopHero
className="pt-2"
title={readingReminderTitle}
subtitle={readingReminderSubtitle}
shouldShowDismiss={shouldShowReadingReminderDismiss}
onCtaClick={() => onEnableHero(NotificationCtaPlacement.TopHero)}
onClose={() => onDismissHero(NotificationCtaPlacement.TopHero)}
/>
) : undefined,
header,
inlineHeader,
className,
Expand All @@ -574,15 +624,6 @@ export default function Feed<T>({
disableListWidthConstraint,
};

const currentPageSize = pageSize ?? currentSettings.pageSize;
const showPromoBanner = !!briefBannerPage;
const columnsDiffWithPage = currentPageSize % virtualizedNumCards;
const showFirstSlotCard = showProfileCompletionCard || showBriefCard;
const indexWhenShowingPromoBanner =
currentPageSize * Number(briefBannerPage) - // number of items at that page
columnsDiffWithPage * Number(briefBannerPage) - // cards let out of rows * page number
Number(showFirstSlotCard);

return (
<ActiveFeedContext.Provider value={feedContextValue}>
<FeedWrapperComponent {...containerProps}>
Expand All @@ -609,20 +650,42 @@ export default function Feed<T>({
<FeedCardContext.Provider
key={getFeedItemKey(item, index)}
value={{
boostedBy:
isBoostedPostAd(item) &&
(item.ad.data?.post?.author || item.ad.data?.post?.scout),
boostedBy: isBoostedPostAd(item)
? item.ad.data?.post?.author || item.ad.data?.post?.scout
: undefined,
}}
>
{showPromoBanner && index === indexWhenShowingPromoBanner && (
<BriefBannerFeed
style={{
gridColumn:
!shouldUseListFeedLayout &&
`span ${virtualizedNumCards}`,
gridColumn: !shouldUseListFeedLayout
? `span ${virtualizedNumCards}`
: undefined,
}}
/>
)}
{shouldShowInFeedHero && index === heroInsertIndex && (
<div
style={{
gridColumn: !shouldUseListFeedLayout
? `span ${virtualizedNumCards}`
: undefined,
}}
>
<TopHero
className="pt-0"
title={readingReminderTitle}
subtitle={readingReminderSubtitle}
shouldShowDismiss={shouldShowReadingReminderDismiss}
onCtaClick={() =>
onEnableHero(NotificationCtaPlacement.InFeedHero)
}
onClose={() =>
onDismissHero(NotificationCtaPlacement.InFeedHero)
}
/>
</div>
)}
<FeedItemComponent
item={item}
index={index}
Expand Down
Loading
Loading