From 371d5f6a90471c2e974d495d01b8a5cf062c0024 Mon Sep 17 00:00:00 2001 From: Igor Lesnenko Date: Tue, 9 Jan 2024 15:09:03 +0800 Subject: [PATCH] feat(kudos): send kudos by text in standups (#9259) * feat(kudos): send kudos by text in standups * Remove console logs * Fix test * Store unicode emoji too * Link teamPromptResponseId * Update slack notification * Update email notification * Mention notification analytics * response mentioned toast analytics * isValid * Add types --- .../components/KudosReceivedNotification.tsx | 9 +- .../client/components/ResponseMentioned.tsx | 24 ++- .../EmailResponseMentioned.tsx | 9 +- .../AddReactjiToReactableMutation.ts | 7 +- .../UpsertTeamPromptResponseMutation.ts | 32 ++- .../toasts/mapKudosReceivedToToast.ts | 8 +- .../toasts/mapResponseMentionedToToast.ts | 24 ++- .../types/NotificationKudosReceived.ts | 5 +- .../types/NotificationResponseMentioned.ts | 8 +- .../helpers/notifications/SlackNotifier.ts | 5 +- .../public/mutations/addReactjiToReactable.ts | 4 +- .../__tests__/getKudosUserIdsFromJson.test.ts | 183 ++++++++++++++++++ .../helpers/getKudosUserIdsFromJson.ts | 29 +++ .../helpers/publishTeamPromptMentions.ts | 35 +++- .../mutations/upsertTeamPromptResponse.ts | 50 ++++- .../graphql/public/typeDefs/Kudos.graphql | 5 + .../typeDefs/NotifyKudosReceived.graphql | 5 + .../typeDefs/NotifyResponseMentioned.graphql | 10 + .../typeDefs/upsertTeamPromptResponse.graphql | 5 + .../public/types/NotifyKudosReceived.ts | 3 +- .../types/UpsertTeamPromptResponseSuccess.ts | 10 + ...1701679264301_storeUnicodeEmojiForKudos.ts | 34 ++++ ...1701769672206_kudosTeamPromptResponseId.ts | 29 +++ 23 files changed, 492 insertions(+), 41 deletions(-) create mode 100644 packages/server/graphql/public/mutations/helpers/__tests__/getKudosUserIdsFromJson.test.ts create mode 100644 packages/server/graphql/public/mutations/helpers/getKudosUserIdsFromJson.ts create mode 100644 packages/server/postgres/migrations/1701679264301_storeUnicodeEmojiForKudos.ts create mode 100644 packages/server/postgres/migrations/1701769672206_kudosTeamPromptResponseId.ts diff --git a/packages/client/components/KudosReceivedNotification.tsx b/packages/client/components/KudosReceivedNotification.tsx index bf3e96ba0b7..b0dc13e4168 100644 --- a/packages/client/components/KudosReceivedNotification.tsx +++ b/packages/client/components/KudosReceivedNotification.tsx @@ -6,7 +6,6 @@ import {KudosReceivedNotification_notification$key} from '~/__generated__/KudosR import NotificationTemplate from './NotificationTemplate' import useAtmosphere from '../hooks/useAtmosphere' import SendClientSideEvent from '../utils/SendClientSideEvent' -import getReactji from '~/utils/getReactji' interface Props { notification: KudosReceivedNotification_notification$key @@ -25,15 +24,13 @@ const KudosReceivedNotification = (props: Props) => { picture meetingName meetingId - emoji + emojiUnicode status } `, notificationRef ) - const {type, name, picture, meetingName, emoji, meetingId, status} = notification - - const {unicode} = getReactji(emoji) + const {type, name, picture, meetingName, emojiUnicode, meetingId, status} = notification useEffect(() => { SendClientSideEvent(atmosphere, 'Notification Viewed', { @@ -46,7 +43,7 @@ const KudosReceivedNotification = (props: Props) => { - {unicode} {name} gave you kudos in{' '} + {emojiUnicode} {name} gave you kudos in{' '} {meetingName} diff --git a/packages/client/components/ResponseMentioned.tsx b/packages/client/components/ResponseMentioned.tsx index bbf5289da12..e948edc9388 100644 --- a/packages/client/components/ResponseMentioned.tsx +++ b/packages/client/components/ResponseMentioned.tsx @@ -1,10 +1,12 @@ import graphql from 'babel-plugin-relay/macro' -import React from 'react' +import React, {useEffect} from 'react' import {useFragment} from 'react-relay' import NotificationAction from '~/components/NotificationAction' import useRouter from '../hooks/useRouter' import {ResponseMentioned_notification$key} from '../__generated__/ResponseMentioned_notification.graphql' import NotificationTemplate from './NotificationTemplate' +import SendClientSideEvent from '../utils/SendClientSideEvent' +import useAtmosphere from '~/hooks/useAtmosphere' interface Props { notification: ResponseMentioned_notification$key @@ -27,24 +29,40 @@ const ResponseMentioned = (props: Props) => { id name } + type + status + kudosEmojiUnicode } `, notificationRef ) const {history} = useRouter() - const {meeting, response} = notification + const atmosphere = useAtmosphere() + const {meeting, response, kudosEmojiUnicode, type, status} = notification const {picture: authorPicture, preferredName: authorName} = response.user + useEffect(() => { + SendClientSideEvent(atmosphere, 'Notification Viewed', { + notificationType: type, + notificationStatus: status, + kudosEmojiUnicode + }) + }, []) + const {id: meetingId, name: meetingName} = meeting const goThere = () => { history.push(`/meet/${meetingId}/responses?responseId=${encodeURIComponent(response.id)}`) } + const message = kudosEmojiUnicode + ? `${kudosEmojiUnicode} ${authorName} mentioned you and gave kudos in their response in ${meetingName}.` + : `${authorName} mentioned you in their response in ${meetingName}.` + // :TODO: (jmtaber129): Show mention preview. return ( } /> diff --git a/packages/client/modules/email/components/EmailNotifications/EmailResponseMentioned.tsx b/packages/client/modules/email/components/EmailNotifications/EmailResponseMentioned.tsx index 4c23c81cc7b..dec6b3acf0c 100644 --- a/packages/client/modules/email/components/EmailNotifications/EmailResponseMentioned.tsx +++ b/packages/client/modules/email/components/EmailNotifications/EmailResponseMentioned.tsx @@ -28,11 +28,12 @@ const EmailResponseMentioned = (props: Props) => { id name } + kudosEmojiUnicode } `, notificationRef ) - const {meeting, response} = notification + const {meeting, response, kudosEmojiUnicode} = notification const {rasterPicture: authorPicture, preferredName: authorName} = response.user const {id: meetingId, name: meetingName} = meeting @@ -46,11 +47,15 @@ const EmailResponseMentioned = (props: Props) => { } }) + const message = kudosEmojiUnicode + ? `${kudosEmojiUnicode} ${authorName} mentioned you and gave kudos in their response in ${meetingName}.` + : `${authorName} mentioned you in their response in ${meetingName}.` + // :TODO: (jmtaber129): Show mention preview. return ( { SendClientSideEvent(atmosphere, 'Snackbar Viewed', { diff --git a/packages/client/mutations/UpsertTeamPromptResponseMutation.ts b/packages/client/mutations/UpsertTeamPromptResponseMutation.ts index dc3aaadaa02..46e757d24a1 100644 --- a/packages/client/mutations/UpsertTeamPromptResponseMutation.ts +++ b/packages/client/mutations/UpsertTeamPromptResponseMutation.ts @@ -4,6 +4,7 @@ import clientTempId from '~/utils/relay/clientTempId' import {UpsertTeamPromptResponseMutation_meeting$data} from '~/__generated__/UpsertTeamPromptResponseMutation_meeting.graphql' import {LocalHandlers, SharedUpdater, StandardMutation} from '../types/relayMutations' import {UpsertTeamPromptResponseMutation as TUpsertTeamPromptResponseMutation} from '../__generated__/UpsertTeamPromptResponseMutation.graphql' +import SendClientSideEvent from '../utils/SendClientSideEvent' graphql` fragment UpsertTeamPromptResponseMutation_meeting on UpsertTeamPromptResponseSuccess { @@ -17,6 +18,12 @@ graphql` createdAt ...TeamPromptResponseEmojis_response } + addedKudoses { + receiverUser { + preferredName + } + emojiUnicode + } } ` @@ -97,7 +104,30 @@ const UpsertTeamPromptResponseMutation: StandardMutation< const payload = store.getRootField('upsertTeamPromptResponse') upsertTeamPromptResponseUpdater(payload as any, {atmosphere, store}) }, - onCompleted, + onCompleted: (res, errors) => { + const addedKudoses = res.upsertTeamPromptResponse.addedKudoses + if (addedKudoses?.length && addedKudoses[0]) { + const {emojiUnicode} = addedKudoses[0] + atmosphere.eventEmitter.emit('addSnackbar', { + key: 'youGaveKudos', + message: `You gave kudos to ${addedKudoses + .map((kudos) => kudos.receiverUser.preferredName) + .join(', ')} ${emojiUnicode}`, + autoDismiss: 5, + onShow: () => { + SendClientSideEvent(atmosphere, 'Snackbar Viewed', { + snackbarType: 'kudosSent' + }) + }, + onManualDismiss: () => { + SendClientSideEvent(atmosphere, 'Snackbar Clicked', { + snackbarType: 'kudosSent' + }) + } + }) + } + onCompleted?.(res, errors) + }, onError }) } diff --git a/packages/client/mutations/toasts/mapKudosReceivedToToast.ts b/packages/client/mutations/toasts/mapKudosReceivedToToast.ts index d3660d1b4ec..59e060a237f 100644 --- a/packages/client/mutations/toasts/mapKudosReceivedToToast.ts +++ b/packages/client/mutations/toasts/mapKudosReceivedToToast.ts @@ -4,7 +4,6 @@ import {mapKudosReceivedToToast_notification$data} from '../../__generated__/map import makeNotificationToastKey from './makeNotificationToastKey' import {OnNextHistoryContext} from '../../types/relayMutations' import SendClientSideEvent from '../../utils/SendClientSideEvent' -import getReactji from '~/utils/getReactji' graphql` fragment mapKudosReceivedToToast_notification on NotifyKudosReceived { @@ -12,7 +11,7 @@ graphql` name meetingName meetingId - emoji + emojiUnicode } ` @@ -20,13 +19,12 @@ const mapKudosReceivedToToast = ( notification: mapKudosReceivedToToast_notification$data, {atmosphere, history}: OnNextHistoryContext ): Snack => { - const {id: notificationId, meetingName, name, emoji, meetingId} = notification - const {unicode} = getReactji(emoji) + const {id: notificationId, meetingName, name, emojiUnicode, meetingId} = notification return { autoDismiss: 5, showDismissButton: true, key: makeNotificationToastKey(notificationId), - message: `${unicode} ${name} gave you kudos in`, + message: `${emojiUnicode} ${name} gave you kudos in`, action: { label: meetingName, callback: () => { diff --git a/packages/client/mutations/toasts/mapResponseMentionedToToast.ts b/packages/client/mutations/toasts/mapResponseMentionedToToast.ts index 676f3ce0ec9..b9d2e5cecd1 100644 --- a/packages/client/mutations/toasts/mapResponseMentionedToToast.ts +++ b/packages/client/mutations/toasts/mapResponseMentionedToToast.ts @@ -3,6 +3,7 @@ import {Snack} from '../../components/Snackbar' import {OnNextHistoryContext} from '../../types/relayMutations' import {mapResponseMentionedToToast_notification$data} from '../../__generated__/mapResponseMentionedToToast_notification.graphql' import makeNotificationToastKey from './makeNotificationToastKey' +import SendClientSideEvent from '../../utils/SendClientSideEvent' graphql` fragment mapResponseMentionedToToast_notification on NotifyResponseMentioned { @@ -17,30 +18,47 @@ graphql` id name } + kudosEmojiUnicode } ` const mapResponseMentionedToToast = ( notification: mapResponseMentionedToToast_notification$data, - {history}: OnNextHistoryContext + {atmosphere, history}: OnNextHistoryContext ): Snack | null => { if (!notification) return null - const {id: notificationId, meeting, response} = notification + const {id: notificationId, meeting, response, kudosEmojiUnicode} = notification const {preferredName: authorName} = response.user const {id: meetingId, name: meetingName} = meeting + const message = kudosEmojiUnicode + ? `${kudosEmojiUnicode} ${authorName} mentioned you and gave kudos in their response in ${meetingName}.` + : `${authorName} mentioned you in their response in ${meetingName}.` + // :TODO: (jmtaber129): Check if we're already open to the relevant standup response discussion // thread, and do nothing if we are. return { key: makeNotificationToastKey(notificationId), autoDismiss: 10, - message: `${authorName} mentioned you in their response in ${meetingName}.`, + message, action: { label: 'See their response', callback: () => { history.push(`/meet/${meetingId}/responses?responseId=${encodeURIComponent(response.id)}`) } + }, + onShow: () => { + SendClientSideEvent(atmosphere, 'Snackbar Viewed', { + snackbarType: 'responseMentioned', + kudosEmojiUnicode + }) + }, + onManualDismiss: () => { + SendClientSideEvent(atmosphere, 'Snackbar Clicked', { + snackbarType: 'responseMentioned', + kudosEmojiUnicode + }) } } } diff --git a/packages/server/database/types/NotificationKudosReceived.ts b/packages/server/database/types/NotificationKudosReceived.ts index 9cdeef4a6b2..b8f8df1806b 100644 --- a/packages/server/database/types/NotificationKudosReceived.ts +++ b/packages/server/database/types/NotificationKudosReceived.ts @@ -8,6 +8,7 @@ interface Input { meetingName: string meetingId: string emoji: string + emojiUnicode: string } export default class NotificationKudosReceived extends Notification { @@ -18,9 +19,10 @@ export default class NotificationKudosReceived extends Notification { meetingName: string meetingId: string emoji: string + emojiUnicode: string constructor(input: Input) { - const {userId, name, picture, senderUserId, meetingName, meetingId, emoji} = input + const {userId, name, picture, senderUserId, meetingName, meetingId, emoji, emojiUnicode} = input super({userId, type: 'KUDOS_RECEIVED'}) this.name = name this.picture = picture @@ -28,5 +30,6 @@ export default class NotificationKudosReceived extends Notification { this.meetingName = meetingName this.meetingId = meetingId this.emoji = emoji + this.emojiUnicode = emojiUnicode } } diff --git a/packages/server/database/types/NotificationResponseMentioned.ts b/packages/server/database/types/NotificationResponseMentioned.ts index 3368b54babb..562f40d1321 100644 --- a/packages/server/database/types/NotificationResponseMentioned.ts +++ b/packages/server/database/types/NotificationResponseMentioned.ts @@ -4,17 +4,23 @@ interface Input { responseId: string meetingId: string userId: string + kudosEmoji?: string | null + kudosEmojiUnicode?: string | null } export default class NotificationResponseMentioned extends Notification { readonly type = 'RESPONSE_MENTIONED' responseId: string meetingId: string + kudosEmoji?: string | null + kudosEmojiUnicode?: string | null constructor(input: Input) { - const {responseId, meetingId, userId} = input + const {responseId, meetingId, userId, kudosEmoji, kudosEmojiUnicode} = input super({userId, type: 'RESPONSE_MENTIONED'}) this.responseId = responseId this.meetingId = meetingId + this.kudosEmoji = kudosEmoji + this.kudosEmojiUnicode = kudosEmojiUnicode } } diff --git a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts index a07a5487edb..d63998ff744 100644 --- a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts @@ -271,9 +271,12 @@ const getSlackMessageForNotification = async ( const responseId = notification.responseId const response = await dataLoader.get('teamPromptResponses').loadNonNull(responseId) const author = await dataLoader.get('users').loadNonNull(response.userId) + const title = notification.kudosEmojiUnicode + ? `${notification.kudosEmojiUnicode} *${author.preferredName}* mentioned you and gave kudos in their response in *${meeting.name}*` + : `*${author.preferredName}* mentioned you in their response in *${meeting.name}*` return { responseId, - title: `*${author.preferredName}* mentioned you in their response in *${meeting.name}*`, + title, buttonText: 'See their response' } } diff --git a/packages/server/graphql/public/mutations/addReactjiToReactable.ts b/packages/server/graphql/public/mutations/addReactjiToReactable.ts index 360e9f8ad9d..c2c69a836cc 100644 --- a/packages/server/graphql/public/mutations/addReactjiToReactable.ts +++ b/packages/server/graphql/public/mutations/addReactjiToReactable.ts @@ -190,7 +190,8 @@ const addReactjiToReactable: MutationResolvers['addReactjiToReactable'] = async reactableType: reactableType, reactableId: reactableId, teamId, - emoji: team.kudosEmoji + emoji: team.kudosEmoji, + emojiUnicode: team.kudosEmojiUnicode }) .returning('id') .executeTakeFirst())!.id @@ -203,6 +204,7 @@ const addReactjiToReactable: MutationResolvers['addReactjiToReactable'] = async meetingId, meetingName: meeting.name, emoji: team.kudosEmoji, + emojiUnicode: team.kudosEmojiUnicode, name: senderUser.preferredName, picture: senderUser.picture }) diff --git a/packages/server/graphql/public/mutations/helpers/__tests__/getKudosUserIdsFromJson.test.ts b/packages/server/graphql/public/mutations/helpers/__tests__/getKudosUserIdsFromJson.test.ts new file mode 100644 index 00000000000..33f70740766 --- /dev/null +++ b/packages/server/graphql/public/mutations/helpers/__tests__/getKudosUserIdsFromJson.test.ts @@ -0,0 +1,183 @@ +import {getKudosUserIdsFromJson} from '../getKudosUserIdsFromJson' +import {JSONContent} from '@tiptap/core' + +describe('findMentionsByEmoji', () => { + let doc: JSONContent + + beforeAll(() => { + doc = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Paragraph1' + } + ] + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Paragraph2' + } + ] + }, + { + type: 'paragraph' + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Paragraph from new line' + } + ] + }, + { + type: 'paragraph' + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Paragraph from new line' + }, + { + type: 'hardBreak' + }, + { + type: 'text', + text: 'and break in the same paragraph' + } + ] + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Just new line with mention ' + }, + { + type: 'mention', + attrs: { + id: 'user_id_1', + label: 'userone' + } + }, + { + type: 'text', + text: ' ❤️' + } + ] + }, + { + type: 'paragraph' + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: '❤️ Another mentions ' + }, + { + type: 'mention', + attrs: { + id: 'user_id_2', + label: 'usertwo' + } + }, + { + type: 'hardBreak' + }, + { + type: 'mention', + attrs: { + id: 'user_id_3', + label: 'userthree' + } + } + ] + }, + { + type: 'paragraph', + content: [ + { + type: 'mention', + attrs: { + id: 'user_id_supermention', + label: 'supermention' + } + }, + { + type: 'text', + text: ' 🌮' + } + ] + }, + { + type: 'paragraph' + }, + { + type: 'paragraph', + content: [ + { + type: 'mention', + attrs: { + id: 'user_id_4', + label: 'userone' + } + }, + { + type: 'text', + text: ' ' + }, + { + type: 'mention', + attrs: { + id: 'user_id_5', + label: 'userfour' + } + }, + { + type: 'text', + text: ' both mentioned ❤️' + } + ] + } + ] + } + }) + + it('returns correct mention user IDs for emoji ❤️', () => { + const emoji = '❤️' + const result = getKudosUserIdsFromJson(doc, emoji) + expect(result).toEqual(['user_id_1', 'user_id_2', 'user_id_3', 'user_id_4', 'user_id_5']) + }) + + it('returns correct mention user IDs for different emoji (🌮)', () => { + const emoji = '🌮' + const result = getKudosUserIdsFromJson(doc, emoji) + expect(result).toEqual(['user_id_supermention']) + }) + + it('returns an empty array for an emoji with no mentions (🔥)', () => { + const emoji = '🔥' + const result = getKudosUserIdsFromJson(doc, emoji) + expect(result).toEqual([]) + }) + + it('does not include duplicate IDs', () => { + const emoji = '❤️' + const result = getKudosUserIdsFromJson(doc, emoji) + const uniqueResult = Array.from(new Set(result)) + expect(result).toEqual(uniqueResult) + }) +}) diff --git a/packages/server/graphql/public/mutations/helpers/getKudosUserIdsFromJson.ts b/packages/server/graphql/public/mutations/helpers/getKudosUserIdsFromJson.ts new file mode 100644 index 00000000000..42e9d08fe41 --- /dev/null +++ b/packages/server/graphql/public/mutations/helpers/getKudosUserIdsFromJson.ts @@ -0,0 +1,29 @@ +import {JSONContent} from '@tiptap/core' + +export const getKudosUserIdsFromJson = (doc: JSONContent, emoji: string): string[] => { + const mentionedIds = new Set() + + if (!doc.content) return [] + for (const paragraph of doc.content) { + if (paragraph.content) { + let emojiFound = false + const tempMentions = new Set() + + for (const node of paragraph.content) { + if (node.type === 'text' && node.text?.includes(emoji)) { + emojiFound = true + } + + if (node.type === 'mention') { + tempMentions.add(node.attrs?.id) + } + } + + if (emojiFound) { + tempMentions.forEach((id) => mentionedIds.add(id)) + } + } + } + + return Array.from(mentionedIds) +} diff --git a/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts b/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts index 8c43d1a37b3..b48fd4e37e7 100644 --- a/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts +++ b/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts @@ -17,17 +17,30 @@ const getMentionedUserIdsFromContent = (content: JSONContent): string[] => { const createTeamPromptMentionNotifications = async ( oldResponse: TeamPromptResponse | undefined, - newResponse: TeamPromptResponse + newResponse: TeamPromptResponse, + addedKudoses: + | { + id: number + emoji: string | null + emojiUnicode: string | null + receiverUserId: string + }[] + | null ) => { // Get mentions from previous and new content. const newResponseMentions = getMentionedUserIdsFromContent(newResponse.content) const oldResponseMentions = oldResponse ? getMentionedUserIdsFromContent(oldResponse.content) : [] + const addedKudosesUserIds = addedKudoses?.map((kudos) => kudos.receiverUserId) ?? [] + // Create notifications that should be added. const addedMentions = Array.from( new Set( newResponseMentions.filter( - (mention) => !oldResponseMentions.includes(mention) && newResponse.userId !== mention + (mention) => + (!oldResponseMentions.includes(mention) && newResponse.userId !== mention) || + // Send mention notification anyway in case it is also include kudos + addedKudosesUserIds.includes(mention) ) ) ) @@ -36,14 +49,16 @@ const createTeamPromptMentionNotifications = async ( return [] } - const notificationsToAdd = addedMentions.map( - (mention) => - new NotificationResponseMentioned({ - userId: mention, - responseId: newResponse.id, - meetingId: newResponse.meetingId - }) - ) + const notificationsToAdd = addedMentions.map((mention) => { + const kudos = addedKudoses?.find((kudos) => kudos.receiverUserId === mention) + return new NotificationResponseMentioned({ + userId: mention, + responseId: newResponse.id, + meetingId: newResponse.meetingId, + kudosEmoji: kudos?.emoji, + kudosEmojiUnicode: kudos?.emojiUnicode + }) + }) const r = await getRethink() await r.table('Notification').insert(notificationsToAdd).run() diff --git a/packages/server/graphql/public/mutations/upsertTeamPromptResponse.ts b/packages/server/graphql/public/mutations/upsertTeamPromptResponse.ts index c274ec36c76..4426f00ebe3 100644 --- a/packages/server/graphql/public/mutations/upsertTeamPromptResponse.ts +++ b/packages/server/graphql/public/mutations/upsertTeamPromptResponse.ts @@ -12,12 +12,15 @@ import {MutationResolvers} from '../resolverTypes' import publishNotification from './helpers/publishNotification' import createTeamPromptMentionNotifications from './helpers/publishTeamPromptMentions' import {IntegrationNotifier} from '../../mutations/helpers/notifications/IntegrationNotifier' +import {getKudosUserIdsFromJson} from './helpers/getKudosUserIdsFromJson' +import getKysely from '../../../postgres/getKysely' const upsertTeamPromptResponse: MutationResolvers['upsertTeamPromptResponse'] = async ( _source, {teamPromptResponseId: inputTeamPromptResponseId, meetingId, content}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -78,6 +81,47 @@ const upsertTeamPromptResponse: MutationResolvers['upsertTeamPromptResponse'] = }) ) + const team = await dataLoader.get('teams').loadNonNull(teamId) + const {kudosEmoji, kudosEmojiUnicode} = team + + let insertedKudoses: + | { + id: number + receiverUserId: string + emoji: string | null + emojiUnicode: string + }[] + | null = null + if (team.giveKudosWithEmoji && kudosEmojiUnicode) { + const oldKudosUserIds = oldTeamPromptResponse + ? getKudosUserIdsFromJson(oldTeamPromptResponse.content, kudosEmojiUnicode) + : [] + const newKudosUserIds = getKudosUserIdsFromJson(contentJSON, kudosEmojiUnicode) + const kudosUserIds = newKudosUserIds.filter( + (userId) => !oldKudosUserIds.includes(userId) && userId !== viewerId + ) + if (kudosUserIds.length) { + const kudosRows = kudosUserIds.map((userId) => ({ + senderUserId: viewerId, + receiverUserId: userId, + teamId, + emoji: kudosEmoji, + emojiUnicode: kudosEmojiUnicode, + teamPromptResponseId: TeamPromptResponseId.split(teamPromptResponseId) + })) + + insertedKudoses = await pg + .insertInto('Kudos') + .values(kudosRows) + .returning(['id', 'receiverUserId', 'emoji', 'emojiUnicode']) + .execute() + + insertedKudoses.forEach((kudos) => { + analytics.kudosSent(viewerId, teamId, kudos.id, kudos.receiverUserId) + }) + } + } + dataLoader.get('teamPromptResponses').clear(teamPromptResponseId) const newTeamPromptResponse = await dataLoader @@ -86,13 +130,15 @@ const upsertTeamPromptResponse: MutationResolvers['upsertTeamPromptResponse'] = const notifications = await createTeamPromptMentionNotifications( oldTeamPromptResponse, - newTeamPromptResponse + newTeamPromptResponse, + insertedKudoses ) const data = { meetingId, teamPromptResponseId, - addedNotificationIds: notifications.map((notification) => notification.id) + addedNotificationIds: notifications.map((notification) => notification.id), + addedKudosesIds: insertedKudoses?.map((row) => row.id) } notifications.forEach((notification) => { diff --git a/packages/server/graphql/public/typeDefs/Kudos.graphql b/packages/server/graphql/public/typeDefs/Kudos.graphql index 80ea9f5385c..89f72c7805e 100644 --- a/packages/server/graphql/public/typeDefs/Kudos.graphql +++ b/packages/server/graphql/public/typeDefs/Kudos.graphql @@ -21,4 +21,9 @@ type Kudos { emoji name """ emoji: String! + + """ + emoji unicode character + """ + emojiUnicode: String! } diff --git a/packages/server/graphql/public/typeDefs/NotifyKudosReceived.graphql b/packages/server/graphql/public/typeDefs/NotifyKudosReceived.graphql index 71035ba3289..b1df328ef79 100644 --- a/packages/server/graphql/public/typeDefs/NotifyKudosReceived.graphql +++ b/packages/server/graphql/public/typeDefs/NotifyKudosReceived.graphql @@ -44,4 +44,9 @@ type NotifyKudosReceived implements Notification { Kudos emoji """ emoji: String! + + """ + Kudos emoji unicode + """ + emojiUnicode: String! } diff --git a/packages/server/graphql/public/typeDefs/NotifyResponseMentioned.graphql b/packages/server/graphql/public/typeDefs/NotifyResponseMentioned.graphql index cf2bb5e4d5b..6af90db36ef 100644 --- a/packages/server/graphql/public/typeDefs/NotifyResponseMentioned.graphql +++ b/packages/server/graphql/public/typeDefs/NotifyResponseMentioned.graphql @@ -39,4 +39,14 @@ type NotifyResponseMentioned implements Notification { The meeting the user was mentioned in. """ meeting: TeamPromptMeeting! + + """ + Kudos emoji if mention includes kudos + """ + kudosEmoji: String + + """ + kudos emoji unicode character + """ + kudosEmojiUnicode: String } diff --git a/packages/server/graphql/public/typeDefs/upsertTeamPromptResponse.graphql b/packages/server/graphql/public/typeDefs/upsertTeamPromptResponse.graphql index 7681dc4cd46..d0cbd048545 100644 --- a/packages/server/graphql/public/typeDefs/upsertTeamPromptResponse.graphql +++ b/packages/server/graphql/public/typeDefs/upsertTeamPromptResponse.graphql @@ -32,6 +32,11 @@ type UpsertTeamPromptResponseSuccess { the updated meeting """ meeting: NewMeeting + + """ + Kudos added with the response + """ + addedKudoses: [Kudos!] } union UpsertTeamPromptResponsePayload = UpsertTeamPromptResponseSuccess | ErrorPayload diff --git a/packages/server/graphql/public/types/NotifyKudosReceived.ts b/packages/server/graphql/public/types/NotifyKudosReceived.ts index cbf2232a7bb..a26a1ff78a8 100644 --- a/packages/server/graphql/public/types/NotifyKudosReceived.ts +++ b/packages/server/graphql/public/types/NotifyKudosReceived.ts @@ -1,7 +1,8 @@ import {NotifyKudosReceivedResolvers} from '../resolverTypes' const NotifyKudosReceived: NotifyKudosReceivedResolvers = { - __isTypeOf: ({type}) => type === 'KUDOS_RECEIVED' + __isTypeOf: ({type}) => type === 'KUDOS_RECEIVED', + emojiUnicode: ({emojiUnicode}) => emojiUnicode ?? '❤️' } export default NotifyKudosReceived diff --git a/packages/server/graphql/public/types/UpsertTeamPromptResponseSuccess.ts b/packages/server/graphql/public/types/UpsertTeamPromptResponseSuccess.ts index cf51634ce03..283b97c5e18 100644 --- a/packages/server/graphql/public/types/UpsertTeamPromptResponseSuccess.ts +++ b/packages/server/graphql/public/types/UpsertTeamPromptResponseSuccess.ts @@ -1,8 +1,10 @@ import {UpsertTeamPromptResponseSuccessResolvers} from '../resolverTypes' +import isValid from '../../../graphql/isValid' export type UpsertTeamPromptResponseSuccessSource = { teamPromptResponseId: string meetingId: string + addedKudosesIds?: number[] } const UpsertTeamPromptResponseSuccess: UpsertTeamPromptResponseSuccessResolvers = { @@ -13,6 +15,14 @@ const UpsertTeamPromptResponseSuccess: UpsertTeamPromptResponseSuccessResolvers meeting: async (source, _args, {dataLoader}) => { const {meetingId} = source return dataLoader.get('newMeetings').load(meetingId) + }, + addedKudoses: async (source, _args, {dataLoader}) => { + const {addedKudosesIds} = source + if (!addedKudosesIds) { + return null + } + + return (await dataLoader.get('kudoses').loadMany(addedKudosesIds)).filter(isValid) } } diff --git a/packages/server/postgres/migrations/1701679264301_storeUnicodeEmojiForKudos.ts b/packages/server/postgres/migrations/1701679264301_storeUnicodeEmojiForKudos.ts new file mode 100644 index 00000000000..e0710e27e60 --- /dev/null +++ b/packages/server/postgres/migrations/1701679264301_storeUnicodeEmojiForKudos.ts @@ -0,0 +1,34 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + ALTER TABLE "Team" + ADD COLUMN IF NOT EXISTS "kudosEmojiUnicode" VARCHAR(100) NOT NULL DEFAULT '❤️'; + ALTER TABLE "Kudos" + ADD COLUMN IF NOT EXISTS "emojiUnicode" VARCHAR(100) NOT NULL DEFAULT '❤️'; + END + $$; + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + ALTER TABLE "Team" + DROP COLUMN "kudosEmojiUnicode"; + ALTER TABLE "Kudos" + DROP COLUMN "emojiUnicode"; + END + $$; + `) + await client.end() +} diff --git a/packages/server/postgres/migrations/1701769672206_kudosTeamPromptResponseId.ts b/packages/server/postgres/migrations/1701769672206_kudosTeamPromptResponseId.ts new file mode 100644 index 00000000000..dc2cb4e381a --- /dev/null +++ b/packages/server/postgres/migrations/1701769672206_kudosTeamPromptResponseId.ts @@ -0,0 +1,29 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "Kudos" + DROP CONSTRAINT IF EXISTS "fk_teamPromptResponseId"; + + ALTER TABLE "Kudos" + ADD COLUMN IF NOT EXISTS "teamPromptResponseId" INT, + ADD CONSTRAINT "fk_teamPromptResponseId" + FOREIGN KEY("teamPromptResponseId") + REFERENCES "TeamPromptResponse"("id") + ON DELETE CASCADE; + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "Kudos" + DROP COLUMN IF EXISTS "teamPromptResponseId"; + `) + await client.end() +}