diff --git a/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx b/packages/shared/src/components/cards/SimilarPosts/PostEngagementCounts.tsx
index 98ab6ed50ee..be4082f648b 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,12 @@ export function PostEngagementCounts({
{upvotes ? `${largeNumberFormat(upvotes)} Upvotes` : ''}
{upvotes && comments ? <> {separatorCharacter} > : ''}
{comments ? `${largeNumberFormat(comments)} Comments` : ''}
+ {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 309f04ef1c9..fb025fdc362 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,11 @@ const ActionButtons = ({
}: ActionButtonsProps): ReactElement => {
const config = variantConfig[variant];
const isFeedPreview = useFeedPreviewMode();
+ const { value: showBookmarkCount } = useConditionalFeature({
+ feature: featureShowBookmarkCount,
+ shouldEvaluate: true,
+ });
+ const bookmarkCount = post?.analytics?.bookmarks ?? 0;
const {
isUpvoteActive,
@@ -224,13 +230,26 @@ 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}
- />
+ >
+ {showBookmarkCount && bookmarkCount > 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 d4b9285b48a..d9320c7d0ce 100644
--- a/packages/shared/src/graphql/fragments.ts
+++ b/packages/shared/src/graphql/fragments.ts
@@ -228,6 +228,9 @@ export const FEED_POST_INFO_FRAGMENT = gql`
numUpvotes
numComments
numAwards
+ analytics {
+ bookmarks
+ }
summary
yggdrasilId
creatorTwitter
@@ -328,6 +331,7 @@ export const SHARED_POST_INFO_FRAGMENT = gql`
bookmarked
analytics {
impressions
+ bookmarks
}
numUpvotes
numComments
diff --git a/packages/shared/src/graphql/posts.ts b/packages/shared/src/graphql/posts.ts
index 058b51d3c03..8532885a58f 100644
--- a/packages/shared/src/graphql/posts.ts
+++ b/packages/shared/src/graphql/posts.ts
@@ -217,7 +217,7 @@ export interface Post {
pollOptions?: PollOption[];
numPollVotes?: number;
endsAt?: string;
- analytics?: Partial>;
+ analytics?: Partial>;
}
export type RelatedPost = Pick<
diff --git a/packages/shared/src/hooks/useBookmarkPost.ts b/packages/shared/src/hooks/useBookmarkPost.ts
index e91b4136c15..89ded0e0f73 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,
@@ -32,7 +33,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 = {
@@ -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;
};
@@ -91,11 +99,40 @@ const useBookmarkPost = ({
const postLogEvent = usePostLogEvent();
const { logOpts } = useActiveFeedContext();
- const defaultOnMutate = ({ id }) => {
- updatePostCache(client, id, (post) => ({ bookmarked: !post.bookmarked }));
+ 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
+ ? {
+ ...post.analytics,
+ bookmarks: getOptimisticBookmarkCount(
+ post.analytics.bookmarks,
+ post.bookmarked,
+ ),
+ }
+ : post.analytics,
+ }));
return () => {
- updatePostCache(client, id, (post) => ({ bookmarked: !post.bookmarked }));
+ if (!previousPost) {
+ return;
+ }
+
+ updatePostCache(client, id, () => ({
+ bookmarked: previousPost.bookmarked,
+ bookmark: previousPost.bookmark,
+ analytics: previousPost.analytics,
+ }));
};
};
@@ -255,22 +292,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 +336,7 @@ export const mutateBookmarkFeedPost = ({
const rollbackMutationHandler = () => ({
bookmarked: previousState,
bookmark: previousBookmark,
+ analytics: previousAnalytics,
});
optimisticPostUpdateInFeed(
@@ -306,8 +353,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 +381,7 @@ export const mutateBookmarkFeedPost = ({
createAdPostRollbackHandler(id, {
bookmarked: previousState,
bookmark: previousBookmark,
+ analytics: previousAnalytics,
}),
);
});
diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts
index 42822f20de5..6b1d9d5d193 100644
--- a/packages/shared/src/lib/featureManagement.ts
+++ b/packages/shared/src/lib/featureManagement.ts
@@ -123,6 +123,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(