honor mutes when sending push notifications (#1145)
* honor mutes when sending push notifications for: * territory subscriptions * mentions * user subscriptions Also, don't allow you to mute a subscribed user, or vice versa * refactor mute detection for more code reuse update mute/subscribe error messages for consistency * variable rename * move `isMuted` to shared user lib, reuse in user resolver and webpush * update awards.csv --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
parent
512b08997e
commit
77c87dae80
|
@ -1231,7 +1231,7 @@ export const createMentions = async (item, models) => {
|
|||
|
||||
// only send if mention is new to avoid duplicates
|
||||
if (mention.createdAt.getTime() === mention.updatedAt.getTime()) {
|
||||
notifyMention(user.id, item)
|
||||
notifyMention({ models, userId: user.id, item })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { viewGroup } from './growth'
|
|||
import { timeUnitForRange, whenRange } from '@/lib/time'
|
||||
import assertApiKeyNotPermitted from './apiKey'
|
||||
import { hashEmail } from '@/lib/crypto'
|
||||
import { isMuted } from '@/lib/user'
|
||||
|
||||
const contributors = new Set()
|
||||
|
||||
|
@ -701,9 +702,16 @@ export default {
|
|||
subscribeUserPosts: async (parent, { id }, { me, models }) => {
|
||||
const lookupData = { followerId: Number(me.id), followeeId: Number(id) }
|
||||
const existing = await models.userSubscription.findUnique({ where: { followerId_followeeId: lookupData } })
|
||||
const muted = await isMuted({ models, muterId: me?.id, mutedId: id })
|
||||
if (existing) {
|
||||
if (muted && !existing.postsSubscribedAt) {
|
||||
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { postsSubscribedAt: existing.postsSubscribedAt ? null : new Date() } })
|
||||
} else {
|
||||
if (muted) {
|
||||
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
await models.userSubscription.create({ data: { ...lookupData, postsSubscribedAt: new Date() } })
|
||||
}
|
||||
return { id }
|
||||
|
@ -711,9 +719,16 @@ export default {
|
|||
subscribeUserComments: async (parent, { id }, { me, models }) => {
|
||||
const lookupData = { followerId: Number(me.id), followeeId: Number(id) }
|
||||
const existing = await models.userSubscription.findUnique({ where: { followerId_followeeId: lookupData } })
|
||||
const muted = await isMuted({ models, muterId: me?.id, mutedId: id })
|
||||
if (existing) {
|
||||
if (muted && !existing.commentsSubscribedAt) {
|
||||
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { commentsSubscribedAt: existing.commentsSubscribedAt ? null : new Date() } })
|
||||
} else {
|
||||
if (muted) {
|
||||
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
await models.userSubscription.create({ data: { ...lookupData, commentsSubscribedAt: new Date() } })
|
||||
}
|
||||
return { id }
|
||||
|
@ -725,6 +740,18 @@ export default {
|
|||
if (existing) {
|
||||
await models.mute.delete({ where })
|
||||
} else {
|
||||
// check to see if current user is subscribed to the target user, and disallow mute if so
|
||||
const subscription = await models.userSubscription.findUnique({
|
||||
where: {
|
||||
followerId_followeeId: {
|
||||
followerId: Number(me.id),
|
||||
followeeId: Number(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (subscription.postsSubscribedAt || subscription.commentsSubscribedAt) {
|
||||
throw new GraphQLError("you can't mute a stacker to whom you've subscribed", { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
await models.mute.create({ data: { ...lookupData } })
|
||||
}
|
||||
return { id }
|
||||
|
@ -782,16 +809,7 @@ export default {
|
|||
if (!me) return false
|
||||
if (typeof user.meMute !== 'undefined') return user.meMute
|
||||
|
||||
const mute = await models.mute.findUnique({
|
||||
where: {
|
||||
muterId_mutedId: {
|
||||
muterId: Number(me.id),
|
||||
mutedId: Number(user.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return !!mute
|
||||
return await isMuted({ models, muterId: me.id, mutedId: user.id })
|
||||
},
|
||||
since: async (user, args, { models }) => {
|
||||
// get the user's first item
|
||||
|
|
|
@ -79,4 +79,5 @@ felipebueno,pr,#1094,,,,2,,80k,felipebueno@getalby.com,2024-05-06
|
|||
benalleng,helpfulness,#1127,#927,good-first-issue,,,,2k,benalleng@mutiny.plus,2024-05-04
|
||||
itsrealfake,pr,#1135,#1016,good-first-issue,,,nonideal solution,10k,itsrealfake2@stacker.news,2024-05-06
|
||||
SatsAllDay,issue,#1135,#1016,good-first-issue,,,,1k,weareallsatoshi@getalby.com,2024-05-04
|
||||
s373nZ,issue,#1136,#1107,medium,high,,,50k,se7enz@minibits.cash,2024-05-05
|
||||
s373nZ,issue,#1136,#1107,medium,high,,,50k,???,???
|
||||
SatsAllDay,pr,#1145,#717,medium,,,,250k,weareallsatoshi@zeuspay.com,???
|
||||
|
|
|
|
@ -47,7 +47,7 @@ export default function MuteDropdownItem ({ user: { name, id, meMute } }) {
|
|||
toaster.success(`${meMute ? 'un' : ''}muted ${name}`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger(`failed to ${meMute ? 'un' : ''}mute ${name}`)
|
||||
toaster.danger(err.message ?? `failed to ${meMute ? 'un' : ''}mute ${name}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -51,7 +51,7 @@ export default function SubscribeUserDropdownItem ({ user, target = 'posts' }) {
|
|||
toaster.success(meSubscription ? 'unsubscribed' : 'subscribed')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger(meSubscription ? 'failed to unsubscribe' : 'failed to subscribe')
|
||||
toaster.danger(err.message ?? (meSubscription ? 'failed to unsubscribe' : 'failed to subscribe'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export const isMuted = async ({ models, muterId, mutedId }) => {
|
||||
const mute = await models.mute.findUnique({
|
||||
where: {
|
||||
muterId_mutedId: {
|
||||
muterId: Number(muterId),
|
||||
mutedId: Number(mutedId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return !!mute
|
||||
}
|
|
@ -3,6 +3,7 @@ import removeMd from 'remove-markdown'
|
|||
import { ANON_USER_ID, COMMENT_DEPTH_LIMIT, FOUND_BLURBS, LOST_BLURBS } from './constants'
|
||||
import { msatsToSats, numWithUnits } from './format'
|
||||
import models from '@/api/models'
|
||||
import { isMuted } from '@/lib/user'
|
||||
|
||||
const webPushEnabled = process.env.NODE_ENV === 'production' ||
|
||||
(process.env.VAPID_MAILTO && process.env.NEXT_PUBLIC_VAPID_PUBKEY && process.env.VAPID_PRIVKEY)
|
||||
|
@ -121,22 +122,20 @@ export async function replyToSubscription (subscriptionId, notification) {
|
|||
export const notifyUserSubscribers = async ({ models, item }) => {
|
||||
try {
|
||||
const isPost = !!item.title
|
||||
const userSubs = await models.userSubscription.findMany({
|
||||
where: {
|
||||
followeeId: Number(item.userId),
|
||||
[isPost ? 'postsSubscribedAt' : 'commentsSubscribedAt']: { not: null }
|
||||
},
|
||||
include: {
|
||||
followee: true
|
||||
}
|
||||
})
|
||||
const userSubsExcludingMutes = await models.$queryRawUnsafe(`
|
||||
SELECT "UserSubscription"."followerId", "UserSubscription"."followeeId", users.name as "followeeName"
|
||||
FROM "UserSubscription"
|
||||
INNER JOIN users ON users.id = "UserSubscription"."followeeId"
|
||||
WHERE "followeeId" = $1 AND ${isPost ? '"postsSubscribedAt"' : '"commentsSubscribedAt"'} IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM "Mute" WHERE "Mute"."muterId" = "UserSubscription"."followerId" AND "Mute"."mutedId" = $1)
|
||||
`, Number(item.userId))
|
||||
const subType = isPost ? 'POST' : 'COMMENT'
|
||||
const tag = `FOLLOW-${item.userId}-${subType}`
|
||||
await Promise.allSettled(userSubs.map(({ followerId, followee }) => sendUserNotification(followerId, {
|
||||
title: `@${followee.name} ${isPost ? 'created a post' : 'replied to a post'}`,
|
||||
await Promise.allSettled(userSubsExcludingMutes.map(({ followerId, followeeName }) => sendUserNotification(followerId, {
|
||||
title: `@${followeeName} ${isPost ? 'created a post' : 'replied to a post'}`,
|
||||
body: isPost ? item.title : item.text,
|
||||
item,
|
||||
data: { followeeName: followee.name, subType },
|
||||
data: { followeeName, subType },
|
||||
tag
|
||||
})))
|
||||
} catch (err) {
|
||||
|
@ -152,17 +151,17 @@ export const notifyTerritorySubscribers = async ({ models, item }) => {
|
|||
// only notify on posts in subs
|
||||
if (!isPost || !subName) return
|
||||
|
||||
const territorySubs = await models.subSubscription.findMany({
|
||||
where: {
|
||||
subName
|
||||
}
|
||||
})
|
||||
const territorySubsExcludingMuted = await models.$queryRawUnsafe(`
|
||||
SELECT "userId" FROM "SubSubscription"
|
||||
WHERE "subName" = $1
|
||||
AND NOT EXISTS (SELECT 1 FROM "Mute" m WHERE m."muterId" = "SubSubscription"."userId" AND m."mutedId" = $2)
|
||||
`, subName, Number(item.userId))
|
||||
|
||||
const author = await models.user.findUnique({ where: { id: item.userId } })
|
||||
|
||||
const tag = `TERRITORY_POST-${subName}`
|
||||
await Promise.allSettled(
|
||||
territorySubs
|
||||
territorySubsExcludingMuted
|
||||
// don't send push notification to author itself
|
||||
.filter(({ userId }) => userId !== author.id)
|
||||
.map(({ userId }) =>
|
||||
|
@ -247,14 +246,17 @@ export const notifyZapped = async ({ models, id }) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const notifyMention = async (userId, item) => {
|
||||
export const notifyMention = async ({ models, userId, item }) => {
|
||||
try {
|
||||
await sendUserNotification(userId, {
|
||||
title: 'you were mentioned',
|
||||
body: item.text,
|
||||
item,
|
||||
tag: 'MENTION'
|
||||
})
|
||||
const muted = await isMuted({ models, muterId: userId, mutedId: item.userId })
|
||||
if (!muted) {
|
||||
await sendUserNotification(userId, {
|
||||
title: 'you were mentioned',
|
||||
body: item.text,
|
||||
item,
|
||||
tag: 'MENTION'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue