Territory post notifications (#745)

* Notify founders of new posts

* Only merge notifications of same territory

* Show territory posts in /notifications

* Don't notify on own posts
This commit is contained in:
ekzyis 2024-01-11 18:27:54 +01:00 committed by GitHub
parent 39c9775c4c
commit bdf9b1f0fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 92 additions and 5 deletions

View File

@ -17,7 +17,7 @@ import uu from 'url-unshort'
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
import { sendUserNotification } from '../webPush'
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '../../lib/item'
import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications'
import { notifyItemParents, notifyUserSubscribers, notifyZapped, notifyFounders } from '../../lib/push-notifications'
import { datePivot, whenRange } from '../../lib/time'
import { imageFeesInfo, uploadIdsFromText } from './image'
import assertGofacYourself from './ofac'
@ -1228,6 +1228,8 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
notifyUserSubscribers({ models, item })
notifyFounders({ models, item })
item.comments = []
return item
}

View File

@ -107,6 +107,17 @@ export default {
LIMIT ${LIMIT}+$3`
)
if (meFull.noteTerritoryPosts) {
itemDrivenQueries.push(
`SELECT "Item".*, "Item".created_at AS "sortTime", 'TerritoryPost' AS type
FROM "Item"
JOIN "Sub" ON "Item"."subName" = "Sub".name
WHERE "Sub"."userId" = $1 AND "Item"."userId" <> $1
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
)
}
// mentions
if (meFull.noteMentions) {
itemDrivenQueries.push(
@ -137,6 +148,7 @@ export default {
WHEN type = 'Mention' THEN 1
WHEN type = 'Reply' THEN 2
WHEN type = 'FollowActivity' THEN 3
WHEN type = 'TerritoryPost' THEN 4
END ASC
)`
)
@ -348,6 +360,9 @@ export default {
FollowActivity: {
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},
TerritoryPost: {
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},
JobChanged: {
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},

View File

@ -102,9 +102,15 @@ export default gql`
sortTime: Date!
}
type TerritoryPost {
id: ID!
item: Item!
sortTime: Date!
}
union Notification = Reply | Votification | Mention
| Invitification | Earn | JobChanged | InvoicePaid | Referral
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus | TerritoryPost
type Notifications {
lastChecked: Date

View File

@ -69,6 +69,7 @@ export default gql`
nostrPubkey: String
nostrRelays: [String!]
noteAllDescendants: Boolean!
noteTerritoryPosts: Boolean!
noteCowboyHat: Boolean!
noteDeposits: Boolean!
noteEarning: Boolean!
@ -125,6 +126,7 @@ export default gql`
nostrPubkey: String
nostrRelays: [String!]
noteAllDescendants: Boolean!
noteTerritoryPosts: Boolean!
noteCowboyHat: Boolean!
noteDeposits: Boolean!
noteEarning: Boolean!

View File

@ -35,6 +35,7 @@ const createUserFilter = (tag) => {
// filter users by notification settings
const tagMap = {
REPLY: 'noteAllDescendants',
TERRITORY_POST: 'noteTerritoryPosts',
MENTION: 'noteMentions',
TIP: 'noteItemSats',
FORWARDEDTIP: 'noteForwardedSats',

View File

@ -47,7 +47,8 @@ function Notification ({ n, fresh }) {
(type === 'JobChanged' && <JobChanged n={n} />) ||
(type === 'Reply' && <Reply n={n} />) ||
(type === 'SubStatus' && <SubStatus n={n} />) ||
(type === 'FollowActivity' && <FollowActivity n={n} />)
(type === 'FollowActivity' && <FollowActivity n={n} />) ||
(type === 'TerritoryPost' && <TerritoryPost n={n} />)
}
</NotificationLayout>
)
@ -419,6 +420,19 @@ function FollowActivity ({ n }) {
)
}
function TerritoryPost ({ n }) {
return (
<>
<small className='fw-bold text-info ms-2'>
new post in ~{n.item.sub.name}
</small>
<div>
<Item item={n.item} />
</div>
</>
)
}
export function NotificationAlert () {
const [showAlert, setShowAlert] = useState(false)
const [hasSubscription, setHasSubscription] = useState(false)

View File

@ -86,6 +86,14 @@ export const NOTIFICATIONS = gql`
text
}
}
... on TerritoryPost {
id
sortTime
item {
...ItemFullFields
text
}
}
... on Invitification {
id
sortTime

View File

@ -23,6 +23,7 @@ export const ME = gql`
lastCheckedJobs
nostrCrossposting
noteAllDescendants
noteTerritoryPosts
noteCowboyHat
noteDeposits
noteEarning
@ -57,6 +58,7 @@ export const SETTINGS_FIELDS = gql`
noteItemSats
noteEarning
noteAllDescendants
noteTerritoryPosts
noteMentions
noteDeposits
noteInvites

View File

@ -96,3 +96,29 @@ export const notifyZapped = async ({ models, id }) => {
console.error(err)
}
}
export const notifyFounders = async ({ models, item }) => {
try {
const isPost = !!item.title
// only notify on posts in subs
if (!isPost || !item.subName) return
const author = await models.user.findUnique({ where: { id: item.userId } })
const sub = await models.sub.findUnique({ where: { name: item.subName } })
// don't send notifications on own posts to founders
if (sub.userId === author.id) return
const tag = `TERRITORY_POST-${sub.name}`
await sendUserNotification(sub.userId, {
title: `@${author.name} created a post in ~${sub.name}`,
body: item.title,
item,
data: { subName: sub.name },
tag
})
} catch (err) {
console.error(err)
}
}

View File

@ -68,6 +68,7 @@ export default function Settings ({ ssrData }) {
noteItemSats: settings?.noteItemSats,
noteEarning: settings?.noteEarning,
noteAllDescendants: settings?.noteAllDescendants,
noteTerritoryPosts: settings?.noteTerritoryPosts,
noteMentions: settings?.noteMentions,
noteDeposits: settings?.noteDeposits,
noteInvites: settings?.noteInvites,
@ -196,6 +197,11 @@ export default function Settings ({ ssrData }) {
name='noteAllDescendants'
groupClassName='mb-0'
/>
<Checkbox
label='someone writes a post in a territory I founded'
name='noteTerritoryPosts'
groupClassName='mb-0'
/>
<Checkbox
label='someone joins using my invite or referral links'
name='noteInvites'

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "noteTerritoryPosts" BOOLEAN NOT NULL DEFAULT true;

View File

@ -35,6 +35,7 @@ model User {
lastSeenAt DateTime?
stackedMsats BigInt @default(0)
noteAllDescendants Boolean @default(true)
noteTerritoryPosts Boolean @default(true)
noteDeposits Boolean @default(true)
noteEarning Boolean @default(true)
noteInvites Boolean @default(true)

View File

@ -113,7 +113,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']
const AMOUNT_TAGS = ['REPLY', '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']
// this should reflect the amount of notifications that were already merged before
@ -135,7 +135,7 @@ const mergeAndShowNotification = async (sw, payload, currentNotifications, tag,
log(`[sw:push] ${nid} - merged payload: ${JSON.stringify(mergedPayload)}`)
// calculate title from merged payload
const { amount, followeeName, subType, sats } = mergedPayload
const { amount, followeeName, subName, subType, sats } = mergedPayload
let title = ''
if (AMOUNT_TAGS.includes(compareTag)) {
if (compareTag === 'REPLY') {
@ -148,6 +148,8 @@ const mergeAndShowNotification = async (sw, payload, currentNotifications, tag,
title = `your invite has been redeemed by ${amount} stackers`
} else if (compareTag === 'FOLLOW') {
title = `@${followeeName} ${subType === 'POST' ? `created ${amount} posts` : `replied ${amount} times`}`
} else if (compareTag === 'TERRITORY_POST') {
title = `you have ${amount} new posts in ~${subName}`
}
} else if (SUM_SATS_TAGS.includes(compareTag)) {
// there is only DEPOSIT in this array