From 27539582339c71a84b92ae8f53a2f2d8de3e52ee Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:55:08 +0200 Subject: [PATCH 1/2] fix: recover notification CTR with celebratory copy and dynamic push headings Post-#3657 data showed CTR declines across 7 notification types. Root causes: lost celebratory tone in achievement/milestone types, and bland push headings replacing the curiosity-inducing "New update" default. Title changes: - user_given_top_reader: restore "Great news!" opener (-84.6% without it) - achievement_unlocked: add "You earned" for personal warmth - squad_member_joined: add "Say hi!" CTA - Upvote milestones: geeky tiered copy (dev/gaming references) replacing dashboard-stat "number. remark" format Push heading changes: - Dynamic headings for squad_post_added ("New post in {squad}"), squad_new_comment and article_new_comment ("{name} commented") - achievement_unlocked: "Achievement unlocked" -> "Level up!" - upvote milestones: "Milestone reached" -> "New milestone" Preserves all improvements from #3657 (source_post_approved +106.5%, post_bookmark_reminder +23.4%, comment_mention +17.6%). Made-with: Cursor --- __tests__/notifications/index.ts | 4 ++- __tests__/workers/newNotificationV2Mail.ts | 4 +-- src/notifications/generate.ts | 6 ++-- src/onesignal.ts | 32 ++++++++++++++++++---- src/workers/notifications/utils.ts | 26 +++++++++--------- 5 files changed, 47 insertions(+), 25 deletions(-) diff --git a/__tests__/notifications/index.ts b/__tests__/notifications/index.ts index 54a25a0cf7..188b5405f2 100644 --- a/__tests__/notifications/index.ts +++ b/__tests__/notifications/index.ts @@ -1100,7 +1100,9 @@ describe('generateNotification', () => { expect(actual.notification.targetUrl).toEqual( 'http://localhost:5002/squads/a', ); - expect(actual.notification.title).toEqual('Tsahi joined A'); + expect(actual.notification.title).toEqual( + 'Tsahi joined A. Say hi!', + ); expect(actual.avatars).toEqual([ { image: 'http://image.com/a', diff --git a/__tests__/workers/newNotificationV2Mail.ts b/__tests__/workers/newNotificationV2Mail.ts index d5f39c250d..7c7e1d9904 100644 --- a/__tests__/workers/newNotificationV2Mail.ts +++ b/__tests__/workers/newNotificationV2Mail.ts @@ -316,7 +316,7 @@ it('should set parameters for article_upvote_milestone email', async () => { post_image: 'https://daily.dev/image.jpg', post_title: 'P1', upvotes: '50', - upvote_title: '50 upvotes. Your post is trending 🔥', + upvote_title: '50 upvotes! Trending on the feed 🔥', }); expect(args.transactional_message_id).toEqual('22'); }); @@ -682,7 +682,7 @@ it('should set parameters for comment_upvote_milestone email', async () => { discussion_link: 'http://localhost:5002/posts/p1?utm_source=notification&utm_medium=email&utm_campaign=comment_upvote_milestone#c-c1', main_comment: 'parent comment', - upvote_title: '50 upvotes. Your post is trending 🔥', + upvote_title: '50 upvotes! Trending on the feed 🔥', }); expect(args.transactional_message_id).toEqual('44'); }); diff --git a/src/notifications/generate.ts b/src/notifications/generate.ts index f882190954..4808e267c8 100644 --- a/src/notifications/generate.ts +++ b/src/notifications/generate.ts @@ -107,7 +107,7 @@ export const notificationTitleMap: Record< ctx: NotificationPostContext & NotificationSourceContext & NotificationDoneByContext, - ) => `${ctx.doneBy.name} joined ${ctx.source.name}`, + ) => `${ctx.doneBy.name} joined ${ctx.source.name}. Say hi!`, squad_new_comment: (ctx: NotificationCommenterContext) => `${ctx.commenter.name} commented on your post on ${ctx.source.name}.`, squad_reply: (ctx: NotificationCommenterContext) => @@ -150,7 +150,7 @@ export const notificationTitleMap: Record< user_given_top_reader: (ctx: NotificationUserTopReaderContext) => { const keyword = (ctx.keyword.flags as KeywordFlags)?.title || ctx.keyword.value; - return `You earned the Top Reader badge in ${keyword}`; + return `Great news! You earned the Top Reader badge in ${keyword}`; }, source_post_approved: (ctx: NotificationPostContext) => `Your post in ${ctx.source.name} has been approved and is now live`, @@ -239,7 +239,7 @@ export const notificationTitleMap: Record< `Your feedback has been resolved`, feedback_cancelled: () => feedbackCancelledTitle, achievement_unlocked: (ctx: NotificationAchievementContext) => - `Achievement unlocked! ${ctx.achievementName}`, + `Achievement unlocked! You earned ${ctx.achievementName}`, digest_ready: () => `Your personalized digest is ready`, }; diff --git a/src/onesignal.ts b/src/onesignal.ts index d7375aa57d..884a6232f4 100644 --- a/src/onesignal.ts +++ b/src/onesignal.ts @@ -25,8 +25,8 @@ const pushHeadingMap: Partial> = { [NotificationType.SquadReply]: 'New reply', [NotificationType.CommentMention]: 'You were mentioned', [NotificationType.PostMention]: 'You were mentioned', - [NotificationType.ArticleUpvoteMilestone]: 'Milestone reached', - [NotificationType.CommentUpvoteMilestone]: 'Milestone reached', + [NotificationType.ArticleUpvoteMilestone]: 'New milestone', + [NotificationType.CommentUpvoteMilestone]: 'New milestone', [NotificationType.SquadPostAdded]: 'New squad post', [NotificationType.SquadMemberJoined]: 'New member', [NotificationType.UserFollow]: 'New follower', @@ -36,7 +36,7 @@ const pushHeadingMap: Partial> = { [NotificationType.DigestReady]: 'Digest ready', [NotificationType.StreakResetRestore]: 'Streak broken', [NotificationType.UserGiftedPlus]: 'Plus gift', - [NotificationType.AchievementUnlocked]: 'Achievement unlocked', + [NotificationType.AchievementUnlocked]: 'Level up!', [NotificationType.PollResult]: 'Poll results', [NotificationType.PollResultAuthor]: 'Poll results', [NotificationType.FeedbackResolved]: 'Feedback update', @@ -75,8 +75,28 @@ const pushHeadingMap: Partial> = { [NotificationType.SquadSubscribeToNotification]: 'Squad notifications', }; -const getPushHeading = (type: string): string => - pushHeadingMap[type as NotificationType] ?? 'daily.dev'; +const pushHeadingFnMap: Partial< + Record string> +> = { + [NotificationType.SquadPostAdded]: (title) => { + const match = title.match(/([^<]+)<\/b>[^<]*([^<]+)<\/b>/); + return match ? `New post in ${match[2]}` : 'New squad post'; + }, + [NotificationType.SquadNewComment]: (title) => { + const match = title.match(/([^<]+)<\/b>/); + return match ? `${match[1]} commented` : 'New comment'; + }, + [NotificationType.ArticleNewComment]: (title) => { + const match = title.match(/([^<]+)<\/b>/); + return match ? `${match[1]} commented` : 'New comment'; + }, +}; + +const getPushHeading = (type: string, title?: string): string => { + const fn = pushHeadingFnMap[type as NotificationType]; + if (fn && title) return fn(title); + return pushHeadingMap[type as NotificationType] ?? 'daily.dev'; +}; type PushOpts = { increaseBadge?: boolean }; @@ -121,7 +141,7 @@ export async function sendPushNotification( const push = createPush(userIds, targetUrl, type, { increaseBadge: true }); push.contents = { en: basicHtmlStrip(title) }; - push.headings = { en: getPushHeading(type) }; + push.headings = { en: getPushHeading(type, title) }; push.data = { notificationId: id }; if (avatar) { push.chrome_web_icon = mapCloudinaryUrl(avatar.image); diff --git a/src/workers/notifications/utils.ts b/src/workers/notifications/utils.ts index 6d1a0e006d..4d93fc0755 100644 --- a/src/workers/notifications/utils.ts +++ b/src/workers/notifications/utils.ts @@ -246,20 +246,20 @@ export async function articleNewCommentHandler( } export const UPVOTE_TITLES = { - 1: '1 upvote. Your first! 🎉', - 3: '3 upvotes. Off to a good start ✨', - 5: '5 upvotes. People are reading 👀', - 10: '10 upvotes. Double digits 🙌', - 20: '20 upvotes. People are noticing 🥳', - 50: '50 upvotes. Your post is trending 🔥', - 100: '100 upvotes. Triple digits ⚡️', - 200: '200 upvotes. This one took off 🚀', - 500: '500 upvotes. Going viral 📈', - 1000: '1,000 upvotes. Legendary 💎', - 2000: '2,000 upvotes. Top of the charts 💥', - 5000: '5,000 upvotes. One for the books 🏆', + 1: '1 upvote! Off to a good start 🎉', + 3: '3 upvotes. No bugs, just vibes ✨', + 5: '5 upvotes! Gaining traction 👀', + 10: '10 upvotes! New high score 🙌', + 20: '20 upvotes! Level up 🥳', + 50: '50 upvotes! Trending on the feed 🔥', + 100: '100 upvotes! Critical hit ⚡️', + 200: '200 upvotes! This one took off 🚀', + 500: '500 upvotes! Going viral 📈', + 1000: '1,000 upvotes! Legendary unlocked 💎', + 2000: '2,000 upvotes! Mythic tier 💥', + 5000: '5,000 upvotes! Hall of fame 🏆', 10000: - '10,000 upvotes. History made 👑', + '10,000 upvotes! GOAT 👑', }; export const UPVOTE_MILESTONES = Object.keys(UPVOTE_TITLES); From 60892ef29a26dbecf4d8ad7d99e06ce2c1aee733 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:07:15 +0200 Subject: [PATCH 2/2] fix: consistent punctuation in upvote milestone copy Change "3 upvotes." to "3 upvotes!" to match all other milestones. Made-with: Cursor --- src/workers/notifications/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workers/notifications/utils.ts b/src/workers/notifications/utils.ts index 4d93fc0755..9f8925cf9d 100644 --- a/src/workers/notifications/utils.ts +++ b/src/workers/notifications/utils.ts @@ -247,7 +247,7 @@ export async function articleNewCommentHandler( export const UPVOTE_TITLES = { 1: '1 upvote! Off to a good start 🎉', - 3: '3 upvotes. No bugs, just vibes ✨', + 3: '3 upvotes! No bugs, just vibes ✨', 5: '5 upvotes! Gaining traction 👀', 10: '10 upvotes! New high score 🙌', 20: '20 upvotes! Level up 🥳',