notify user when invoice is paid
This commit is contained in:
parent
69155139e6
commit
987a5ed3a3
|
@ -1,6 +1,7 @@
|
||||||
import { AuthenticationError } from 'apollo-server-micro'
|
import { AuthenticationError } from 'apollo-server-micro'
|
||||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||||
import { getItem } from './item'
|
import { getItem } from './item'
|
||||||
|
import { getInvoice } from './wallet'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
|
@ -70,6 +71,7 @@ export default {
|
||||||
// at most 25 ancesestors belonging to the same user for a given reply, see: (LIMIT+OFFSET)*25 which is
|
// at most 25 ancesestors belonging to the same user for a given reply, see: (LIMIT+OFFSET)*25 which is
|
||||||
// undoubtably the case today ... this probably won't hold indefinitely though
|
// undoubtably the case today ... this probably won't hold indefinitely though
|
||||||
// One other less HACKy way to do this is to store in each reply, an set of users it's in response to
|
// One other less HACKy way to do this is to store in each reply, an set of users it's in response to
|
||||||
|
// or to simply denormalize the replies (simply the ids which we wouldn't have to be concerned about consitency)
|
||||||
const notifications = await models.$queryRaw(`
|
const notifications = await models.$queryRaw(`
|
||||||
SELECT DISTINCT *
|
SELECT DISTINCT *
|
||||||
FROM
|
FROM
|
||||||
|
@ -128,8 +130,17 @@ export default {
|
||||||
(SELECT "Earn".id::text, "Earn".created_at AS "sortTime", FLOOR(msats / 1000) as "earnedSats",
|
(SELECT "Earn".id::text, "Earn".created_at AS "sortTime", FLOOR(msats / 1000) as "earnedSats",
|
||||||
'Earn' AS type
|
'Earn' AS type
|
||||||
FROM "Earn"
|
FROM "Earn"
|
||||||
WHERE "Earn"."userId" = $1 AND
|
WHERE "Earn"."userId" = $1
|
||||||
FLOOR(msats / 1000) > 0
|
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
|
||||||
|
FROM "Invoice"
|
||||||
|
WHERE "Invoice"."userId" = $1
|
||||||
|
AND "confirmedAt" IS NOT NULL
|
||||||
AND created_at <= $2
|
AND created_at <= $2
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT}+$3)) AS n
|
LIMIT ${LIMIT}+$3)) AS n
|
||||||
|
@ -165,6 +176,9 @@ export default {
|
||||||
mention: async (n, args, { models }) => true,
|
mention: async (n, args, { models }) => true,
|
||||||
item: async (n, args, { models }) => getItem(n, { id: n.id }, { models })
|
item: async (n, args, { models }) => getItem(n, { id: n.id }, { models })
|
||||||
},
|
},
|
||||||
|
InvoicePaid: {
|
||||||
|
invoice: async (n, args, { me, models }) => getInvoice(n, { id: n.id }, { me, models })
|
||||||
|
},
|
||||||
Invitification: {
|
Invitification: {
|
||||||
invite: async (n, args, { models }) => {
|
invite: async (n, args, { models }) => {
|
||||||
return await models.invite.findUnique({
|
return await models.invite.findUnique({
|
||||||
|
|
|
@ -260,6 +260,18 @@ export default {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const invoice = await models.invoice.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
confirmedAt: {
|
||||||
|
gt: user.checkedNotesAt || new Date(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (invoice) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// check if new invites have been redeemed
|
// check if new invites have been redeemed
|
||||||
const newInvitees = await models.$queryRaw(`
|
const newInvitees = await models.$queryRaw(`
|
||||||
SELECT "Invite".id
|
SELECT "Invite".id
|
||||||
|
|
|
@ -5,28 +5,30 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||||
import lnpr from 'bolt11'
|
import lnpr from 'bolt11'
|
||||||
import { SELECT } from './item'
|
import { SELECT } from './item'
|
||||||
|
|
||||||
|
export async function getInvoice (parent, { id }, { me, models }) {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
|
||||||
|
const inv = await models.invoice.findUnique({
|
||||||
|
where: {
|
||||||
|
id: Number(id)
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (inv.user.id !== me.id) {
|
||||||
|
throw new AuthenticationError('not ur invoice')
|
||||||
|
}
|
||||||
|
|
||||||
|
return inv
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
invoice: async (parent, { id }, { me, models, lnd }) => {
|
invoice: getInvoice,
|
||||||
if (!me) {
|
|
||||||
throw new AuthenticationError('you must be logged in')
|
|
||||||
}
|
|
||||||
|
|
||||||
const inv = await models.invoice.findUnique({
|
|
||||||
where: {
|
|
||||||
id: Number(id)
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (inv.user.id !== me.id) {
|
|
||||||
throw new AuthenticationError('not ur invoice')
|
|
||||||
}
|
|
||||||
|
|
||||||
return inv
|
|
||||||
},
|
|
||||||
withdrawl: async (parent, { id }, { me, models, lnd }) => {
|
withdrawl: async (parent, { id }, { me, models, lnd }) => {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new AuthenticationError('you must be logged in')
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
|
|
@ -37,8 +37,14 @@ export default gql`
|
||||||
sortTime: String!
|
sortTime: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvoicePaid {
|
||||||
|
earnedSats: Int!
|
||||||
|
invoice: Invoice!
|
||||||
|
sortTime: String!
|
||||||
|
}
|
||||||
|
|
||||||
union Notification = Reply | Votification | Mention
|
union Notification = Reply | Votification | Mention
|
||||||
| Invitification | JobChanged | Earn
|
| Invitification | JobChanged | Earn | InvoicePaid
|
||||||
|
|
||||||
type Notifications {
|
type Notifications {
|
||||||
lastChecked: String
|
lastChecked: String
|
||||||
|
|
|
@ -7,7 +7,9 @@ import MoreFooter from './more-footer'
|
||||||
import Invite from './invite'
|
import Invite from './invite'
|
||||||
import { ignoreClick } from '../lib/clicks'
|
import { ignoreClick } from '../lib/clicks'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import Check from '../svgs/check-double-line.svg'
|
||||||
|
|
||||||
|
// TODO: oh man, this is a mess ... each notification type should just be a component ...
|
||||||
function Notification ({ n }) {
|
function Notification ({ n }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
|
@ -22,7 +24,9 @@ function Notification ({ n }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (n.__typename === 'Invitification') {
|
if (n.__typename === 'InvoicePaid') {
|
||||||
|
router.push(`/invoices/${n.invoice.id}`)
|
||||||
|
} else if (n.__typename === 'Invitification') {
|
||||||
router.push('/invites')
|
router.push('/invites')
|
||||||
} else if (!n.item.title) {
|
} else if (!n.item.title) {
|
||||||
router.push({
|
router.push({
|
||||||
|
@ -64,33 +68,35 @@ function Notification ({ n }) {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (
|
: n.__typename === 'InvoicePaid'
|
||||||
<>
|
? <div className='font-weight-bold text-info ml-2 py-1'><Check className='fill-info mr-1' />{n.earnedSats} sats were deposited in your account</div>
|
||||||
{n.__typename === 'Votification' &&
|
: (
|
||||||
<small className='font-weight-bold text-success ml-2'>
|
<>
|
||||||
your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
|
{n.__typename === 'Votification' &&
|
||||||
</small>}
|
<small className='font-weight-bold text-success ml-2'>
|
||||||
{n.__typename === 'Mention' &&
|
your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
|
||||||
<small className='font-weight-bold text-info ml-2'>
|
</small>}
|
||||||
you were mentioned in
|
{n.__typename === 'Mention' &&
|
||||||
</small>}
|
<small className='font-weight-bold text-info ml-2'>
|
||||||
{n.__typename === 'JobChanged' &&
|
you were mentioned in
|
||||||
<small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}>
|
</small>}
|
||||||
{n.item.status === 'NOSATS'
|
{n.__typename === 'JobChanged' &&
|
||||||
? 'your job ran out of sats'
|
<small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}>
|
||||||
: 'your job is active again'}
|
{n.item.status === 'NOSATS'
|
||||||
</small>}
|
? 'your job ran out of sats'
|
||||||
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
|
: 'your job is active again'}
|
||||||
{n.item.maxBid
|
</small>}
|
||||||
? <ItemJob item={n.item} />
|
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
|
||||||
: n.item.title
|
{n.item.maxBid
|
||||||
? <Item item={n.item} />
|
? <ItemJob item={n.item} />
|
||||||
: (
|
: n.item.title
|
||||||
<div className='pb-2'>
|
? <Item item={n.item} />
|
||||||
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
: (
|
||||||
</div>)}
|
<div className='pb-2'>
|
||||||
</div>
|
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
||||||
</>)}
|
</div>)}
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,13 @@ export const NOTIFICATIONS = gql`
|
||||||
sortTime
|
sortTime
|
||||||
earnedSats
|
earnedSats
|
||||||
}
|
}
|
||||||
|
... on InvoicePaid {
|
||||||
|
sortTime
|
||||||
|
earnedSats
|
||||||
|
invoice {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} `
|
} `
|
||||||
|
|
|
@ -380,11 +380,15 @@ textarea.form-control {
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill-success {
|
.fill-success {
|
||||||
fill: #5c8001;
|
fill: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-info {
|
||||||
|
fill: var(--info);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill-danger {
|
.fill-danger {
|
||||||
fill: #c03221;
|
fill: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill-theme-color {
|
.fill-theme-color {
|
||||||
|
|
Loading…
Reference in New Issue