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,