From edae709f5f25c192e386dded4838182684c9e6d9 Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Thu, 4 Aug 2022 15:35:55 -0700 Subject: [PATCH] Notify mentioned users on market publish (#683) * Add function to parse at mentions * Notify mentioned users on market create - refactor createNotification to accept list of recipients' ids --- common/util/parse.ts | 10 +++ functions/src/create-notification.ts | 66 ++++++++++--------- .../src/on-create-comment-on-contract.ts | 5 +- functions/src/on-create-contract.ts | 9 ++- functions/src/on-create-group.ts | 28 ++++---- functions/src/on-follow-user.ts | 2 +- 6 files changed, 68 insertions(+), 52 deletions(-) diff --git a/common/util/parse.ts b/common/util/parse.ts index cacd0862..f07e4097 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -22,6 +22,7 @@ import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' import { Mention } from '@tiptap/extension-mention' import Iframe from './tiptap-iframe' +import { uniq } from 'lodash' export function parseTags(text: string) { const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi @@ -61,6 +62,15 @@ const checkAgainstQuery = (query: string, corpus: string) => export const searchInAny = (query: string, ...fields: string[]) => fields.some((field) => checkAgainstQuery(query, field)) +/** @return user ids of all \@mentions */ +export function parseMentions(data: JSONContent): string[] { + const mentions = data.content?.flatMap(parseMentions) ?? [] //dfs + if (data.type === 'mention' && data.attrs) { + mentions.push(data.attrs.id as string) + } + return uniq(mentions) +} + // can't just do [StarterKit, Image...] because it doesn't work with cjs imports export const exhibitExts = [ Blockquote, diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 83568535..e16920f7 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -33,7 +33,7 @@ export const createNotification = async ( miscData?: { contract?: Contract relatedSourceType?: notification_source_types - relatedUserId?: string + recipients?: string[] slug?: string title?: string } @@ -41,7 +41,7 @@ export const createNotification = async ( const { contract: sourceContract, relatedSourceType, - relatedUserId, + recipients, slug, title, } = miscData ?? {} @@ -128,7 +128,7 @@ export const createNotification = async ( }) } - const notifyRepliedUsers = async ( + const notifyRepliedUser = ( userToReasonTexts: user_to_reason_texts, relatedUserId: string, relatedSourceType: notification_source_types @@ -145,7 +145,7 @@ export const createNotification = async ( } } - const notifyFollowedUser = async ( + const notifyFollowedUser = ( userToReasonTexts: user_to_reason_texts, followedUserId: string ) => { @@ -155,21 +155,24 @@ export const createNotification = async ( } } - const notifyTaggedUsers = async ( - userToReasonTexts: user_to_reason_texts, - sourceText: string - ) => { - const taggedUsers = sourceText.match(/@\w+/g) - if (!taggedUsers) return - // await all get tagged users: - const users = await Promise.all( - taggedUsers.map(async (username) => { - return await getUserByUsername(username.slice(1)) - }) + /** @deprecated parse from rich text instead */ + const parseMentions = async (source: string) => { + const mentions = source.match(/@\w+/g) + if (!mentions) return [] + return Promise.all( + mentions.map( + async (username) => (await getUserByUsername(username.slice(1)))?.id + ) ) - users.forEach((taggedUser) => { - if (taggedUser && shouldGetNotification(taggedUser.id, userToReasonTexts)) - userToReasonTexts[taggedUser.id] = { + } + + const notifyTaggedUsers = ( + userToReasonTexts: user_to_reason_texts, + userIds: (string | undefined)[] + ) => { + userIds.forEach((id) => { + if (id && shouldGetNotification(id, userToReasonTexts)) + userToReasonTexts[id] = { reason: 'tagged_user', } }) @@ -254,7 +257,7 @@ export const createNotification = async ( }) } - const notifyUserAddedToGroup = async ( + const notifyUserAddedToGroup = ( userToReasonTexts: user_to_reason_texts, relatedUserId: string ) => { @@ -276,11 +279,14 @@ export const createNotification = async ( const getUsersToNotify = async () => { const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. - if (sourceType === 'follow' && relatedUserId) { - await notifyFollowedUser(userToReasonTexts, relatedUserId) - } else if (sourceType === 'group' && relatedUserId) { - if (sourceUpdateType === 'created') - await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) + if (sourceType === 'follow' && recipients?.[0]) { + notifyFollowedUser(userToReasonTexts, recipients[0]) + } else if ( + sourceType === 'group' && + sourceUpdateType === 'created' && + recipients + ) { + recipients.forEach((r) => notifyUserAddedToGroup(userToReasonTexts, r)) } // The following functions need sourceContract to be defined. @@ -293,13 +299,10 @@ export const createNotification = async ( (sourceUpdateType === 'updated' || sourceUpdateType === 'resolved')) ) { if (sourceType === 'comment') { - if (relatedUserId && relatedSourceType) - await notifyRepliedUsers( - userToReasonTexts, - relatedUserId, - relatedSourceType - ) - if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText) + if (recipients?.[0] && relatedSourceType) + notifyRepliedUser(userToReasonTexts, recipients[0], relatedSourceType) + if (sourceText) + notifyTaggedUsers(userToReasonTexts, await parseMentions(sourceText)) } await notifyContractCreator(userToReasonTexts, sourceContract) await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) @@ -308,6 +311,7 @@ export const createNotification = async ( await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract) } else if (sourceType === 'contract' && sourceUpdateType === 'created') { await notifyUsersFollowers(userToReasonTexts) + notifyTaggedUsers(userToReasonTexts, recipients ?? []) } else if (sourceType === 'contract' && sourceUpdateType === 'closed') { await notifyContractCreator(userToReasonTexts, sourceContract, { force: true, diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index 8d841ac0..4719fd08 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -68,9 +68,10 @@ export const onCreateCommentOnContract = functions ? 'answer' : undefined - const relatedUserId = comment.replyToCommentId + const repliedUserId = comment.replyToCommentId ? comments.find((c) => c.id === comment.replyToCommentId)?.userId : answer?.userId + const recipients = repliedUserId ? [repliedUserId] : [] await createNotification( comment.id, @@ -79,7 +80,7 @@ export const onCreateCommentOnContract = functions commentCreator, eventId, comment.text, - { contract, relatedSourceType, relatedUserId } + { contract, relatedSourceType, recipients } ) const recipientUserIds = uniq([ diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index a43beda7..6b57a9a0 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions' import { getUser } from './utils' import { createNotification } from './create-notification' import { Contract } from '../../common/contract' -import { richTextToString } from '../../common/util/parse' +import { parseMentions, richTextToString } from '../../common/util/parse' import { JSONContent } from '@tiptap/core' export const onCreateContract = functions.firestore @@ -14,13 +14,16 @@ export const onCreateContract = functions.firestore const contractCreator = await getUser(contract.creatorId) if (!contractCreator) throw new Error('Could not find contract creator') + const desc = contract.description as JSONContent + const mentioned = parseMentions(desc) + await createNotification( contract.id, 'contract', 'created', contractCreator, eventId, - richTextToString(contract.description as JSONContent), - { contract } + richTextToString(desc), + { contract, recipients: mentioned } ) }) diff --git a/functions/src/on-create-group.ts b/functions/src/on-create-group.ts index 47618d7a..5209788d 100644 --- a/functions/src/on-create-group.ts +++ b/functions/src/on-create-group.ts @@ -12,19 +12,17 @@ export const onCreateGroup = functions.firestore const groupCreator = await getUser(group.creatorId) if (!groupCreator) throw new Error('Could not find group creator') // create notifications for all members of the group - for (const memberId of group.memberIds) { - await createNotification( - group.id, - 'group', - 'created', - groupCreator, - eventId, - group.about, - { - relatedUserId: memberId, - slug: group.slug, - title: group.name, - } - ) - } + await createNotification( + group.id, + 'group', + 'created', + groupCreator, + eventId, + group.about, + { + recipients: group.memberIds, + slug: group.slug, + title: group.name, + } + ) }) diff --git a/functions/src/on-follow-user.ts b/functions/src/on-follow-user.ts index 9a6e6dce..52042345 100644 --- a/functions/src/on-follow-user.ts +++ b/functions/src/on-follow-user.ts @@ -30,7 +30,7 @@ export const onFollowUser = functions.firestore followingUser, eventId, '', - { relatedUserId: follow.userId } + { recipients: [follow.userId] } ) })