From 8ec3cf61b7bd0b706394a3e080cc4c795a880085 Mon Sep 17 00:00:00 2001
From: rebelchris
Date: Thu, 5 Mar 2026 09:00:39 +0000
Subject: [PATCH 1/8] feat(shared): display bookmark count on post cards
Add feature flag `show_bookmark_count` to gate bookmark count display.
Update Post interface and GraphQL fragments with numBookmarks field.
Show InteractionCounter in ActionButtons BookmarkButton and
PostEngagementCounts, both gated by the feature flag.
Co-Authored-By: Claude Opus 4.6
---
.../SimilarPosts/PostEngagementCounts.tsx | 4 ++++
.../components/cards/common/ActionButtons.tsx | 20 +++++++++++++++++--
.../src/components/widgets/SimilarPosts.tsx | 15 +++++++++++++-
packages/shared/src/graphql/fragments.ts | 2 ++
packages/shared/src/graphql/posts.ts | 1 +
packages/shared/src/lib/featureManagement.ts | 5 +++++
6 files changed, 44 insertions(+), 3 deletions(-)
diff --git a/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx b/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx
index 98ab6ed50ee..5c18d9101a7 100644
--- a/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx
+++ b/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx
@@ -7,12 +7,14 @@ import { largeNumberFormat } from '../../../lib';
interface PostEngagementCountsProps {
upvotes: number;
comments: number;
+ bookmarks?: number;
className?: string;
}
export function PostEngagementCounts({
upvotes,
comments,
+ bookmarks,
className,
}: PostEngagementCountsProps): ReactElement {
return (
@@ -23,6 +25,8 @@ export function PostEngagementCounts({
{upvotes ? `${largeNumberFormat(upvotes)} Upvotes` : ''}
{upvotes && comments ? <> {separatorCharacter} > : ''}
{comments ? `${largeNumberFormat(comments)} Comments` : ''}
+ {bookmarks && (upvotes || comments) ? <> {separatorCharacter} > : ''}
+ {bookmarks ? `${largeNumberFormat(bookmarks)} Bookmarks` : ''}
);
}
diff --git a/packages/shared/src/components/cards/common/ActionButtons.tsx b/packages/shared/src/components/cards/common/ActionButtons.tsx
index 309f04ef1c9..1e916b54f71 100644
--- a/packages/shared/src/components/cards/common/ActionButtons.tsx
+++ b/packages/shared/src/components/cards/common/ActionButtons.tsx
@@ -10,7 +10,8 @@ import {
DownvoteIcon,
} from '../../icons';
import { ButtonColor, ButtonSize, ButtonVariant } from '../../buttons/Button';
-import { useFeedPreviewMode } from '../../../hooks';
+import { useFeedPreviewMode, useConditionalFeature } from '../../../hooks';
+import { featureShowBookmarkCount } from '../../../lib/featureManagement';
import { UpvoteButtonIcon } from './UpvoteButtonIcon';
import { BookmarkButton } from '../../buttons';
import { IconSize } from '../../Icon';
@@ -75,6 +76,10 @@ const ActionButtons = ({
}: ActionButtonsProps): ReactElement => {
const config = variantConfig[variant];
const isFeedPreview = useFeedPreviewMode();
+ const { value: showBookmarkCount } = useConditionalFeature({
+ feature: featureShowBookmarkCount,
+ shouldEvaluate: true,
+ });
const {
isUpvoteActive,
@@ -230,7 +235,18 @@ const ActionButtons = ({
}),
}}
iconSize={config.iconSize}
- />
+ >
+ {showBookmarkCount && post?.numBookmarks > 0 && (
+
+ )}
+
unknown;
+ showBookmarkCount?: boolean;
};
const imageClassName = 'w-7 h-7 rounded-full mt-1';
const textContainerClassName = 'flex flex-col ml-3 mr-2 flex-1';
-const DefaultListItem = ({ post, onLinkClick }: PostProps): ReactElement => (
+const DefaultListItem = ({
+ post,
+ onLinkClick,
+ showBookmarkCount,
+}: PostProps): ReactElement => (
(
)}
@@ -112,6 +120,10 @@ export default function SimilarPosts({
}: SimilarPostsProps): ReactElement {
const { logEvent } = useLogContext();
const { logOpts } = useContext(ActiveFeedContext);
+ const { value: showBookmarkCount } = useConditionalFeature({
+ feature: featureShowBookmarkCount,
+ shouldEvaluate: true,
+ });
const moreButtonHref =
moreButtonProps?.href || process.env.NEXT_PUBLIC_WEBAPP_URL;
const moreButtonText = moreButtonProps?.text || 'View all';
@@ -143,6 +155,7 @@ export default function SimilarPosts({
key={post.id}
post={post}
onLinkClick={() => onLinkClick(post)}
+ showBookmarkCount={showBookmarkCount}
/>
))}
>
diff --git a/packages/shared/src/graphql/fragments.ts b/packages/shared/src/graphql/fragments.ts
index dc7872ee494..799358af1fb 100644
--- a/packages/shared/src/graphql/fragments.ts
+++ b/packages/shared/src/graphql/fragments.ts
@@ -228,6 +228,7 @@ export const FEED_POST_INFO_FRAGMENT = gql`
numUpvotes
numComments
numAwards
+ numBookmarks
summary
yggdrasilId
creatorTwitter
@@ -333,6 +334,7 @@ export const SHARED_POST_INFO_FRAGMENT = gql`
numComments
numAwards
numReposts
+ numBookmarks
videoId
yggdrasilId
creatorTwitter
diff --git a/packages/shared/src/graphql/posts.ts b/packages/shared/src/graphql/posts.ts
index 058b51d3c03..0a46cd48821 100644
--- a/packages/shared/src/graphql/posts.ts
+++ b/packages/shared/src/graphql/posts.ts
@@ -179,6 +179,7 @@ export interface Post {
numComments?: number;
numAwards?: number;
numReposts?: number;
+ numBookmarks?: number;
author?: Author;
scout?: Scout;
read?: boolean;
diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts
index e1503d8f262..45cfcec3596 100644
--- a/packages/shared/src/lib/featureManagement.ts
+++ b/packages/shared/src/lib/featureManagement.ts
@@ -118,6 +118,11 @@ export const boostSettingsFeature = new Feature('boost_settings', {
export const adImprovementsV3Feature = new Feature('ad_improvements_v3', false);
+export const featureShowBookmarkCount = new Feature(
+ 'show_bookmark_count',
+ false,
+);
+
export const featureYearInReview = new Feature('year_in_review_2025', false);
export const featureProfileCompletionIndicator = new Feature(
From c032e71095d84ab37e5204e43761c894ae553ef8 Mon Sep 17 00:00:00 2001
From: rebelchris
Date: Thu, 5 Mar 2026 09:09:26 +0000
Subject: [PATCH 2/8] fix(shared): clean up bookmark count rendering logic
Remove redundant invisible class from ActionButtons bookmark counter
(already gated by > 0 check). Fix PostEngagementCounts to avoid
rendering falsy number 0 in separator conditional.
Co-Authored-By: Claude Opus 4.6
---
.../cards/SimilarPosts/PostEngagementCounts.tsx | 8 ++++++--
.../shared/src/components/cards/common/ActionButtons.tsx | 1 -
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx b/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx
index 5c18d9101a7..be4082f648b 100644
--- a/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx
+++ b/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx
@@ -25,8 +25,12 @@ export function PostEngagementCounts({
{upvotes ? `${largeNumberFormat(upvotes)} Upvotes` : ''}
{upvotes && comments ? <> {separatorCharacter} > : ''}
{comments ? `${largeNumberFormat(comments)} Comments` : ''}
- {bookmarks && (upvotes || comments) ? <> {separatorCharacter} > : ''}
- {bookmarks ? `${largeNumberFormat(bookmarks)} Bookmarks` : ''}
+ {bookmarks ? (
+ <>
+ {upvotes || comments ? <> {separatorCharacter} > : null}
+ {largeNumberFormat(bookmarks)} Bookmarks
+ >
+ ) : null}
);
}
diff --git a/packages/shared/src/components/cards/common/ActionButtons.tsx b/packages/shared/src/components/cards/common/ActionButtons.tsx
index 1e916b54f71..119d1d13139 100644
--- a/packages/shared/src/components/cards/common/ActionButtons.tsx
+++ b/packages/shared/src/components/cards/common/ActionButtons.tsx
@@ -241,7 +241,6 @@ const ActionButtons = ({
className={classNames(
'tabular-nums',
variant === 'grid' && 'typo-footnote',
- !post.numBookmarks && 'invisible',
)}
value={post.numBookmarks}
/>
From 7774b0e03edab6bde66602e74f8f28a686b1d07e Mon Sep 17 00:00:00 2001
From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com>
Date: Thu, 5 Mar 2026 13:07:16 +0200
Subject: [PATCH 3/8] refactor(shared): read bookmark counts from analytics
Switch bookmark-count consumers from post.numBookmarks to post.analytics.bookmarks and update shared GraphQL fragments/types accordingly.
Made-with: Cursor
---
.../shared/src/components/cards/common/ActionButtons.tsx | 5 +++--
packages/shared/src/components/widgets/SimilarPosts.tsx | 2 +-
packages/shared/src/graphql/fragments.ts | 7 +++++--
packages/shared/src/graphql/posts.ts | 3 +--
4 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/packages/shared/src/components/cards/common/ActionButtons.tsx b/packages/shared/src/components/cards/common/ActionButtons.tsx
index 119d1d13139..1c4e5178e3f 100644
--- a/packages/shared/src/components/cards/common/ActionButtons.tsx
+++ b/packages/shared/src/components/cards/common/ActionButtons.tsx
@@ -80,6 +80,7 @@ const ActionButtons = ({
feature: featureShowBookmarkCount,
shouldEvaluate: true,
});
+ const bookmarkCount = post?.analytics?.bookmarks ?? 0;
const {
isUpvoteActive,
@@ -236,13 +237,13 @@ const ActionButtons = ({
}}
iconSize={config.iconSize}
>
- {showBookmarkCount && post?.numBookmarks > 0 && (
+ {showBookmarkCount && bookmarkCount > 0 && (
)}
diff --git a/packages/shared/src/components/widgets/SimilarPosts.tsx b/packages/shared/src/components/widgets/SimilarPosts.tsx
index b3ce0c92f18..c6b7aefad79 100644
--- a/packages/shared/src/components/widgets/SimilarPosts.tsx
+++ b/packages/shared/src/components/widgets/SimilarPosts.tsx
@@ -88,7 +88,7 @@ const DefaultListItem = ({
)}
diff --git a/packages/shared/src/graphql/fragments.ts b/packages/shared/src/graphql/fragments.ts
index 799358af1fb..6a9795715ac 100644
--- a/packages/shared/src/graphql/fragments.ts
+++ b/packages/shared/src/graphql/fragments.ts
@@ -228,7 +228,10 @@ export const FEED_POST_INFO_FRAGMENT = gql`
numUpvotes
numComments
numAwards
- numBookmarks
+ analytics {
+ impressions
+ bookmarks
+ }
summary
yggdrasilId
creatorTwitter
@@ -329,12 +332,12 @@ export const SHARED_POST_INFO_FRAGMENT = gql`
bookmarked
analytics {
impressions
+ bookmarks
}
numUpvotes
numComments
numAwards
numReposts
- numBookmarks
videoId
yggdrasilId
creatorTwitter
diff --git a/packages/shared/src/graphql/posts.ts b/packages/shared/src/graphql/posts.ts
index 0a46cd48821..8532885a58f 100644
--- a/packages/shared/src/graphql/posts.ts
+++ b/packages/shared/src/graphql/posts.ts
@@ -179,7 +179,6 @@ export interface Post {
numComments?: number;
numAwards?: number;
numReposts?: number;
- numBookmarks?: number;
author?: Author;
scout?: Scout;
read?: boolean;
@@ -218,7 +217,7 @@ export interface Post {
pollOptions?: PollOption[];
numPollVotes?: number;
endsAt?: string;
- analytics?: Partial>;
+ analytics?: Partial>;
}
export type RelatedPost = Pick<
From f18539e32505f2b73d6f19b3e3982bab1f0cae20 Mon Sep 17 00:00:00 2001
From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com>
Date: Thu, 5 Mar 2026 15:08:47 +0200
Subject: [PATCH 4/8] fix(shared): align bookmark counter button styling
Apply the bun tertiary button class to bookmark actions in card ActionButtons so counter color and hover state match upvote/comment interaction patterns.
Made-with: Cursor
---
.../shared/src/components/cards/common/ActionButtons.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/packages/shared/src/components/cards/common/ActionButtons.tsx b/packages/shared/src/components/cards/common/ActionButtons.tsx
index 1c4e5178e3f..fb025fdc362 100644
--- a/packages/shared/src/components/cards/common/ActionButtons.tsx
+++ b/packages/shared/src/components/cards/common/ActionButtons.tsx
@@ -230,9 +230,12 @@ const ActionButtons = ({
id: `post-${post.id}-bookmark-btn`,
onClick: onToggleBookmark,
size: config.buttonSize,
+ className: classNames(
+ 'btn-tertiary-bun',
+ variant === 'list' && 'pointer-events-auto',
+ ),
...(variant === 'list' && {
variant: ButtonVariant.Tertiary,
- className: 'pointer-events-auto',
}),
}}
iconSize={config.iconSize}
From 493cc95597d1636d03465e68935be93c649a4730 Mon Sep 17 00:00:00 2001
From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com>
Date: Thu, 5 Mar 2026 15:25:59 +0200
Subject: [PATCH 5/8] fix(shared): optimistic bookmark count updates in cache
Update bookmark mutation cache handlers to optimistically adjust analytics.bookmarks on toggle and restore previous values on rollback for feed and ad posts.
Made-with: Cursor
---
packages/shared/src/hooks/useBookmarkPost.ts | 58 +++++++++++++++++---
1 file changed, 50 insertions(+), 8 deletions(-)
diff --git a/packages/shared/src/hooks/useBookmarkPost.ts b/packages/shared/src/hooks/useBookmarkPost.ts
index e91b4136c15..8356df11825 100644
--- a/packages/shared/src/hooks/useBookmarkPost.ts
+++ b/packages/shared/src/hooks/useBookmarkPost.ts
@@ -73,6 +73,14 @@ const prepareBookmarkPostLogOptions = ({
};
};
+const getOptimisticBookmarkCount = (
+ currentBookmarks: number | null | undefined,
+ isCurrentlyBookmarked: boolean | null | undefined,
+): number => {
+ const delta = isCurrentlyBookmarked ? -1 : 1;
+ return Math.max(0, (currentBookmarks ?? 0) + delta);
+};
+
export type UseBookmarkPost = {
toggleBookmark: (props: ToggleBookmarkProps) => Promise;
};
@@ -92,10 +100,32 @@ const useBookmarkPost = ({
const { logOpts } = useActiveFeedContext();
const defaultOnMutate = ({ id }) => {
- updatePostCache(client, id, (post) => ({ bookmarked: !post.bookmarked }));
+ updatePostCache(client, id, (post) => ({
+ bookmarked: !post.bookmarked,
+ analytics: post.analytics
+ ? {
+ ...post.analytics,
+ bookmarks: getOptimisticBookmarkCount(
+ post.analytics.bookmarks,
+ post.bookmarked,
+ ),
+ }
+ : post.analytics,
+ }));
return () => {
- updatePostCache(client, id, (post) => ({ bookmarked: !post.bookmarked }));
+ updatePostCache(client, id, (post) => ({
+ bookmarked: !post.bookmarked,
+ analytics: post.analytics
+ ? {
+ ...post.analytics,
+ bookmarks: getOptimisticBookmarkCount(
+ post.analytics.bookmarks,
+ post.bookmarked,
+ ),
+ }
+ : post.analytics,
+ }));
};
};
@@ -255,22 +285,31 @@ export const mutateBookmarkFeedPost = ({
const mutationHandler = (post: Post) => {
const isBookmarked = !post?.bookmarked;
+ const nextBookmarks = getOptimisticBookmarkCount(
+ post?.analytics?.bookmarks,
+ post?.bookmarked,
+ );
return {
bookmarked: isBookmarked,
bookmark: !isBookmarked ? undefined : post?.bookmark,
+ analytics: post?.analytics
+ ? {
+ ...post.analytics,
+ bookmarks: nextBookmarks,
+ }
+ : post?.analytics,
};
};
- let previousBookmark: Bookmark;
- let previousState: boolean | undefined;
const rollbackFunctions: (() => void)[] = [];
// Handle regular post update
if (postIndexToUpdate !== -1) {
const postItem = (items[postIndexToUpdate] as PostItem)?.post;
- previousBookmark = postItem?.bookmark;
- previousState = postItem?.bookmarked;
+ const previousBookmark = postItem?.bookmark;
+ const previousState = postItem?.bookmarked;
+ const previousAnalytics = postItem?.analytics;
optimisticPostUpdateInFeed(
items,
@@ -290,6 +329,7 @@ export const mutateBookmarkFeedPost = ({
const rollbackMutationHandler = () => ({
bookmarked: previousState,
bookmark: previousBookmark,
+ analytics: previousAnalytics,
});
optimisticPostUpdateInFeed(
@@ -306,8 +346,9 @@ export const mutateBookmarkFeedPost = ({
const adPost = adItem.ad.data?.post;
if (adPost) {
- previousBookmark = adPost.bookmark;
- previousState = adPost.bookmarked;
+ const previousBookmark = adPost.bookmark;
+ const previousState = adPost.bookmarked;
+ const previousAnalytics = adPost.analytics;
// Update the ad's post in the ads cache
const adsQueryKey = [RequestKey.Ads, ...feedQueryKey];
@@ -333,6 +374,7 @@ export const mutateBookmarkFeedPost = ({
createAdPostRollbackHandler(id, {
bookmarked: previousState,
bookmark: previousBookmark,
+ analytics: previousAnalytics,
}),
);
});
From 749b20b2911914de4c6674c0bc09751939cc47be Mon Sep 17 00:00:00 2001
From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com>
Date: Thu, 5 Mar 2026 15:37:01 +0200
Subject: [PATCH 6/8] fix(shared): remove unused bookmark type import
Drop stale Bookmark type import after optimistic cache refactor to satisfy shared lint checks.
Made-with: Cursor
---
packages/shared/src/hooks/useBookmarkPost.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/shared/src/hooks/useBookmarkPost.ts b/packages/shared/src/hooks/useBookmarkPost.ts
index 8356df11825..705cb0ce037 100644
--- a/packages/shared/src/hooks/useBookmarkPost.ts
+++ b/packages/shared/src/hooks/useBookmarkPost.ts
@@ -32,7 +32,6 @@ import { useActions } from './useActions';
import { bookmarkMutationKey } from './bookmark/types';
import { useLazyModal } from './useLazyModal';
import { LazyModal } from '../components/modals/common/types';
-import type { Bookmark } from '../graphql/bookmarks';
import { useActiveFeedContext } from '../contexts';
export type ToggleBookmarkProps = {
From f6f231adf10ccd4f747327e822d44f89eac2970f Mon Sep 17 00:00:00 2001
From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com>
Date: Thu, 5 Mar 2026 17:33:36 +0200
Subject: [PATCH 7/8] fix(shared): harden bookmark optimistic rollback
Capture and restore the exact pre-mutation bookmark snapshot in defaultOnMutate, add an explicit onMutate type, and avoid requesting feed impressions where they are not needed.
Made-with: Cursor
---
packages/shared/src/graphql/fragments.ts | 1 -
packages/shared/src/hooks/useBookmarkPost.ts | 32 ++++++++++++--------
2 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/packages/shared/src/graphql/fragments.ts b/packages/shared/src/graphql/fragments.ts
index 6a9795715ac..99933b130d3 100644
--- a/packages/shared/src/graphql/fragments.ts
+++ b/packages/shared/src/graphql/fragments.ts
@@ -229,7 +229,6 @@ export const FEED_POST_INFO_FRAGMENT = gql`
numComments
numAwards
analytics {
- impressions
bookmarks
}
summary
diff --git a/packages/shared/src/hooks/useBookmarkPost.ts b/packages/shared/src/hooks/useBookmarkPost.ts
index 705cb0ce037..40e7836d566 100644
--- a/packages/shared/src/hooks/useBookmarkPost.ts
+++ b/packages/shared/src/hooks/useBookmarkPost.ts
@@ -6,7 +6,7 @@ import type {
QueryKey,
} from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import type { Ad, Post, ReadHistoryPost } from '../graphql/posts';
+import type { Ad, Post, PostData, ReadHistoryPost } from '../graphql/posts';
import {
ADD_BOOKMARKS_MUTATION,
REMOVE_BOOKMARK_MUTATION,
@@ -17,6 +17,7 @@ import { useRequestProtocol } from './useRequestProtocol';
import AuthContext from '../contexts/AuthContext';
import {
updatePostCache,
+ getPostByIdKey,
RequestKey,
updateAdPostInCache,
createAdPostRollbackHandler,
@@ -98,7 +99,15 @@ const useBookmarkPost = ({
const postLogEvent = usePostLogEvent();
const { logOpts } = useActiveFeedContext();
- const defaultOnMutate = ({ id }) => {
+ const defaultOnMutate: NonNullable = ({
+ id,
+ }) => {
+ if (!id) {
+ return undefined;
+ }
+
+ const previousPost = client.getQueryData(getPostByIdKey(id))?.post;
+
updatePostCache(client, id, (post) => ({
bookmarked: !post.bookmarked,
analytics: post.analytics
@@ -113,17 +122,14 @@ const useBookmarkPost = ({
}));
return () => {
- updatePostCache(client, id, (post) => ({
- bookmarked: !post.bookmarked,
- analytics: post.analytics
- ? {
- ...post.analytics,
- bookmarks: getOptimisticBookmarkCount(
- post.analytics.bookmarks,
- post.bookmarked,
- ),
- }
- : post.analytics,
+ if (!previousPost) {
+ return;
+ }
+
+ updatePostCache(client, id, () => ({
+ bookmarked: previousPost.bookmarked,
+ bookmark: previousPost.bookmark,
+ analytics: previousPost.analytics,
}));
};
};
From 806018ad70a6bdc4632451efdb494753852bdfda Mon Sep 17 00:00:00 2001
From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com>
Date: Fri, 6 Mar 2026 05:13:04 +0200
Subject: [PATCH 8/8] fix(shared): satisfy lint formatting in bookmark mutate
Format the snapshot query call in useBookmarkPost to satisfy prettier in shared lint CI.
Made-with: Cursor
---
packages/shared/src/hooks/useBookmarkPost.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/shared/src/hooks/useBookmarkPost.ts b/packages/shared/src/hooks/useBookmarkPost.ts
index 40e7836d566..89ded0e0f73 100644
--- a/packages/shared/src/hooks/useBookmarkPost.ts
+++ b/packages/shared/src/hooks/useBookmarkPost.ts
@@ -106,7 +106,9 @@ const useBookmarkPost = ({
return undefined;
}
- const previousPost = client.getQueryData(getPostByIdKey(id))?.post;
+ const previousPost = client.getQueryData(
+ getPostByIdKey(id),
+ )?.post;
updatePostCache(client, id, (post) => ({
bookmarked: !post.bookmarked,