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
// queries ... we only ever need at most LIMIT+current offset in the child queries to
// have enough items to return in the union
const notifications = await models.$queryRaw(
inc === 'replies'
? `SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
const queries = []
if (inc === 'replies') {
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 "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
AND "Item"."userId" <> $1 AND "Item".created_at <= $2
ORDER BY "sortTime" DESC
OFFSET $3
LIMIT ${LIMIT}`
: `(SELECT DISTINCT "Item".id::TEXT, "Item".created_at AS "sortTime", NULL::BIGINT as "earnedSats",
'Reply' AS type
LIMIT ${LIMIT}+$3)`
)
queries.push(
`(SELECT "Item".id::text, "Item"."statusUpdatedAt" AS "sortTime", NULL as "earnedSats",
'JobChanged' AS type
FROM "Item"
JOIN "Item" p ON "Item".path <@ p.path
WHERE p."userId" = $1
AND "Item"."userId" <> $1 AND "Item".created_at <= $2
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 "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
LIMIT ${LIMIT}+$3)`
)
if (me.noteItemSats) {
queries.push(
`(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
sum("ItemAct".sats) as "earnedSats", 'Votification' AS type
FROM "Item"
JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id
@ -94,9 +112,26 @@ export default {
AND "Item"."userId" = $1
GROUP BY "Item".id
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)
UNION ALL
(SELECT "Item".id::TEXT, "Mention".created_at AS "sortTime", NULL as "earnedSats",
LIMIT ${LIMIT}+$3)`
)
}
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
FROM "Mention"
JOIN "Item" ON "Mention"."itemId" = "Item".id
@ -106,44 +141,39 @@ export default {
AND "Item"."userId" <> $1
AND (p."userId" IS NULL OR p."userId" <> $1)
ORDER BY "sortTime" DESC
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
WHERE "Invite"."userId" = $1
AND users.created_at <= $2
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",
LIMIT ${LIMIT}+$3)`
)
}
if (me.noteDeposits) {
queries.push(
`(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime", FLOOR("msatsReceived" / 1000) as "earnedSats",
'InvoicePaid' AS type
FROM "Invoice"
WHERE "Invoice"."userId" = $1
AND "confirmedAt" IS NOT NULL
AND created_at <= $2
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
OFFSET $3
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)

View File

@ -98,12 +98,12 @@ export default {
throw error
}
},
setSettings: async (parent, { tipDefault }, { me, models }) => {
setSettings: async (parent, data, { me, models }) => {
if (!me) {
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
},
@ -172,9 +172,11 @@ export default {
})
return !!anInvite
},
hasNewNotes: async (user, args, { models }) => {
// check if any votes have been cast for them since checkedNotesAt
hasNewNotes: async (user, args, { me, models }) => {
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(`
SELECT "ItemAct".id, "ItemAct".created_at
FROM "Item"
@ -187,12 +189,13 @@ export default {
if (votes.length > 0) {
return true
}
}
// check if they have any replies since checkedNotesAt
const newReplies = await models.$queryRaw(`
SELECT "Item".id, "Item".created_at
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".created_at > $2 AND "Item"."userId" <> $1
LIMIT 1`, user.id, lastChecked)
@ -201,6 +204,7 @@ export default {
}
// check if they have any mentions since checkedNotesAt
if (me.noteMentions) {
const newMentions = await models.$queryRaw(`
SELECT "Item".id, "Item".created_at
FROM "Mention"
@ -212,6 +216,7 @@ export default {
if (newMentions.length > 0) {
return true
}
}
const job = await models.item.findFirst({
where: {
@ -231,6 +236,7 @@ export default {
return true
}
if (me.noteEarning) {
const earn = await models.earn.findFirst({
where: {
userId: user.id,
@ -245,7 +251,9 @@ export default {
if (earn) {
return true
}
}
if (me.noteDeposits) {
const invoice = await models.invoice.findFirst({
where: {
userId: user.id,
@ -257,15 +265,22 @@ export default {
if (invoice) {
return true
}
}
// check if new invites have been redeemed
if (me.noteInvites) {
const newInvitees = await models.$queryRaw(`
SELECT "Invite".id
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
WHERE "Invite"."userId" = $1
AND users.created_at > $2
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: {
models,
me: session ? session.user : me,
me: session
? await models.user.findUnique({ where: { id: session.user?.id } })
: me,
lnd,
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,
variables: vars
})
}))
if (error || !data || (notFoundFunc && notFoundFunc(data))) {
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({
query: ME
})
@ -61,10 +73,7 @@ export function getGetServerSideProps (query, variables = null, notFoundFunc, re
return {
props: {
apollo: {
query: print(query),
variables: { ...params, ...variables }
},
...props,
me,
price,
data

View File

@ -27,7 +27,9 @@ export default gql`
extend type Mutation {
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!
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
}
@ -48,5 +50,11 @@ export default gql`
sats: Int!
upvotePopover: 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
upvotePopover
tipPopover
noteItemSats
noteEarning
noteAllDescendants
noteMentions
noteDeposits
noteInvites
}
}`

View File

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

View File

@ -5,6 +5,9 @@ import LayoutCenter from '../components/layout-center'
import { useMe } from '../components/me'
import { DiscussionForm } from '../components/discussion-form'
import { LinkForm } from '../components/link-form'
import { getGetServerSideProps } from '../api/ssrApollo'
export const getServerSideProps = getGetServerSideProps()
export function PostForm () {
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 { Alert, InputGroup } from 'react-bootstrap'
import { useMe } from '../components/me'
import LayoutCenter from '../components/layout-center'
import { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import { getGetServerSideProps } from '../api/ssrApollo'
export const getServerSideProps = getGetServerSideProps()
export const SettingsSchema = Yup.object({
tipDefault: Yup.number().typeError('must be a number').required('required')
@ -16,8 +19,12 @@ export default function Settings () {
const [success, setSuccess] = useState()
const [setSettings] = useMutation(
gql`
mutation setSettings($tipDefault: Int!) {
setSettings(tipDefault: $tipDefault)
mutation setSettings($tipDefault: Int!, $noteItemSats: Boolean!, $noteEarning: Boolean!,
$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>
<Form
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}
onSubmit={async ({ tipDefault }) => {
await setSettings({ variables: { tipDefault: Number(tipDefault) } })
onSubmit={async ({ tipDefault, ...values }) => {
await setSettings({ variables: { tipDefault: Number(tipDefault), ...values } })
setSuccess('settings saved')
}}
>
@ -42,6 +55,36 @@ export default function Settings () {
autoFocus
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'>
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>
</div>

View File

@ -13,6 +13,9 @@ import { useEffect, useState } from 'react'
import { requestProvider } from 'webln'
import { Alert } from 'react-bootstrap'
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
import { getGetServerSideProps } from '../api/ssrApollo'
export const getServerSideProps = getGetServerSideProps()
export default function Wallet () {
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)
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[]
@@index([createdAt])
@@index([inviteId])