From 849182b2c0ac1e3ecb5ee59f5dffd6d8e02af122 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 12 Jun 2025 09:41:00 +0100 Subject: [PATCH] tip video comment notification --- schema/auth.graphql | 1 + schema/notifications.graphql | 25 ++- .../resolvers/NotificationResolver/index.ts | 4 + .../resolvers/NotificationResolver/types.ts | 7 + src/tests/integration/notifications.test.ts | 149 ++++++++++++++++++ src/utils/notification/notificationsData.ts | 17 +- 6 files changed, 200 insertions(+), 3 deletions(-) diff --git a/schema/auth.graphql b/schema/auth.graphql index 055c3cf6d..2ed9cb783 100644 --- a/schema/auth.graphql +++ b/schema/auth.graphql @@ -161,6 +161,7 @@ type AccountNotificationPreferences { channelReceivedFundsFromWg: NotificationPreference! newPayoutUpdatedByCouncil: NotificationPreference! #v2 channelFundsWithdrawn: NotificationPreference! + tipVideoCommentCreated: NotificationPreference! # member notifications: https://www.figma.com/file/yhZpTHdf1sxJx13uRZ71GV/Membership-profile?type=design&node-id=2977-58785&mode=design channelCreated: NotificationPreference! diff --git a/schema/notifications.graphql b/schema/notifications.graphql index 411407e28..8acc8f9fd 100644 --- a/schema/notifications.graphql +++ b/schema/notifications.graphql @@ -124,7 +124,7 @@ union NotificationType = | CreatorTokenRevenueShareStarted | CreatorTokenRevenueSharePlanned | CreatorTokenRevenueShareEnded - + | TipCommentPostedToVideo # tip video comment created type ChannelSuspended @variant { phantom: Int } @@ -587,3 +587,26 @@ type CreatorTokenRevenueShareEnded @variant { "id of token" tokenId: String! } + +type TipCommentPostedToVideo @variant { + "video title used for notification text" + videoTitle: String! + + "video Id used for link" + videoId: String! + + "commenter id for the avatar" + memberId: String! + + "commenter handle for text" + memberHandle: String! + + "id for the comment used for the link" + commentId: String! + + "Tier received for adding a tip to the comment (if any)" + tipTier: CommentTipTier + + "Tip included when adding the comment (in HAPI)" + tipAmount: BigInt! +} diff --git a/src/server-extension/resolvers/NotificationResolver/index.ts b/src/server-extension/resolvers/NotificationResolver/index.ts index 8d90348ee..a237b3721 100644 --- a/src/server-extension/resolvers/NotificationResolver/index.ts +++ b/src/server-extension/resolvers/NotificationResolver/index.ts @@ -188,6 +188,10 @@ export class NotificationResolver { newPreferences.fundsFromWgReceived, account.notificationPreferences.fundsFromWgReceived ) + maybeUpdateNotificationPreference( + newPreferences.tipVideoCommentCreated, + account.notificationPreferences.tipVideoCommentCreated + ) await em.save(account) return toOutputGQL(account.notificationPreferences) diff --git a/src/server-extension/resolvers/NotificationResolver/types.ts b/src/server-extension/resolvers/NotificationResolver/types.ts index 4ce02249f..2984074cd 100644 --- a/src/server-extension/resolvers/NotificationResolver/types.ts +++ b/src/server-extension/resolvers/NotificationResolver/types.ts @@ -87,6 +87,9 @@ export class AccountNotificationPreferencesInput { @Field(() => NotificationPreferenceGQL, { nullable: true }) channelFundsWithdrawn: NotificationPreference + @Field(() => NotificationPreferenceGQL, { nullable: true }) + tipVideoCommentCreated: NotificationPreference + // member @Field(() => NotificationPreferenceGQL, { nullable: true }) @@ -190,6 +193,9 @@ export class AccountNotificationPreferencesOutput @Field(() => NotificationPreferenceOutput, { nullable: true }) channelFundsWithdrawn: NotificationPreference + @Field(() => NotificationPreferenceOutput, { nullable: true }) + tipVideoCommentCreated: NotificationPreference + // member @Field(() => NotificationPreferenceOutput, { nullable: true }) @@ -277,5 +283,6 @@ export function toOutputGQL( fundsFromCouncilReceived: preferences.fundsFromCouncilReceived, fundsToExternalWalletSent: preferences.fundsToExternalWalletSent, fundsFromWgReceived: preferences.fundsFromWgReceived, + tipVideoCommentCreated: preferences.tipVideoCommentCreated, } } diff --git a/src/tests/integration/notifications.test.ts b/src/tests/integration/notifications.test.ts index 6ff931146..81cf8bb74 100644 --- a/src/tests/integration/notifications.test.ts +++ b/src/tests/integration/notifications.test.ts @@ -27,6 +27,7 @@ import { OwnedNft, Video, VideoLiked, + TipCommentPostedToVideo, } from '../../model' import { setFeaturedNftsInner } from '../../server-extension/resolvers/AdminResolver' import { @@ -547,4 +548,152 @@ describe('notifications tests', () => { }) }) }) + + describe('πŸ‘‰Tip Comment Posted To Video', () => { + let nextNotificationIdPre: number + let notificationId: string + const block = { timestamp: 123456 } as any + const indexInBlock = 1 + const extrinsicHash = '0x1234567890abcdef' + const commentId = backwardCompatibleMetaID(block, indexInBlock) + const videoId = '1' + const tipAmount = BigInt(100000) + let video: Video + let event: any + const metadataMessage: IMemberRemarked = { + createComment: { + videoId: Long.fromNumber(1), + parentCommentId: null, + body: 'test', + }, + } + before(async () => { + nextNotificationIdPre = await getNextNotificationId(overlay, true) + notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString() + video = await em + .getRepository(Video) + .findOneOrFail({ where: { id: videoId }, relations: { channel: true } }) + event = { + isV2001: true, + asV2001: [ + '2', + metadataToBytes(MemberRemarked, metadataMessage), + [video.channel.rewardAccount, tipAmount], + ], // avoid comment author == creator + } + }) + it('should process comment to video and deposit notification', async () => { + await processMemberRemarkedEvent({ + overlay, + block, + indexInBlock, + extrinsicHash, + event, + }) + + const nextNotificationId = await getNextNotificationId(overlay, true) + notification = (await overlay + .getRepository(Notification) + .getByIdOrFail(notificationId)) as Notification | null + + it('notification type is comment posted to video', () => { + expect(notification).not.to.be.null + expect(notification!.notificationType.isTypeOf).to.equal('TipCommentPostedToVideo') + }) + it('notification data for comment posted to video should be ok', () => { + const notificationData = notification!.notificationType as TipCommentPostedToVideo + expect(notificationData.videoId).to.equal('1') + expect(notificationData.commentId).to.equal(commentId) + expect(notificationData.memberHandle).to.equal('handle-2') + expect(notificationData.videoTitle).to.equal('test-video-1') + expect(notificationData.tipAmount).to.equal(tipAmount) + }) + + it('general notification creation setting should be as default', () => { + expect(notification!.status.isTypeOf).to.equal('Unread') + expect(notification!.inApp).to.be.true + expect(nextNotificationId.toString()).to.equal((nextNotificationIdPre + 1).toString()) + expect(notification!.recipient.isTypeOf).to.equal('ChannelRecipient') + }) + it('notification email entity should be correctly deposited on overlay', async () => { + const notificationEmailDelivery = (await overlay + .getRepository(NotificationEmailDelivery) + .getOneByRelation('notificationId', notificationId)) as NotificationEmailDelivery | null + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.empty + }) + }) + describe('πŸ‘‰ Reply To Comment', () => { + let nextNotificationIdPre: number + let notificationId: string + const block = { timestamp: 123457 } as any + const indexInBlock = 1 + const metadataMessage = { + createComment: { + videoId: Long.fromNumber(1), + parentCommentId: commentId, + body: 'reply test', + }, + } + const event = { + isV2001: true, + asV2001: ['3', metadataToBytes(MemberRemarked, metadataMessage!), undefined], + } as any + + before(async () => { + nextNotificationIdPre = await getNextNotificationId(overlay, true) + notificationId = RUNTIME_NOTIFICATION_ID_TAG + '-' + nextNotificationIdPre.toString() + + await processMemberRemarkedEvent({ + overlay, + block, + indexInBlock, + extrinsicHash, + event, + }) + }) + + describe('should process reply to comment and deposit notification', () => { + let nextNotificationId: number + before(async () => { + nextNotificationId = await getNextNotificationId(overlay, true) + notification = (await overlay + .getRepository(Notification) + .getByIdOrFail(notificationId)) as Notification | null + }) + + it('notification type is reply to comment', () => { + expect(notification).not.to.be.null + expect(notification!.notificationType.isTypeOf).to.equal('CommentReply') + expect(notification?.accountId).to.equal('2') + }) + it('notification data for comment reply should be ok', () => { + const notificationData = notification!.notificationType as CommentReply + expect(notificationData.videoId).to.equal('1') + expect(notificationData.memberHandle).to.equal('handle-3') + expect(notificationData.commentId).to.equal(backwardCompatibleMetaID(block, indexInBlock)) + expect(notificationData.videoTitle).to.equal('test-video-1') + expect(notification!.recipient.isTypeOf).to.equal('MemberRecipient') + expect((notification!.recipient as MemberRecipient).membership).to.equal( + '2', + 'member recipient should be parent comment author' + ) + }) + it('general notification creation setting should be as default', () => { + expect(notification!.status.isTypeOf).to.equal('Unread') + expect(notification!.inApp).to.be.true + expect(nextNotificationId.toString()).to.equal((nextNotificationIdPre + 1).toString()) + }) + it('notification email entity should be correctly deposited on overlay', async () => { + const notificationEmailDelivery = (await overlay + .getRepository(NotificationEmailDelivery) + .getOneByRelation('notificationId', notificationId)) as NotificationEmailDelivery | null + expect(notificationEmailDelivery).not.to.be.null + expect(notificationEmailDelivery!.discard).to.be.false + expect(notificationEmailDelivery!.attempts).to.be.empty + }) + }) + }) + }) }) diff --git a/src/utils/notification/notificationsData.ts b/src/utils/notification/notificationsData.ts index bd209be7a..4dd08b5df 100644 --- a/src/utils/notification/notificationsData.ts +++ b/src/utils/notification/notificationsData.ts @@ -174,15 +174,28 @@ export const getNotificationData = async ( } } case 'CommentPostedToVideo': { - const { videoId, videoTitle, memberId, memberHandle } = notificationType + const { videoId, videoTitle, memberId, memberHandle, comentId } = notificationType return { icon: await getNotificationIcon(em, 'follow'), - link: await getNotificationLink(em, 'nft-page', [videoId]), + link: await getNotificationLink(em, 'video-page', [videoId, comentId]), avatar: await getNotificationAvatar(em, 'membershipId', memberId), text: `πŸ’¬ ${memberHandle} left a comment on your video: β€œ${videoTitle}”`, subject: `πŸ’¬ ${memberHandle} left a comment on your video: β€œ${videoTitle}”`, } } + case 'TipCommentPostedToVideo': { + const { videoId, videoTitle, memberId, memberHandle, commentId, tipAmount } = notificationType + return { + icon: await getNotificationIcon(em, 'follow'), + link: await getNotificationLink(em, 'video-page', [videoId, commentId]), + avatar: await getNotificationAvatar(em, 'membershipId', memberId), + text: `πŸ’¬ You received (${convertHapiToUSD(tipAmount) ?? '-'}$) ${formatJOY( + tipAmount + )} JOY from ${memberHandle} under your video: β€œ${videoTitle}”`, + // You received {tipAmount} JOY tip from {memberHandle} under your video: β€œ{videoTitle}” + subject: `πŸ’¬ ${memberHandle} left a comment on your video: β€œ${videoTitle}”`, + } + } case 'VideoLiked': { const { videoId, videoTitle, memberId, memberHandle } = notificationType return {