From a059d8fdd1aa3d1e7f3e17a8c034aab026383404 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Wed, 29 May 2024 06:20:16 -0500 Subject: [PATCH] Item mention notifications --- api/resolvers/item.js | 43 ++++++++++++++++++- api/resolvers/notifications.js | 20 +++++++++ api/resolvers/user.js | 20 +++++++++ api/typeDefs/notifications.js | 8 +++- api/typeDefs/user.js | 2 + components/notifications.js | 21 +++++++++ fragments/notifications.js | 8 ++++ fragments/users.js | 2 + lib/webPush.js | 22 ++++++++++ pages/settings/index.js | 6 +++ .../migration.sql | 31 +++++++++++++ prisma/schema.prisma | 18 ++++++++ sw/eventListener.js | 4 +- 13 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 prisma/migrations/20240529105359_item_mentions/migration.sql diff --git a/api/resolvers/item.js b/api/resolvers/item.js index cc34e4fc8e..3b6a760d52 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -15,7 +15,7 @@ import { msatsToSats } from '@/lib/format' import { parse } from 'tldts' import uu from 'url-unshort' import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '@/lib/validate' -import { notifyItemParents, notifyUserSubscribers, notifyZapped, notifyTerritorySubscribers, notifyMention } from '@/lib/webPush' +import { notifyItemParents, notifyUserSubscribers, notifyZapped, notifyTerritorySubscribers, notifyMention, notifyItemMention } from '@/lib/webPush' import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand, getReminderCommand, hasReminderCommand } from '@/lib/item' import { datePivot, whenRange } from '@/lib/time' import { imageFeesInfo, uploadIdsFromText } from './image' @@ -1179,6 +1179,7 @@ export default { } const namePattern = /\B@[\w_]+/gi +const refPattern = /\B#\d+/gi export const createMentions = async (item, models) => { // if we miss a mention, in the rare circumstance there's some kind of @@ -1188,6 +1189,7 @@ export const createMentions = async (item, models) => { return } + // user mentions try { const mentions = item.text.match(namePattern)?.map(m => m.slice(1)) if (mentions?.length > 0) { @@ -1220,7 +1222,44 @@ export const createMentions = async (item, models) => { }) } } catch (e) { - console.error('mention failure', e) + console.error('user mention failure', e) + } + + // item mentions + try { + const refs = item.text.match(refPattern)?.map(m => Number(m.slice(1))) + if (refs?.length > 0) { + const referee = await models.item.findMany({ + where: { + id: { in: refs }, + // Don't create mentions for your own items + userId: { not: item.userId } + + } + }) + + referee.forEach(async r => { + const data = { + referrerId: item.id, + refereeId: r.id + } + + const mention = await models.itemMention.upsert({ + where: { + referrerId_refereeId: data + }, + update: data, + create: data + }) + + // only send if mention is new to avoid duplicates + if (mention.createdAt.getTime() === mention.updatedAt.getTime()) { + notifyItemMention({ models, userId: r.userId, item }) + } + }) + } + } catch (e) { + console.error('item mention failure', e) } } diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index a43acecdb9..3dfa4868ce 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -140,6 +140,22 @@ export default { LIMIT ${LIMIT}` ) } + // item mentions + if (meFull.noteItemMentions) { + itemDrivenQueries.push( + `SELECT "Referrer".*, "ItemMention".created_at AS "sortTime", 'ItemMention' AS type + FROM "ItemMention" + JOIN "Item" "Referee" ON "ItemMention"."refereeId" = "Referee".id + JOIN "Item" "Referrer" ON "ItemMention"."referrerId" = "Referrer".id + ${whereClause( + '"ItemMention".created_at < $2', + '"Referrer"."userId" <> $1', + '"Referee"."userId" = $1' + )} + ORDER BY "sortTime" DESC + LIMIT ${LIMIT}` + ) + } // Inner union to de-dupe item-driven notifications queries.push( // Only record per item ID @@ -157,6 +173,7 @@ export default { WHEN type = 'Reply' THEN 2 WHEN type = 'FollowActivity' THEN 3 WHEN type = 'TerritoryPost' THEN 4 + WHEN type = 'ItemMention' THEN 5 END ASC )` ) @@ -456,6 +473,9 @@ export default { mention: async (n, args, { models }) => true, item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me }) }, + ItemMention: { + item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me }) + }, InvoicePaid: { invoice: async (n, args, { me, models }) => getInvoice(n, { id: n.id }, { me, models }) }, diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 632dc1af82..abe2d0f143 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -347,6 +347,26 @@ export default { } } + if (user.noteItemMentions) { + const [newMentions] = await models.$queryRawUnsafe(` + SELECT EXISTS( + SELECT * + FROM "ItemMention" + JOIN "Item" "Referee" ON "ItemMention"."refereeId" = "Referee".id + JOIN "Item" ON "ItemMention"."referrerId" = "Item".id + ${whereClause( + '"ItemMention".created_at < $2', + '"Item"."userId" <> $1', + '"Referee"."userId" = $1', + await filterClause(me, models), + muteClause(me) + )})`, me.id, lastChecked) + if (newMentions.exists) { + foundNotes() + return true + } + } + if (user.noteForwardedSats) { const [newFwdSats] = await models.$queryRawUnsafe(` SELECT EXISTS( diff --git a/api/typeDefs/notifications.js b/api/typeDefs/notifications.js index 20fc13a45b..215f368da2 100644 --- a/api/typeDefs/notifications.js +++ b/api/typeDefs/notifications.js @@ -43,6 +43,12 @@ export default gql` sortTime: Date! } + type ItemMention { + id: ID! + item: Item! + sortTime: Date! + } + type Invitification { id: ID! invite: Invite! @@ -130,7 +136,7 @@ export default gql` union Notification = Reply | Votification | Mention | Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral | Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus - | TerritoryPost | TerritoryTransfer | Reminder + | TerritoryPost | TerritoryTransfer | Reminder | ItemMention type Notifications { lastChecked: Date diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 5c0f29dbe4..1e4fd01484 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -95,6 +95,7 @@ export default gql` noteItemSats: Boolean! noteJobIndicator: Boolean! noteMentions: Boolean! + noteItemMentions: Boolean! nsfwMode: Boolean! tipDefault: Int! turboTipping: Boolean! @@ -161,6 +162,7 @@ export default gql` noteItemSats: Boolean! noteJobIndicator: Boolean! noteMentions: Boolean! + noteItemMentions: Boolean! nsfwMode: Boolean! tipDefault: Int! turboTipping: Boolean! diff --git a/components/notifications.js b/components/notifications.js index b346b4aa56..2895549657 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -54,6 +54,7 @@ function Notification ({ n, fresh }) { (type === 'Votification' && ) || (type === 'ForwardedVotification' && ) || (type === 'Mention' && ) || + (type === 'ItemMention' && ) || (type === 'JobChanged' && ) || (type === 'Reply' && ) || (type === 'SubStatus' && ) || @@ -391,6 +392,26 @@ function Mention ({ n }) { ) } +function ItemMention ({ n }) { + return ( + <> + + one of your {n.title ? 'posts' : 'comments'} was mentioned + +
+ {n.item?.title + ? + : ( +
+ + + +
)} +
+ + ) +} + function JobChanged ({ n }) { return ( <> diff --git a/fragments/notifications.js b/fragments/notifications.js index 7670670181..01c3519170 100644 --- a/fragments/notifications.js +++ b/fragments/notifications.js @@ -25,6 +25,14 @@ export const NOTIFICATIONS = gql` text } } + ... on ItemMention { + id + sortTime + item { + ...ItemFullFields + text + } + } ... on Votification { id sortTime diff --git a/fragments/users.js b/fragments/users.js index e6bfd0853d..723f67b5d1 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -38,6 +38,7 @@ export const ME = gql` noteItemSats noteJobIndicator noteMentions + noteItemMentions sats tipDefault tipPopover @@ -73,6 +74,7 @@ export const SETTINGS_FIELDS = gql` noteEarning noteAllDescendants noteMentions + noteItemMentions noteDeposits noteWithdrawals noteInvites diff --git a/lib/webPush.js b/lib/webPush.js index edbb919d00..f8bc0add3e 100644 --- a/lib/webPush.js +++ b/lib/webPush.js @@ -38,6 +38,7 @@ const createUserFilter = (tag) => { const tagMap = { REPLY: 'noteAllDescendants', MENTION: 'noteMentions', + ITEM_MENTION: 'noteItemMentions', TIP: 'noteItemSats', FORWARDEDTIP: 'noteForwardedSats', REFERRAL: 'noteInvites', @@ -262,6 +263,27 @@ export const notifyMention = async ({ models, userId, item }) => { } } +export const notifyItemMention = async ({ models, userId, item }) => { + try { + const muted = await isMuted({ models, muterId: userId, mutedId: item.userId }) + if (!muted) { + const user = await models.user.findUnique({ where: { id: item.userId } }) + const isPost = !!item.title + const subType = isPost ? 'POST' : 'COMMENT' + const tag = `ITEM_MENTION-${subType}` + await sendUserNotification(userId, { + title: `@${user.name} mentioned one of your ${isPost ? 'posts' : 'comments'}`, + body: item.text, + item, + data: { subType }, + tag + }) + } + } catch (err) { + console.error(err) + } +} + export const notifyReferral = async (userId) => { try { await sendUserNotification(userId, { title: 'someone joined via one of your referral links', tag: 'REFERRAL' }) diff --git a/pages/settings/index.js b/pages/settings/index.js index 362a7a390f..3c501b9e23 100644 --- a/pages/settings/index.js +++ b/pages/settings/index.js @@ -121,6 +121,7 @@ export default function Settings ({ ssrData }) { noteEarning: settings?.noteEarning, noteAllDescendants: settings?.noteAllDescendants, noteMentions: settings?.noteMentions, + noteItemMentions: settings?.noteItemMentions, noteDeposits: settings?.noteDeposits, noteWithdrawals: settings?.noteWithdrawals, noteInvites: settings?.noteInvites, @@ -281,6 +282,11 @@ export default function Settings ({ ssrData }) { name='noteMentions' groupClassName='mb-0' /> +