From 1c5b5f6efaed1337ee189058e39ca4afc4ba3825 Mon Sep 17 00:00:00 2001
From: tomeredlich
Date: Mon, 23 Mar 2026 14:48:41 +0200
Subject: [PATCH 1/5] fix(shared): preserve card sequence with in-feed reminder
hero
Adjust in-feed hero insertion index to account for pre-feed slot cards so the hero renders in its own row without breaking card order.
Made-with: Cursor
---
packages/shared/src/components/Feed.tsx | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx
index 288f5ec1c9..bb803df95a 100644
--- a/packages/shared/src/components/Feed.tsx
+++ b/packages/shared/src/components/Feed.tsx
@@ -589,6 +589,11 @@ export default function Feed({
const showPromoBanner = !!briefBannerPage;
const columnsDiffWithPage = currentPageSize % virtualizedNumCards;
const showFirstSlotCard = showProfileCompletionCard || showBriefCard;
+ const firstGridSlotOffset = Number(showFirstSlotCard);
+ const adjustedHeroInsertIndex = Math.max(
+ heroInsertIndex - firstGridSlotOffset,
+ 0,
+ );
const indexWhenShowingPromoBanner =
currentPageSize * Number(briefBannerPage) - // number of items at that page
columnsDiffWithPage * Number(briefBannerPage) - // cards let out of rows * page number
@@ -662,7 +667,7 @@ export default function Feed({
}}
/>
)}
- {shouldShowInFeedHero && index === heroInsertIndex && (
+ {shouldShowInFeedHero && index === adjustedHeroInsertIndex && (
Date: Mon, 23 Mar 2026 15:03:00 +0200
Subject: [PATCH 2/5] fix: align in-feed reminder visibility guard with
rendered index
Pass the first feed-slot offset into the reminder hook and use the adjusted index for inline hero visibility checks, so edge cases with a pre-feed card still render consistently.
Made-with: Cursor
---
packages/shared/src/components/Feed.tsx | 1 +
.../src/hooks/notifications/useReadingReminderFeedHero.ts | 8 +++++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx
index bb803df95a..06f2ca246f 100644
--- a/packages/shared/src/components/Feed.tsx
+++ b/packages/shared/src/components/Feed.tsx
@@ -336,6 +336,7 @@ export default function Feed({
} = useReadingReminderFeedHero({
itemCount: items.length,
itemsPerRow: virtualizedNumCards,
+ firstSlotOffset: Number(showProfileCompletionCard || showBriefCard),
});
useMutationSubscription({
diff --git a/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.ts b/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.ts
index 1a378d5dce..b3bcdb8f0f 100644
--- a/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.ts
+++ b/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.ts
@@ -19,6 +19,7 @@ type ReadingReminderHeroPlacement =
interface UseReadingReminderFeedHeroProps {
itemCount: number;
itemsPerRow: number;
+ firstSlotOffset?: number;
}
interface UseReadingReminderFeedHero {
@@ -42,10 +43,15 @@ const getInitialDismissedPlacements = (): Record<
export const useReadingReminderFeedHero = ({
itemCount,
itemsPerRow,
+ firstSlotOffset = 0,
}: UseReadingReminderFeedHeroProps): UseReadingReminderFeedHero => {
const safeItemsPerRow = Math.max(1, itemsPerRow);
const heroInsertIndex =
Math.ceil(HERO_INSERT_INDEX / safeItemsPerRow) * safeItemsPerRow;
+ const adjustedHeroInsertIndex = Math.max(
+ heroInsertIndex - firstSlotOffset,
+ 0,
+ );
const { pathname } = useRouter();
const { shouldShow, title, subtitle, onEnable, onDismiss } =
useReadingReminderHero({
@@ -94,7 +100,7 @@ export const useReadingReminderFeedHero = ({
isInline &&
hasScrolledForHero &&
!dismissedPlacements[NotificationCtaPlacement.InFeedHero] &&
- itemCount > heroInsertIndex;
+ itemCount > adjustedHeroInsertIndex;
useNotificationCtaImpression(
getReadingReminderCtaParams(NotificationCtaPlacement.TopHero),
From 12ab976bd02ab86cada55ed9c9b5bed4a76d2032 Mon Sep 17 00:00:00 2001
From: tomeredlich
Date: Mon, 23 Mar 2026 17:31:42 +0200
Subject: [PATCH 3/5] fix(shared): polish bookmark reminder widget layout
Align reminder widget interactions with latest UI updates by removing the inline notification CTA, updating quick option labels, and preventing overflow in the options row. Also normalize enable-notification action alignment in modal CTA rendering.
Made-with: Cursor
---
.../notifications/EnableNotification.tsx | 71 ++++++++++++-------
.../post/common/PostContentReminder.tsx | 3 -
.../post/common/PostReminderOptions.tsx | 8 +--
3 files changed, 49 insertions(+), 33 deletions(-)
diff --git a/packages/shared/src/components/notifications/EnableNotification.tsx b/packages/shared/src/components/notifications/EnableNotification.tsx
index 6d0e3071c8..b5ef1e9752 100644
--- a/packages/shared/src/components/notifications/EnableNotification.tsx
+++ b/packages/shared/src/components/notifications/EnableNotification.tsx
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
-import React from 'react';
+import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import {
Button,
@@ -72,6 +72,7 @@ function EnableNotification({
label,
onEnableAction,
}: EnableNotificationProps): ReactElement | null {
+ const [isDismissedLocally, setIsDismissedLocally] = useState(false);
const { isEnabled: isNotificationCtaExperimentEnabled } =
useNotificationCtaExperiment();
const { shouldShowCta, acceptedJustNow, onEnable, onDismiss } =
@@ -81,7 +82,12 @@ function EnableNotification({
onEnableAction,
});
- if (!shouldShowCta) {
+ const handleDismiss = useCallback(() => {
+ setIsDismissedLocally(true);
+ onDismiss();
+ }, [onDismiss]);
+
+ if (!shouldShowCta || isDismissedLocally) {
return null;
}
@@ -102,13 +108,15 @@ function EnableNotification({
[NotificationPromptSource.SquadChecklist]: '',
[NotificationPromptSource.SourceSubscribe]: `Get notified whenever there are new posts from ${contentName}.`,
[NotificationPromptSource.ReadingReminder]: '',
- [NotificationPromptSource.BookmarkReminder]: '',
+ [NotificationPromptSource.BookmarkReminder]:
+ 'Get a push reminder right on time, even when you leave the app.',
};
const message = sourceToMessage[source] ?? '';
const classes = containerClassName[source] ?? '';
const showTextCloseButton = sourceRenderTextCloseButton[source] ?? false;
- const hideCloseButton = source === NotificationPromptSource.NewComment;
const buttonText = sourceToButtonText[source] ?? 'Enable notifications';
+ const shouldUseTopRightCloseButton =
+ source === NotificationPromptSource.NotificationsPage;
const shouldShowNotificationArtwork =
source === NotificationPromptSource.NotificationsPage;
const shouldAnimateBellCta =
@@ -177,7 +185,7 @@ function EnableNotification({
>
{buttonText}
-
+
);
}
@@ -236,7 +244,7 @@ function EnableNotification({
alt="A sample browser notification"
/>
-
+
{!acceptedJustNow && (
)}
+ {!showTextCloseButton && !shouldUseTopRightCloseButton && (
+
+ )}
- {!showTextCloseButton && (
+ {shouldUseTopRightCloseButton && (
)}
@@ -327,19 +338,22 @@ function EnableNotification({
)}
{shouldInlineActionWithMessage && (
-
- ) : undefined
- }
- onClick={onEnable}
- >
- {buttonText}
-
+
+
+ ) : undefined
+ }
+ onClick={onEnable}
+ >
+ {buttonText}
+
+
+
)}
{shouldUseVerticalContentLayout &&
!acceptedJustNow &&
@@ -363,7 +377,7 @@ function EnableNotification({
Dismiss
)}
+ {!showTextCloseButton &&
+ !shouldInlineActionWithMessage &&
+ !shouldUseTopRightCloseButton && (
+
+ )}
- {!showTextCloseButton && !hideCloseButton && (
+ {shouldUseTopRightCloseButton && (
)}
diff --git a/packages/shared/src/components/post/common/PostContentReminder.tsx b/packages/shared/src/components/post/common/PostContentReminder.tsx
index 03119bdc6a..0344b077f4 100644
--- a/packages/shared/src/components/post/common/PostContentReminder.tsx
+++ b/packages/shared/src/components/post/common/PostContentReminder.tsx
@@ -3,8 +3,6 @@ import React from 'react';
import classNames from 'classnames';
import { PostContentWidget } from './PostContentWidget';
import type { Post } from '../../../graphql/posts';
-import { BookmarkReminderIcon } from '../../icons/Bookmark/Reminder';
-import { IconSize } from '../../Icon';
import { PostReminderOptions } from './PostReminderOptions';
import { useBookmarkReminderCover } from '../../../hooks/bookmark/useBookmarkReminderCover';
@@ -26,7 +24,6 @@ export function PostContentReminder({
return (
}
title="Don’t have time now? Set a reminder"
>
diff --git a/packages/shared/src/components/post/common/PostReminderOptions.tsx b/packages/shared/src/components/post/common/PostReminderOptions.tsx
index decd48675f..0f8366d913 100644
--- a/packages/shared/src/components/post/common/PostReminderOptions.tsx
+++ b/packages/shared/src/components/post/common/PostReminderOptions.tsx
@@ -39,14 +39,14 @@ export function PostReminderOptions({
onInteract(previousInteraction);
};
return (
-
+
-
+
);
}
@@ -260,20 +254,20 @@ function EnableNotification({
)}
{!showTextCloseButton && !shouldUseTopRightCloseButton && (
-
+
)}
{shouldUseTopRightCloseButton && (
)}
@@ -352,7 +346,7 @@ function EnableNotification({
>
{buttonText}
-
+
)}
{shouldUseVerticalContentLayout &&
@@ -405,7 +399,7 @@ function EnableNotification({
@@ -413,14 +407,14 @@ function EnableNotification({
{!showTextCloseButton &&
!shouldInlineActionWithMessage &&
!shouldUseTopRightCloseButton && (
-
+
)}
{shouldUseTopRightCloseButton && (
)}
diff --git a/packages/shared/src/hooks/notifications/notificationExperimentDebug.ts b/packages/shared/src/hooks/notifications/notificationExperimentDebug.ts
deleted file mode 100644
index 6f663b1180..0000000000
--- a/packages/shared/src/hooks/notifications/notificationExperimentDebug.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { isDevelopment } from '../../lib/constants';
-
-const DEBUG_TOGGLE_QUERY_PARAM = 'debug_notification_experiment';
-const DEBUG_VARIATION_QUERY_PARAM = 'debug_notification_variation';
-const DEBUG_TOGGLE_STORAGE_KEY = 'debug_notification_experiment';
-const DEBUG_VARIATION_STORAGE_KEY = 'debug_notification_variation';
-
-export const NotificationDebugVariation = {
- All: 'all',
- Control: 'control',
- Hero: 'hero',
- Inline: 'inline',
-} as const;
-
-type NotificationDebugVariationType =
- (typeof NotificationDebugVariation)[keyof typeof NotificationDebugVariation];
-
-const variationValues = new Set(
- Object.values(NotificationDebugVariation),
-);
-
-const parseDebugToggle = (value: string | null): boolean | undefined => {
- if (!value) {
- return undefined;
- }
-
- if (value === '1' || value === 'true' || value === 'on') {
- return true;
- }
-
- if (value === '0' || value === 'false' || value === 'off') {
- return false;
- }
-
- return undefined;
-};
-
-const parseDebugVariation = (
- value: string | null,
-): NotificationDebugVariationType | undefined => {
- if (!value) {
- return undefined;
- }
-
- return variationValues.has(value as NotificationDebugVariationType)
- ? (value as NotificationDebugVariationType)
- : undefined;
-};
-
-const getQueryParam = (param: string): string | null => {
- if (typeof window === 'undefined') {
- return null;
- }
-
- return new URLSearchParams(window.location.search).get(param);
-};
-
-const getStorageValue = (key: string): string | null => {
- if (typeof window === 'undefined') {
- return null;
- }
-
- try {
- return window.localStorage.getItem(key);
- } catch {
- return null;
- }
-};
-
-const setStorageValue = (key: string, value: string): void => {
- if (typeof window === 'undefined') {
- return;
- }
-
- try {
- window.localStorage.setItem(key, value);
- } catch {
- // Ignore localStorage failures in debug mode.
- }
-};
-
-const resolveDebugToggle = (): boolean => {
- const queryValue = parseDebugToggle(getQueryParam(DEBUG_TOGGLE_QUERY_PARAM));
- if (queryValue !== undefined) {
- setStorageValue(DEBUG_TOGGLE_STORAGE_KEY, queryValue ? '1' : '0');
- return queryValue;
- }
-
- return getStorageValue(DEBUG_TOGGLE_STORAGE_KEY) === '1';
-};
-
-const resolveDebugVariation = (): NotificationDebugVariationType | null => {
- const queryValue = parseDebugVariation(
- getQueryParam(DEBUG_VARIATION_QUERY_PARAM),
- );
- if (queryValue) {
- setStorageValue(DEBUG_VARIATION_STORAGE_KEY, queryValue);
- return queryValue;
- }
-
- const storedValue = parseDebugVariation(
- getStorageValue(DEBUG_VARIATION_STORAGE_KEY),
- );
- return storedValue ?? null;
-};
-
-export const isNotificationExperimentDebugEnabled = (): boolean => {
- if (!isDevelopment) {
- return false;
- }
-
- return resolveDebugToggle();
-};
-
-export const getNotificationExperimentDebugVariation =
- (): NotificationDebugVariationType | null => {
- if (!isNotificationExperimentDebugEnabled()) {
- return null;
- }
-
- return resolveDebugVariation() ?? NotificationDebugVariation.All;
- };
-
-export const isNotificationExperimentVariationDebugEnabled = (
- variation: NotificationDebugVariationType,
-): boolean => {
- const debugVariation = getNotificationExperimentDebugVariation();
- if (!debugVariation) {
- return false;
- }
-
- return (
- debugVariation === NotificationDebugVariation.All ||
- debugVariation === variation
- );
-};
diff --git a/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.spec.tsx b/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.spec.tsx
index b9630aab01..55dcc987a7 100644
--- a/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.spec.tsx
+++ b/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.spec.tsx
@@ -166,6 +166,31 @@ describe('useReadingReminderFeedHero', () => {
expect(result.current.shouldShowInFeedHero).toBe(false);
});
+ it('should account for the first feed slot offset when placing the inline hero', () => {
+ mockUseReadingReminderVariation.mockReturnValue({
+ variation: 'inline',
+ isControl: false,
+ isHero: false,
+ isInline: true,
+ });
+
+ const { result } = renderHook(() =>
+ useReadingReminderFeedHero({
+ itemCount: 6,
+ itemsPerRow: 1,
+ firstSlotOffset: 1,
+ }),
+ );
+
+ act(() => {
+ window.scrollY = 350;
+ window.dispatchEvent(new Event('scroll'));
+ });
+
+ expect(result.current.adjustedHeroInsertIndex).toBe(5);
+ expect(result.current.shouldShowInFeedHero).toBe(true);
+ });
+
it('should not show either variation off the homepage', () => {
mockUseRouter.mockReturnValue({ pathname: '/bookmarks' });
diff --git a/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.ts b/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.ts
index be1d7fb7d9..6f947df3b0 100644
--- a/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.ts
+++ b/packages/shared/src/hooks/notifications/useReadingReminderFeedHero.ts
@@ -9,7 +9,6 @@ import {
useNotificationCtaImpression,
} from './useNotificationCtaAnalytics';
import { useReadingReminderVariation } from './useReadingReminderVariation';
-import { isNotificationExperimentDebugEnabled } from './notificationExperimentDebug';
const HERO_INSERT_INDEX = 6;
const HERO_SCROLL_THRESHOLD_PX = 300;
@@ -24,7 +23,7 @@ interface UseReadingReminderFeedHeroProps {
}
interface UseReadingReminderFeedHero {
- heroInsertIndex: number;
+ adjustedHeroInsertIndex: number;
shouldShowTopHero: boolean;
shouldShowInFeedHero: boolean;
title: string;
@@ -46,7 +45,6 @@ export const useReadingReminderFeedHero = ({
itemsPerRow,
firstSlotOffset = 0,
}: UseReadingReminderFeedHeroProps): UseReadingReminderFeedHero => {
- const isNotificationDebugMode = isNotificationExperimentDebugEnabled();
const safeItemsPerRow = Math.max(1, itemsPerRow);
const heroInsertIndex =
Math.ceil(HERO_INSERT_INDEX / safeItemsPerRow) * safeItemsPerRow;
@@ -60,8 +58,7 @@ export const useReadingReminderFeedHero = ({
requireMobile: false,
});
const isHomePage = pathname === webappUrl;
- const shouldEvaluateReminderPlacement =
- (isHomePage || isNotificationDebugMode) && shouldShow;
+ const shouldEvaluateReminderPlacement = isHomePage && shouldShow;
const { isHero, isInline } = useReadingReminderVariation({
shouldEvaluate: shouldEvaluateReminderPlacement,
});
@@ -101,7 +98,7 @@ export const useReadingReminderFeedHero = ({
const shouldShowInFeedHero =
canShowReminderPlacements &&
isInline &&
- (isNotificationDebugMode || hasScrolledForHero) &&
+ hasScrolledForHero &&
!dismissedPlacements[NotificationCtaPlacement.InFeedHero] &&
itemCount > adjustedHeroInsertIndex;
@@ -143,7 +140,7 @@ export const useReadingReminderFeedHero = ({
);
return {
- heroInsertIndex,
+ adjustedHeroInsertIndex,
shouldShowTopHero,
shouldShowInFeedHero,
title,
diff --git a/packages/shared/src/hooks/notifications/useReadingReminderHero.ts b/packages/shared/src/hooks/notifications/useReadingReminderHero.ts
index 6b3d8ac2b5..661c3e5524 100644
--- a/packages/shared/src/hooks/notifications/useReadingReminderHero.ts
+++ b/packages/shared/src/hooks/notifications/useReadingReminderHero.ts
@@ -12,7 +12,6 @@ import { usePushNotificationMutation } from './usePushNotificationMutation';
import { LogEvent, NotificationPromptSource } from '../../lib/log';
import { featureReadingReminderHeroCopy } from '../../lib/featureManagement';
import { useConditionalFeature } from '../useConditionalFeature';
-import { isNotificationExperimentDebugEnabled } from './notificationExperimentDebug';
interface UseReadingReminderHero {
shouldShow: boolean;
@@ -61,7 +60,6 @@ const getIsRegisteredToday = (createdAt?: string | Date): boolean => {
export const useReadingReminderHero = ({
requireMobile = true,
}: UseReadingReminderHeroProps = {}): UseReadingReminderHero => {
- const isNotificationDebugMode = isNotificationExperimentDebugEnabled();
const { isLoggedIn, user } = useAuthContext();
const { logEvent } = useLogContext();
const { onEnablePush } = usePushNotificationMutation();
@@ -98,7 +96,6 @@ export const useReadingReminderHero = ({
const hasSeenToday = getHasSeenToday(lastSeen);
const [hasShownInSession, setHasShownInSession] = useState(false);
- const [hasDismissedInSession, setHasDismissedInSession] = useState(false);
const shouldShowBase = shouldEvaluate && !hasSeenToday && isFetched;
useEffect(() => {
@@ -133,15 +130,13 @@ export const useReadingReminderHero = ({
}, [logEvent, onEnablePush, setLastSeen, subscribePersonalizedDigest, user]);
const onDismiss = useCallback(async () => {
- setHasDismissedInSession(true);
await setLastSeen(READING_REMINDER_DISMISSED);
}, [setLastSeen]);
- const shouldShow = isNotificationDebugMode
- ? !hasDismissedInSession
- : !isSubscribedToReadingReminder &&
- !isDismissed &&
- (shouldShowBase || hasShownInSession);
+ const shouldShow =
+ !isSubscribedToReadingReminder &&
+ !isDismissed &&
+ (shouldShowBase || hasShownInSession);
return {
shouldShow,