add notification settings

This commit is contained in:
keyan 2022-04-21 17:50:02 -05:00
parent d91eb02c74
commit 188230c37c
11 changed files with 257 additions and 123 deletions

View File

@ -64,27 +64,45 @@ export default {
// HACK to make notifications faster, we only return a limited sub set of the unioned // HACK to make notifications faster, we only return a limited sub set of the unioned
// queries ... we only ever need at most LIMIT+current offset in the child queries to // queries ... we only ever need at most LIMIT+current offset in the child queries to
// have enough items to return in the union // have enough items to return in the union
const notifications = await models.$queryRaw(
inc === 'replies' const queries = []
? `SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
if (inc === 'replies') {
queries.push(
`SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
'Reply' AS type 'Reply' AS type
FROM "Item" FROM "Item"
JOIN "Item" p ON "Item".path <@ p.path JOIN "Item" p ON ${me.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
WHERE p."userId" = $1
AND "Item"."userId" <> $1 AND "Item".created_at <= $2`
)
} else {
queries.push(
`(SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
'Reply' AS type
FROM "Item"
JOIN "Item" p ON ${me.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
WHERE p."userId" = $1 WHERE p."userId" = $1
AND "Item"."userId" <> $1 AND "Item".created_at <= $2 AND "Item"."userId" <> $1 AND "Item".created_at <= $2
ORDER BY "sortTime" DESC ORDER BY "sortTime" DESC
OFFSET $3 LIMIT ${LIMIT}+$3)`
LIMIT ${LIMIT}` )
: `(SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
'Reply' AS type queries.push(
`(SELECT "Item".id::text, "Item"."statusUpdatedAt" AS "sortTime", NULL as "earnedSats",
'JobChanged' AS type
FROM "Item" FROM "Item"
JOIN "Item" p ON "Item".path <@ p.path WHERE "Item"."userId" = $1
WHERE p."userId" = $1 AND "maxBid" IS NOT NULL
AND "Item"."userId" <> $1 AND "Item".created_at <= $2 AND status <> 'STOPPED'
AND "statusUpdatedAt" <= $2
ORDER BY "sortTime" DESC ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3) LIMIT ${LIMIT}+$3)`
UNION ALL )
(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
if (me.noteItemSats) {
queries.push(
`(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
sum("ItemAct".sats) as "earnedSats", 'Votification' AS type sum("ItemAct".sats) as "earnedSats", 'Votification' AS type
FROM "Item" FROM "Item"
JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id
@ -94,9 +112,26 @@ export default {
AND "Item"."userId" = $1 AND "Item"."userId" = $1
GROUP BY "Item".id GROUP BY "Item".id
ORDER BY "sortTime" DESC ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3) LIMIT ${LIMIT}+$3)`
UNION ALL )
(SELECT "Item".id::TEXT, "Mention".created_at AS "sortTime", NULL as "earnedSats", }
if (me.noteEarning) {
queries.push(
`(SELECT "Earn".id::text, "Earn".created_at AS "sortTime", FLOOR(msats / 1000) as "earnedSats",
'Earn' AS type
FROM "Earn"
WHERE "Earn"."userId" = $1
AND FLOOR(msats / 1000) > 0
AND created_at <= $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
)
}
if (me.noteMentions) {
queries.push(
`(SELECT "Item".id::TEXT, "Mention".created_at AS "sortTime", NULL as "earnedSats",
'Mention' AS type 'Mention' AS type
FROM "Mention" FROM "Mention"
JOIN "Item" ON "Mention"."itemId" = "Item".id JOIN "Item" ON "Mention"."itemId" = "Item".id
@ -106,44 +141,39 @@ export default {
AND "Item"."userId" <> $1 AND "Item"."userId" <> $1
AND (p."userId" IS NULL OR p."userId" <> $1) AND (p."userId" IS NULL OR p."userId" <> $1)
ORDER BY "sortTime" DESC ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3) LIMIT ${LIMIT}+$3)`
UNION ALL )
(SELECT "Invite".id, MAX(users.created_at) AS "sortTime", NULL as "earnedSats", }
'Invitification' AS type
FROM users JOIN "Invite" on users."inviteId" = "Invite".id if (me.noteDeposits) {
WHERE "Invite"."userId" = $1 queries.push(
AND users.created_at <= $2 `(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime", FLOOR("msatsReceived" / 1000) as "earnedSats",
GROUP BY "Invite".id
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)
UNION ALL
(SELECT "Item".id::text, "Item"."statusUpdatedAt" AS "sortTime", NULL as "earnedSats",
'JobChanged' AS type
FROM "Item"
WHERE "Item"."userId" = $1
AND "maxBid" IS NOT NULL
AND status <> 'STOPPED'
AND "statusUpdatedAt" <= $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)
UNION ALL
(SELECT "Earn".id::text, "Earn".created_at AS "sortTime", FLOOR(msats / 1000) as "earnedSats",
'Earn' AS type
FROM "Earn"
WHERE "Earn"."userId" = $1
AND FLOOR(msats / 1000) > 0
AND created_at <= $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)
UNION ALL
(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime", FLOOR("msatsReceived" / 1000) as "earnedSats",
'InvoicePaid' AS type 'InvoicePaid' AS type
FROM "Invoice" FROM "Invoice"
WHERE "Invoice"."userId" = $1 WHERE "Invoice"."userId" = $1
AND "confirmedAt" IS NOT NULL AND "confirmedAt" IS NOT NULL
AND created_at <= $2 AND created_at <= $2
ORDER BY "sortTime" DESC ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3) LIMIT ${LIMIT}+$3)`
)
}
if (me.noteInvites) {
queries.push(
`(SELECT "Invite".id, MAX(users.created_at) AS "sortTime", NULL as "earnedSats",
'Invitification' AS type
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
WHERE "Invite"."userId" = $1
AND users.created_at <= $2
GROUP BY "Invite".id
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
)
}
}
const notifications = await models.$queryRaw(
`${queries.join(' UNION ALL ')}
ORDER BY "sortTime" DESC ORDER BY "sortTime" DESC
OFFSET $3 OFFSET $3
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset) LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)

View File

@ -98,12 +98,12 @@ export default {
throw error throw error
} }
}, },
setSettings: async (parent, { tipDefault }, { me, models }) => { setSettings: async (parent, data, { me, models }) => {
if (!me) { if (!me) {
throw new AuthenticationError('you must be logged in') throw new AuthenticationError('you must be logged in')
} }
await models.user.update({ where: { id: me.id }, data: { tipDefault } }) await models.user.update({ where: { id: me.id }, data })
return true return true
}, },
@ -172,9 +172,11 @@ export default {
}) })
return !!anInvite return !!anInvite
}, },
hasNewNotes: async (user, args, { models }) => { hasNewNotes: async (user, args, { me, models }) => {
// check if any votes have been cast for them since checkedNotesAt
const lastChecked = user.checkedNotesAt || new Date(0) const lastChecked = user.checkedNotesAt || new Date(0)
// check if any votes have been cast for them since checkedNotesAt
if (me.noteItemSats) {
const votes = await models.$queryRaw(` const votes = await models.$queryRaw(`
SELECT "ItemAct".id, "ItemAct".created_at SELECT "ItemAct".id, "ItemAct".created_at
FROM "Item" FROM "Item"
@ -187,12 +189,13 @@ export default {
if (votes.length > 0) { if (votes.length > 0) {
return true return true
} }
}
// check if they have any replies since checkedNotesAt // check if they have any replies since checkedNotesAt
const newReplies = await models.$queryRaw(` const newReplies = await models.$queryRaw(`
SELECT "Item".id, "Item".created_at SELECT "Item".id, "Item".created_at
FROM "Item" FROM "Item"
JOIN "Item" p ON "Item".path <@ p.path JOIN "Item" p ON ${me.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
WHERE p."userId" = $1 WHERE p."userId" = $1
AND "Item".created_at > $2 AND "Item"."userId" <> $1 AND "Item".created_at > $2 AND "Item"."userId" <> $1
LIMIT 1`, user.id, lastChecked) LIMIT 1`, user.id, lastChecked)
@ -201,6 +204,7 @@ export default {
} }
// check if they have any mentions since checkedNotesAt // check if they have any mentions since checkedNotesAt
if (me.noteMentions) {
const newMentions = await models.$queryRaw(` const newMentions = await models.$queryRaw(`
SELECT "Item".id, "Item".created_at SELECT "Item".id, "Item".created_at
FROM "Mention" FROM "Mention"
@ -212,6 +216,7 @@ export default {
if (newMentions.length > 0) { if (newMentions.length > 0) {
return true return true
} }
}
const job = await models.item.findFirst({ const job = await models.item.findFirst({
where: { where: {
@ -231,6 +236,7 @@ export default {
return true return true
} }
if (me.noteEarning) {
const earn = await models.earn.findFirst({ const earn = await models.earn.findFirst({
where: { where: {
userId: user.id, userId: user.id,
@ -245,7 +251,9 @@ export default {
if (earn) { if (earn) {
return true return true
} }
}
if (me.noteDeposits) {
const invoice = await models.invoice.findFirst({ const invoice = await models.invoice.findFirst({
where: { where: {
userId: user.id, userId: user.id,
@ -257,15 +265,22 @@ export default {
if (invoice) { if (invoice) {
return true return true
} }
}
// check if new invites have been redeemed // check if new invites have been redeemed
if (me.noteInvites) {
const newInvitees = await models.$queryRaw(` const newInvitees = await models.$queryRaw(`
SELECT "Invite".id SELECT "Invite".id
FROM users JOIN "Invite" on users."inviteId" = "Invite".id FROM users JOIN "Invite" on users."inviteId" = "Invite".id
WHERE "Invite"."userId" = $1 WHERE "Invite"."userId" = $1
AND users.created_at > $2 AND users.created_at > $2
LIMIT 1`, user.id, lastChecked) LIMIT 1`, user.id, lastChecked)
return newInvitees.length > 0 if (newInvitees.length > 0) {
return true
}
}
return false
} }
} }
} }

View File

@ -22,7 +22,9 @@ export default async function getSSRApolloClient (req, me = null) {
}), }),
context: { context: {
models, models,
me: session ? session.user : me, me: session
? await models.user.findUnique({ where: { id: session.user?.id } })
: me,
lnd, lnd,
search search
} }
@ -42,10 +44,12 @@ export function getGetServerSideProps (query, variables = null, notFoundFunc, re
} }
} }
const { error, data } = await client.query({ let error = null; let data = null; let props = {}
if (query) {
({ error, data } = await client.query({
query, query,
variables: vars variables: vars
}) }))
if (error || !data || (notFoundFunc && notFoundFunc(data))) { if (error || !data || (notFoundFunc && notFoundFunc(data))) {
return { return {
@ -53,6 +57,14 @@ export function getGetServerSideProps (query, variables = null, notFoundFunc, re
} }
} }
props = {
apollo: {
query: print(query),
variables: { ...params, ...variables }
}
}
}
const { data: { me } } = await client.query({ const { data: { me } } = await client.query({
query: ME query: ME
}) })
@ -61,10 +73,7 @@ export function getGetServerSideProps (query, variables = null, notFoundFunc, re
return { return {
props: { props: {
apollo: { ...props,
query: print(query),
variables: { ...params, ...variables }
},
me, me,
price, price,
data data

View File

@ -27,7 +27,9 @@ export default gql`
extend type Mutation { extend type Mutation {
setName(name: String!): Boolean setName(name: String!): Boolean
setSettings(tipDefault: Int!): Boolean setSettings(tipDefault: Int!, noteItemSats: Boolean!, noteEarning: Boolean!,
noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
noteInvites:Boolean!): Boolean
upsertBio(bio: String!): User! upsertBio(bio: String!): User!
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
} }
@ -48,5 +50,11 @@ export default gql`
sats: Int! sats: Int!
upvotePopover: Boolean! upvotePopover: Boolean!
tipPopover: Boolean! tipPopover: Boolean!
noteItemSats: Boolean!
noteEarning: Boolean!
noteAllDescendants: Boolean!
noteMentions: Boolean!
noteDeposits: Boolean!
noteInvites: Boolean!
} }
` `

View File

@ -19,6 +19,12 @@ export const ME = gql`
hasInvites hasInvites
upvotePopover upvotePopover
tipPopover tipPopover
noteItemSats
noteEarning
noteAllDescendants
noteMentions
noteDeposits
noteInvites
} }
}` }`

View File

@ -15,7 +15,9 @@ global.apolloServer ||= new ApolloServer({
return { return {
models, models,
lnd, lnd,
me: session ? session.user : null, me: session
? await models.user.findUnique({ where: { id: session.user?.id } })
: null,
search search
} }
} }

View File

@ -5,6 +5,9 @@ import LayoutCenter from '../components/layout-center'
import { useMe } from '../components/me' import { useMe } from '../components/me'
import { DiscussionForm } from '../components/discussion-form' import { DiscussionForm } from '../components/discussion-form'
import { LinkForm } from '../components/link-form' import { LinkForm } from '../components/link-form'
import { getGetServerSideProps } from '../api/ssrApollo'
export const getServerSideProps = getGetServerSideProps()
export function PostForm () { export function PostForm () {
const router = useRouter() const router = useRouter()

View File

@ -1,10 +1,13 @@
import { Form, Input, SubmitButton } from '../components/form' import { Checkbox, Form, Input, SubmitButton } from '../components/form'
import * as Yup from 'yup' import * as Yup from 'yup'
import { Alert, InputGroup } from 'react-bootstrap' import { Alert, InputGroup } from 'react-bootstrap'
import { useMe } from '../components/me' import { useMe } from '../components/me'
import LayoutCenter from '../components/layout-center' import LayoutCenter from '../components/layout-center'
import { useState } from 'react' import { useState } from 'react'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import { getGetServerSideProps } from '../api/ssrApollo'
export const getServerSideProps = getGetServerSideProps()
export const SettingsSchema = Yup.object({ export const SettingsSchema = Yup.object({
tipDefault: Yup.number().typeError('must be a number').required('required') tipDefault: Yup.number().typeError('must be a number').required('required')
@ -16,8 +19,12 @@ export default function Settings () {
const [success, setSuccess] = useState() const [success, setSuccess] = useState()
const [setSettings] = useMutation( const [setSettings] = useMutation(
gql` gql`
mutation setSettings($tipDefault: Int!) { mutation setSettings($tipDefault: Int!, $noteItemSats: Boolean!, $noteEarning: Boolean!,
setSettings(tipDefault: $tipDefault) $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
$noteInvites: Boolean!) {
setSettings(tipDefault: $tipDefault, noteItemSats: $noteItemSats,
noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites)
}` }`
) )
@ -26,11 +33,17 @@ export default function Settings () {
<h2 className='mb-5 text-left'>settings</h2> <h2 className='mb-5 text-left'>settings</h2>
<Form <Form
initial={{ initial={{
tipDefault: me?.tipDefault || 21 tipDefault: me?.tipDefault || 21,
noteItemSats: me?.noteItemSats,
noteEarning: me?.noteEarning,
noteAllDescendants: me?.noteAllDescendants,
noteMentions: me?.noteMentions,
noteDeposits: me?.noteDeposits,
noteInvites: me?.noteInvites
}} }}
schema={SettingsSchema} schema={SettingsSchema}
onSubmit={async ({ tipDefault }) => { onSubmit={async ({ tipDefault, ...values }) => {
await setSettings({ variables: { tipDefault: Number(tipDefault) } }) await setSettings({ variables: { tipDefault: Number(tipDefault), ...values } })
setSuccess('settings saved') setSuccess('settings saved')
}} }}
> >
@ -42,6 +55,36 @@ export default function Settings () {
autoFocus autoFocus
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/> />
<div className='form-label'>notify me when ...</div>
<Checkbox
label='I stack sats from posts and comments'
name='noteItemSats'
groupClassName='mb-0'
/>
<Checkbox
label='I get a daily airdrops'
name='noteEarning'
groupClassName='mb-0'
/>
<Checkbox
label='someone replies to someone who replied to me'
name='noteAllDescendants'
groupClassName='mb-0'
/>
<Checkbox
label='my invite links are redeemed'
name='noteInvites'
groupClassName='mb-0'
/>
<Checkbox
label='sats are deposited in my account'
name='noteDeposits'
groupClassName='mb-0'
/>
<Checkbox
label='someone mentions me'
name='noteMentions'
/>
<div className='d-flex'> <div className='d-flex'>
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton> <SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>
</div> </div>

View File

@ -13,6 +13,9 @@ import { useEffect, useState } from 'react'
import { requestProvider } from 'webln' import { requestProvider } from 'webln'
import { Alert } from 'react-bootstrap' import { Alert } from 'react-bootstrap'
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet' import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
import { getGetServerSideProps } from '../api/ssrApollo'
export const getServerSideProps = getGetServerSideProps()
export default function Wallet () { export default function Wallet () {
return ( return (

View File

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "noteAllDescendants" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "noteDeposits" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "noteEarning" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "noteInvites" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "noteItemSats" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "noteMentions" BOOLEAN NOT NULL DEFAULT true;

View File

@ -43,6 +43,14 @@ model User {
upvotePopover Boolean @default(false) upvotePopover Boolean @default(false)
tipPopover Boolean @default(false) tipPopover Boolean @default(false)
// notification settings
noteItemSats Boolean @default(true)
noteEarning Boolean @default(true)
noteAllDescendants Boolean @default(true)
noteMentions Boolean @default(true)
noteDeposits Boolean @default(true)
noteInvites Boolean @default(true)
Earn Earn[] Earn Earn[]
@@index([createdAt]) @@index([createdAt])
@@index([inviteId]) @@index([inviteId])