Skip to content

Commit

Permalink
Item mention notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
ekzyis committed May 31, 2024
1 parent 7e3b813 commit a059d8f
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 4 deletions.
43 changes: 41 additions & 2 deletions api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
}

Expand Down
20 changes: 20 additions & 0 deletions api/resolvers/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)`
)
Expand Down Expand Up @@ -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 })
},
Expand Down
20 changes: 20 additions & 0 deletions api/resolvers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 7 additions & 1 deletion api/typeDefs/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export default gql`
sortTime: Date!
}
type ItemMention {
id: ID!
item: Item!
sortTime: Date!
}
type Invitification {
id: ID!
invite: Invite!
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions api/typeDefs/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export default gql`
noteItemSats: Boolean!
noteJobIndicator: Boolean!
noteMentions: Boolean!
noteItemMentions: Boolean!
nsfwMode: Boolean!
tipDefault: Int!
turboTipping: Boolean!
Expand Down Expand Up @@ -161,6 +162,7 @@ export default gql`
noteItemSats: Boolean!
noteJobIndicator: Boolean!
noteMentions: Boolean!
noteItemMentions: Boolean!
nsfwMode: Boolean!
tipDefault: Int!
turboTipping: Boolean!
Expand Down
21 changes: 21 additions & 0 deletions components/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function Notification ({ n, fresh }) {
(type === 'Votification' && <Votification n={n} />) ||
(type === 'ForwardedVotification' && <ForwardedVotification n={n} />) ||
(type === 'Mention' && <Mention n={n} />) ||
(type === 'ItemMention' && <ItemMention n={n} />) ||
(type === 'JobChanged' && <JobChanged n={n} />) ||
(type === 'Reply' && <Reply n={n} />) ||
(type === 'SubStatus' && <SubStatus n={n} />) ||
Expand Down Expand Up @@ -391,6 +392,26 @@ function Mention ({ n }) {
)
}

function ItemMention ({ n }) {
return (
<>
<small className='fw-bold text-info ms-2'>
one of your {n.title ? 'posts' : 'comments'} was mentioned
</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={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
</RootProvider>
</div>)}
</div>
</>
)
}

function JobChanged ({ n }) {
return (
<>
Expand Down
8 changes: 8 additions & 0 deletions fragments/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export const NOTIFICATIONS = gql`
text
}
}
... on ItemMention {
id
sortTime
item {
...ItemFullFields
text
}
}
... on Votification {
id
sortTime
Expand Down
2 changes: 2 additions & 0 deletions fragments/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const ME = gql`
noteItemSats
noteJobIndicator
noteMentions
noteItemMentions
sats
tipDefault
tipPopover
Expand Down Expand Up @@ -73,6 +74,7 @@ export const SETTINGS_FIELDS = gql`
noteEarning
noteAllDescendants
noteMentions
noteItemMentions
noteDeposits
noteWithdrawals
noteInvites
Expand Down
22 changes: 22 additions & 0 deletions lib/webPush.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const createUserFilter = (tag) => {
const tagMap = {
REPLY: 'noteAllDescendants',
MENTION: 'noteMentions',
ITEM_MENTION: 'noteItemMentions',
TIP: 'noteItemSats',
FORWARDEDTIP: 'noteForwardedSats',
REFERRAL: 'noteInvites',
Expand Down Expand Up @@ -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' })
Expand Down
6 changes: 6 additions & 0 deletions pages/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -281,6 +282,11 @@ export default function Settings ({ ssrData }) {
name='noteMentions'
groupClassName='mb-0'
/>
<Checkbox
label='someone mentions one of my posts or comments'
name='noteItemMentions'
groupClassName='mb-0'
/>
<Checkbox
label='there is a new job'
name='noteJobIndicator'
Expand Down
31 changes: 31 additions & 0 deletions prisma/migrations/20240529105359_item_mentions/migration.sql
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 18 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ model User {
noteInvites Boolean @default(true)
noteItemSats Boolean @default(true)
noteMentions Boolean @default(true)
noteItemMentions Boolean @default(true)
noteForwardedSats Boolean @default(true)
lastCheckedJobs DateTime?
noteJobIndicator Boolean @default(true)
Expand Down Expand Up @@ -411,6 +412,8 @@ model Item {
user User @relation("UserItems", fields: [userId], references: [id], onDelete: Cascade)
actions ItemAct[]
mentions Mention[]
referrer ItemMention[] @relation("referrer")
referee ItemMention[] @relation("referee")
PollOption PollOption[]
PollVote PollVote[]
ThreadSubscription ThreadSubscription[]
Expand Down Expand Up @@ -660,6 +663,21 @@ model Mention {
@@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 {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
Expand Down
4 changes: 3 additions & 1 deletion sw/eventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const mergeAndShowNotification = async (sw, payload, currentNotifications, tag,
// merge notifications into single notification payload
// ---
// 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
const SUM_SATS_TAGS = ['DEPOSIT', 'WITHDRAWAL']
// this should reflect the amount of notifications that were already merged before
Expand Down Expand Up @@ -143,6 +143,8 @@ const mergeAndShowNotification = async (sw, payload, currentNotifications, tag,
title = `you have ${amount} new replies`
} else if (compareTag === 'MENTION') {
title = `you were mentioned ${amount} times`
} else if (compareTag === 'ITEM_MENTION') {
title = `your ${subType === 'POST' ? 'posts' : 'comments'} were mentioned ${amount} times`
} else if (compareTag === 'REFERRAL') {
title = `${amount} stackers joined via your referral links`
} else if (compareTag === 'INVITE') {
Expand Down

0 comments on commit a059d8f

Please sign in to comment.