Item mention notifications (#1208)
* Parse internal refs to links * Item mention notifications * Also parse item mentions as URLs * Fix subType determined by referrer item instead of referee item * Ignore subType Considering if the item that was referred to was a post or comment made the code more complex than initially necessary. For example, notifications for /notifications are deduplicated based on item id and the same item could refer to posts and comments, so to include "one of your posts" or "one of your comments" in the title would require splitting notifications based on the type of referred item. I didn't want to do this but also wanted to have consistent notification titles between push and /notifications, so I use "items" in both places now, even though I think using "items" isn't ideal from a user perspective. I think it might be confusing. * Fix rootText * Replace full links to #<id> syntax in push notifications * Refactor mention code into separate functions
This commit is contained in:
parent
d454bbdb72
commit
2597eb56f3
|
@ -15,7 +15,7 @@ import { msatsToSats } from '@/lib/format'
|
||||||
import { parse } from 'tldts'
|
import { parse } from 'tldts'
|
||||||
import uu from 'url-unshort'
|
import uu from 'url-unshort'
|
||||||
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '@/lib/validate'
|
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 { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand, getReminderCommand, hasReminderCommand } from '@/lib/item'
|
||||||
import { datePivot, whenRange } from '@/lib/time'
|
import { datePivot, whenRange } from '@/lib/time'
|
||||||
import { imageFeesInfo, uploadIdsFromText } from './image'
|
import { imageFeesInfo, uploadIdsFromText } from './image'
|
||||||
|
@ -1179,6 +1179,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const namePattern = /\B@[\w_]+/gi
|
const namePattern = /\B@[\w_]+/gi
|
||||||
|
const refPattern = new RegExp(`(?:#|${process.env.NEXT_PUBLIC_URL}/items/)(?<id>\\d+)`, 'gi')
|
||||||
|
|
||||||
export const createMentions = async (item, models) => {
|
export const createMentions = async (item, models) => {
|
||||||
// if we miss a mention, in the rare circumstance there's some kind of
|
// if we miss a mention, in the rare circumstance there's some kind of
|
||||||
|
@ -1188,40 +1189,90 @@ export const createMentions = async (item, models) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// user mentions
|
||||||
try {
|
try {
|
||||||
const mentions = item.text.match(namePattern)?.map(m => m.slice(1))
|
await createUserMentions(item, models)
|
||||||
if (mentions?.length > 0) {
|
|
||||||
const users = await models.user.findMany({
|
|
||||||
where: {
|
|
||||||
name: { in: mentions },
|
|
||||||
// Don't create mentions when mentioning yourself
|
|
||||||
id: { not: item.userId }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
users.forEach(async user => {
|
|
||||||
const data = {
|
|
||||||
itemId: item.id,
|
|
||||||
userId: user.id
|
|
||||||
}
|
|
||||||
|
|
||||||
const mention = await models.mention.upsert({
|
|
||||||
where: {
|
|
||||||
itemId_userId: data
|
|
||||||
},
|
|
||||||
update: data,
|
|
||||||
create: data
|
|
||||||
})
|
|
||||||
|
|
||||||
// only send if mention is new to avoid duplicates
|
|
||||||
if (mention.createdAt.getTime() === mention.updatedAt.getTime()) {
|
|
||||||
notifyMention({ models, userId: user.id, item })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('mention failure', e)
|
console.error('user mention failure', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// item mentions
|
||||||
|
try {
|
||||||
|
await createItemMentions(item, models)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('item mention failure', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUserMentions = async (item, models) => {
|
||||||
|
const mentions = item.text.match(namePattern)?.map(m => m.slice(1))
|
||||||
|
if (!mentions || mentions.length === 0) return
|
||||||
|
|
||||||
|
const users = await models.user.findMany({
|
||||||
|
where: {
|
||||||
|
name: { in: mentions },
|
||||||
|
// Don't create mentions when mentioning yourself
|
||||||
|
id: { not: item.userId }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
users.forEach(async user => {
|
||||||
|
const data = {
|
||||||
|
itemId: item.id,
|
||||||
|
userId: user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const mention = await models.mention.upsert({
|
||||||
|
where: {
|
||||||
|
itemId_userId: data
|
||||||
|
},
|
||||||
|
update: data,
|
||||||
|
create: data
|
||||||
|
})
|
||||||
|
|
||||||
|
// only send if mention is new to avoid duplicates
|
||||||
|
if (mention.createdAt.getTime() === mention.updatedAt.getTime()) {
|
||||||
|
notifyMention({ models, userId: user.id, item })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createItemMentions = async (item, models) => {
|
||||||
|
const refs = item.text.match(refPattern)?.map(m => {
|
||||||
|
if (m.startsWith('#')) return Number(m.slice(1))
|
||||||
|
// is not #<id> syntax but full URL
|
||||||
|
return Number(m.split('/').slice(-1)[0])
|
||||||
|
})
|
||||||
|
if (!refs || refs.length === 0) return
|
||||||
|
|
||||||
|
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, referrerItem: item, refereeItem: r })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models, lnd, hash, hmac }) => {
|
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models, lnd, hash, hmac }) => {
|
||||||
|
|
|
@ -140,6 +140,22 @@ export default {
|
||||||
LIMIT ${LIMIT}`
|
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
|
// Inner union to de-dupe item-driven notifications
|
||||||
queries.push(
|
queries.push(
|
||||||
// Only record per item ID
|
// Only record per item ID
|
||||||
|
@ -157,6 +173,7 @@ export default {
|
||||||
WHEN type = 'Reply' THEN 2
|
WHEN type = 'Reply' THEN 2
|
||||||
WHEN type = 'FollowActivity' THEN 3
|
WHEN type = 'FollowActivity' THEN 3
|
||||||
WHEN type = 'TerritoryPost' THEN 4
|
WHEN type = 'TerritoryPost' THEN 4
|
||||||
|
WHEN type = 'ItemMention' THEN 5
|
||||||
END ASC
|
END ASC
|
||||||
)`
|
)`
|
||||||
)
|
)
|
||||||
|
@ -456,6 +473,9 @@ export default {
|
||||||
mention: async (n, args, { models }) => true,
|
mention: async (n, args, { models }) => true,
|
||||||
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
|
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: {
|
InvoicePaid: {
|
||||||
invoice: async (n, args, { me, models }) => getInvoice(n, { id: n.id }, { me, models })
|
invoice: async (n, args, { me, models }) => getInvoice(n, { id: n.id }, { me, models })
|
||||||
},
|
},
|
||||||
|
|
|
@ -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) {
|
if (user.noteForwardedSats) {
|
||||||
const [newFwdSats] = await models.$queryRawUnsafe(`
|
const [newFwdSats] = await models.$queryRawUnsafe(`
|
||||||
SELECT EXISTS(
|
SELECT EXISTS(
|
||||||
|
|
|
@ -43,6 +43,12 @@ export default gql`
|
||||||
sortTime: Date!
|
sortTime: Date!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ItemMention {
|
||||||
|
id: ID!
|
||||||
|
item: Item!
|
||||||
|
sortTime: Date!
|
||||||
|
}
|
||||||
|
|
||||||
type Invitification {
|
type Invitification {
|
||||||
id: ID!
|
id: ID!
|
||||||
invite: Invite!
|
invite: Invite!
|
||||||
|
@ -130,7 +136,7 @@ export default gql`
|
||||||
union Notification = Reply | Votification | Mention
|
union Notification = Reply | Votification | Mention
|
||||||
| Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral
|
| Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral
|
||||||
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
|
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
|
||||||
| TerritoryPost | TerritoryTransfer | Reminder
|
| TerritoryPost | TerritoryTransfer | Reminder | ItemMention
|
||||||
|
|
||||||
type Notifications {
|
type Notifications {
|
||||||
lastChecked: Date
|
lastChecked: Date
|
||||||
|
|
|
@ -95,6 +95,7 @@ export default gql`
|
||||||
noteItemSats: Boolean!
|
noteItemSats: Boolean!
|
||||||
noteJobIndicator: Boolean!
|
noteJobIndicator: Boolean!
|
||||||
noteMentions: Boolean!
|
noteMentions: Boolean!
|
||||||
|
noteItemMentions: Boolean!
|
||||||
nsfwMode: Boolean!
|
nsfwMode: Boolean!
|
||||||
tipDefault: Int!
|
tipDefault: Int!
|
||||||
turboTipping: Boolean!
|
turboTipping: Boolean!
|
||||||
|
@ -161,6 +162,7 @@ export default gql`
|
||||||
noteItemSats: Boolean!
|
noteItemSats: Boolean!
|
||||||
noteJobIndicator: Boolean!
|
noteJobIndicator: Boolean!
|
||||||
noteMentions: Boolean!
|
noteMentions: Boolean!
|
||||||
|
noteItemMentions: Boolean!
|
||||||
nsfwMode: Boolean!
|
nsfwMode: Boolean!
|
||||||
tipDefault: Int!
|
tipDefault: Int!
|
||||||
turboTipping: Boolean!
|
turboTipping: Boolean!
|
||||||
|
|
|
@ -54,6 +54,7 @@ function Notification ({ n, fresh }) {
|
||||||
(type === 'Votification' && <Votification n={n} />) ||
|
(type === 'Votification' && <Votification n={n} />) ||
|
||||||
(type === 'ForwardedVotification' && <ForwardedVotification n={n} />) ||
|
(type === 'ForwardedVotification' && <ForwardedVotification n={n} />) ||
|
||||||
(type === 'Mention' && <Mention n={n} />) ||
|
(type === 'Mention' && <Mention n={n} />) ||
|
||||||
|
(type === 'ItemMention' && <ItemMention n={n} />) ||
|
||||||
(type === 'JobChanged' && <JobChanged n={n} />) ||
|
(type === 'JobChanged' && <JobChanged n={n} />) ||
|
||||||
(type === 'Reply' && <Reply n={n} />) ||
|
(type === 'Reply' && <Reply n={n} />) ||
|
||||||
(type === 'SubStatus' && <SubStatus n={n} />) ||
|
(type === 'SubStatus' && <SubStatus n={n} />) ||
|
||||||
|
@ -391,6 +392,26 @@ function Mention ({ n }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ItemMention ({ n }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<small className='fw-bold text-info ms-2'>
|
||||||
|
your item was mentioned in
|
||||||
|
</small>
|
||||||
|
<div>
|
||||||
|
{n.item?.title
|
||||||
|
? <Item item={n.item} />
|
||||||
|
: (
|
||||||
|
<div className='pb-2'>
|
||||||
|
<RootProvider root={n.item.root}>
|
||||||
|
<Comment item={n.item} noReply includeParent rootText='replying on:' clickToContext />
|
||||||
|
</RootProvider>
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function JobChanged ({ n }) {
|
function JobChanged ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
import isEqual from 'lodash/isEqual'
|
import isEqual from 'lodash/isEqual'
|
||||||
import UserPopover from './user-popover'
|
import UserPopover from './user-popover'
|
||||||
import ItemPopover from './item-popover'
|
import ItemPopover from './item-popover'
|
||||||
|
import ref from '@/lib/remark-ref2link'
|
||||||
|
|
||||||
export function SearchText ({ text }) {
|
export function SearchText ({ text }) {
|
||||||
return (
|
return (
|
||||||
|
@ -298,7 +299,7 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
|
||||||
},
|
},
|
||||||
img: Img
|
img: Img
|
||||||
}}
|
}}
|
||||||
remarkPlugins={[gfm, mention, sub]}
|
remarkPlugins={[gfm, mention, sub, ref]}
|
||||||
rehypePlugins={[rehypeInlineCodeProperty]}
|
rehypePlugins={[rehypeInlineCodeProperty]}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -25,6 +25,14 @@ export const NOTIFICATIONS = gql`
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
... on ItemMention {
|
||||||
|
id
|
||||||
|
sortTime
|
||||||
|
item {
|
||||||
|
...ItemFullFields
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
... on Votification {
|
... on Votification {
|
||||||
id
|
id
|
||||||
sortTime
|
sortTime
|
||||||
|
|
|
@ -38,6 +38,7 @@ export const ME = gql`
|
||||||
noteItemSats
|
noteItemSats
|
||||||
noteJobIndicator
|
noteJobIndicator
|
||||||
noteMentions
|
noteMentions
|
||||||
|
noteItemMentions
|
||||||
sats
|
sats
|
||||||
tipDefault
|
tipDefault
|
||||||
tipPopover
|
tipPopover
|
||||||
|
@ -73,6 +74,7 @@ export const SETTINGS_FIELDS = gql`
|
||||||
noteEarning
|
noteEarning
|
||||||
noteAllDescendants
|
noteAllDescendants
|
||||||
noteMentions
|
noteMentions
|
||||||
|
noteItemMentions
|
||||||
noteDeposits
|
noteDeposits
|
||||||
noteWithdrawals
|
noteWithdrawals
|
||||||
noteInvites
|
noteInvites
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { findAndReplace } from 'mdast-util-find-and-replace'
|
||||||
|
|
||||||
|
const refRegex = /#(\d+(\/(edit|related|ots))?)/gi
|
||||||
|
|
||||||
|
export default function ref (options) {
|
||||||
|
return function transformer (tree) {
|
||||||
|
findAndReplace(
|
||||||
|
tree,
|
||||||
|
[
|
||||||
|
[refRegex, replaceRef]
|
||||||
|
],
|
||||||
|
{ ignore: ['link', 'linkReference'] }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceRef (value, itemId, match) {
|
||||||
|
const node = { type: 'text', value }
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'link',
|
||||||
|
title: null,
|
||||||
|
url: `/items/${itemId}`,
|
||||||
|
children: [node]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ const createUserFilter = (tag) => {
|
||||||
const tagMap = {
|
const tagMap = {
|
||||||
REPLY: 'noteAllDescendants',
|
REPLY: 'noteAllDescendants',
|
||||||
MENTION: 'noteMentions',
|
MENTION: 'noteMentions',
|
||||||
|
ITEM_MENTION: 'noteItemMentions',
|
||||||
TIP: 'noteItemSats',
|
TIP: 'noteItemSats',
|
||||||
FORWARDEDTIP: 'noteForwardedSats',
|
FORWARDEDTIP: 'noteForwardedSats',
|
||||||
REFERRAL: 'noteInvites',
|
REFERRAL: 'noteInvites',
|
||||||
|
@ -262,6 +263,27 @@ export const notifyMention = async ({ models, userId, item }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const notifyItemMention = async ({ models, referrerItem, refereeItem }) => {
|
||||||
|
try {
|
||||||
|
const muted = await isMuted({ models, muterId: refereeItem.userId, mutedId: referrerItem.userId })
|
||||||
|
if (!muted) {
|
||||||
|
const referrer = await models.user.findUnique({ where: { id: referrerItem.userId } })
|
||||||
|
|
||||||
|
// replace full links to #<id> syntax as rendered on site
|
||||||
|
const body = referrerItem.text.replace(new RegExp(`${process.env.NEXT_PUBLIC_URL}/items/(\\d+)`, 'gi'), '#$1')
|
||||||
|
|
||||||
|
await sendUserNotification(refereeItem.userId, {
|
||||||
|
title: `@${referrer.name} mentioned one of your items`,
|
||||||
|
body,
|
||||||
|
item: referrerItem,
|
||||||
|
tag: 'ITEM_MENTION'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const notifyReferral = async (userId) => {
|
export const notifyReferral = async (userId) => {
|
||||||
try {
|
try {
|
||||||
await sendUserNotification(userId, { title: 'someone joined via one of your referral links', tag: 'REFERRAL' })
|
await sendUserNotification(userId, { title: 'someone joined via one of your referral links', tag: 'REFERRAL' })
|
||||||
|
|
|
@ -120,6 +120,7 @@ export default function Settings ({ ssrData }) {
|
||||||
noteEarning: settings?.noteEarning,
|
noteEarning: settings?.noteEarning,
|
||||||
noteAllDescendants: settings?.noteAllDescendants,
|
noteAllDescendants: settings?.noteAllDescendants,
|
||||||
noteMentions: settings?.noteMentions,
|
noteMentions: settings?.noteMentions,
|
||||||
|
noteItemMentions: settings?.noteItemMentions,
|
||||||
noteDeposits: settings?.noteDeposits,
|
noteDeposits: settings?.noteDeposits,
|
||||||
noteWithdrawals: settings?.noteWithdrawals,
|
noteWithdrawals: settings?.noteWithdrawals,
|
||||||
noteInvites: settings?.noteInvites,
|
noteInvites: settings?.noteInvites,
|
||||||
|
@ -280,6 +281,11 @@ export default function Settings ({ ssrData }) {
|
||||||
name='noteMentions'
|
name='noteMentions'
|
||||||
groupClassName='mb-0'
|
groupClassName='mb-0'
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label='someone mentions one of my items'
|
||||||
|
name='noteItemMentions'
|
||||||
|
groupClassName='mb-0'
|
||||||
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label='there is a new job'
|
label='there is a new job'
|
||||||
name='noteJobIndicator'
|
name='noteJobIndicator'
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ItemMention" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"referrerId" INTEGER NOT NULL,
|
||||||
|
"refereeId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ItemMention_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ItemMention.created_at_index" ON "ItemMention"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ItemMention.referrerId_index" ON "ItemMention"("referrerId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ItemMention.refereeId_index" ON "ItemMention"("refereeId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ItemMention.referrerId_refereeId_unique" ON "ItemMention"("referrerId", "refereeId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ItemMention" ADD CONSTRAINT "ItemMention_referrerId_fkey" FOREIGN KEY ("referrerId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ItemMention" ADD CONSTRAINT "ItemMention_refereeId_fkey" FOREIGN KEY ("refereeId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "noteItemMentions" BOOLEAN NOT NULL DEFAULT true;
|
|
@ -44,6 +44,7 @@ model User {
|
||||||
noteInvites Boolean @default(true)
|
noteInvites Boolean @default(true)
|
||||||
noteItemSats Boolean @default(true)
|
noteItemSats Boolean @default(true)
|
||||||
noteMentions Boolean @default(true)
|
noteMentions Boolean @default(true)
|
||||||
|
noteItemMentions Boolean @default(true)
|
||||||
noteForwardedSats Boolean @default(true)
|
noteForwardedSats Boolean @default(true)
|
||||||
lastCheckedJobs DateTime?
|
lastCheckedJobs DateTime?
|
||||||
noteJobIndicator Boolean @default(true)
|
noteJobIndicator Boolean @default(true)
|
||||||
|
@ -411,6 +412,8 @@ model Item {
|
||||||
user User @relation("UserItems", fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation("UserItems", fields: [userId], references: [id], onDelete: Cascade)
|
||||||
actions ItemAct[]
|
actions ItemAct[]
|
||||||
mentions Mention[]
|
mentions Mention[]
|
||||||
|
referrer ItemMention[] @relation("referrer")
|
||||||
|
referee ItemMention[] @relation("referee")
|
||||||
PollOption PollOption[]
|
PollOption PollOption[]
|
||||||
PollVote PollVote[]
|
PollVote PollVote[]
|
||||||
ThreadSubscription ThreadSubscription[]
|
ThreadSubscription ThreadSubscription[]
|
||||||
|
@ -661,6 +664,21 @@ model Mention {
|
||||||
@@index([userId], map: "Mention.userId_index")
|
@@index([userId], map: "Mention.userId_index")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ItemMention {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||||
|
referrerId Int
|
||||||
|
refereeId Int
|
||||||
|
referrerItem Item @relation("referrer", fields: [referrerId], references: [id], onDelete: Cascade)
|
||||||
|
refereeItem Item @relation("referee", fields: [refereeId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([referrerId, refereeId], map: "ItemMention.referrerId_refereeId_unique")
|
||||||
|
@@index([createdAt], map: "ItemMention.created_at_index")
|
||||||
|
@@index([referrerId], map: "ItemMention.referrerId_index")
|
||||||
|
@@index([refereeId], map: "ItemMention.refereeId_index")
|
||||||
|
}
|
||||||
|
|
||||||
model Invoice {
|
model Invoice {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
|
@ -114,7 +114,7 @@ const mergeAndShowNotification = async (sw, payload, currentNotifications, tag,
|
||||||
// merge notifications into single notification payload
|
// merge notifications into single notification payload
|
||||||
// ---
|
// ---
|
||||||
// tags that need to know the amount of notifications with same tag for merging
|
// tags that need to know the amount of notifications with same tag for merging
|
||||||
const AMOUNT_TAGS = ['REPLY', 'MENTION', 'REFERRAL', 'INVITE', 'FOLLOW', 'TERRITORY_POST']
|
const AMOUNT_TAGS = ['REPLY', 'MENTION', 'ITEM_MENTION', 'REFERRAL', 'INVITE', 'FOLLOW', 'TERRITORY_POST']
|
||||||
// tags that need to know the sum of sats of notifications with same tag for merging
|
// tags that need to know the sum of sats of notifications with same tag for merging
|
||||||
const SUM_SATS_TAGS = ['DEPOSIT', 'WITHDRAWAL']
|
const SUM_SATS_TAGS = ['DEPOSIT', 'WITHDRAWAL']
|
||||||
// this should reflect the amount of notifications that were already merged before
|
// this should reflect the amount of notifications that were already merged before
|
||||||
|
@ -143,6 +143,8 @@ const mergeAndShowNotification = async (sw, payload, currentNotifications, tag,
|
||||||
title = `you have ${amount} new replies`
|
title = `you have ${amount} new replies`
|
||||||
} else if (compareTag === 'MENTION') {
|
} else if (compareTag === 'MENTION') {
|
||||||
title = `you were mentioned ${amount} times`
|
title = `you were mentioned ${amount} times`
|
||||||
|
} else if (compareTag === 'ITEM_MENTION') {
|
||||||
|
title = `your items were mentioned ${amount} times`
|
||||||
} else if (compareTag === 'REFERRAL') {
|
} else if (compareTag === 'REFERRAL') {
|
||||||
title = `${amount} stackers joined via your referral links`
|
title = `${amount} stackers joined via your referral links`
|
||||||
} else if (compareTag === 'INVITE') {
|
} else if (compareTag === 'INVITE') {
|
||||||
|
|
Loading…
Reference in New Issue