add ln addr + lnurl pay qr code to profile pages
This commit is contained in:
parent
4ba1227605
commit
aa4ac2ecc9
|
@ -868,7 +868,7 @@ function newTimedOrderByWeightedSats (num) {
|
||||||
return `
|
return `
|
||||||
GROUP BY "Item".id
|
GROUP BY "Item".id
|
||||||
ORDER BY (SUM(CASE WHEN "ItemAct".act = 'VOTE' AND "Item"."userId" <> "ItemAct"."userId" THEN users.trust ELSE 0 END)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 1.3) +
|
ORDER BY (SUM(CASE WHEN "ItemAct".act = 'VOTE' AND "Item"."userId" <> "ItemAct"."userId" THEN users.trust ELSE 0 END)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 1.3) +
|
||||||
GREATEST(SUM(CASE WHEN "ItemAct".act = 'BOOST' THEN "ItemAct".sats ELSE 0 END)-1000+5, 0)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 5)) DESC NULLS LAST, "Item".id DESC`
|
GREATEST(SUM(CASE WHEN "ItemAct".act = 'BOOST' THEN "ItemAct".sats ELSE 0 END)-1000+5, 0)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 3)) DESC NULLS LAST, "Item".id DESC`
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOP_ORDER_BY_SATS = 'GROUP BY "Item".id ORDER BY (SUM(CASE WHEN "ItemAct".act = \'VOTE\' AND "Item"."userId" <> "ItemAct"."userId" THEN users.trust ELSE 0 END)) DESC NULLS LAST, "Item".created_at DESC'
|
const TOP_ORDER_BY_SATS = 'GROUP BY "Item".id ORDER BY (SUM(CASE WHEN "ItemAct".act = \'VOTE\' AND "Item"."userId" <> "ItemAct"."userId" THEN users.trust ELSE 0 END)) DESC NULLS LAST, "Item".created_at DESC'
|
||||||
|
|
|
@ -32,11 +32,7 @@ export default {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
models.user.update(
|
return await models.user.update({ where: { id: me.id }, data: { lastSeenAt: new Date() } })
|
||||||
{ where: { id: me.id }, data: { lastSeenAt: new Date() } }
|
|
||||||
).catch(console.log)
|
|
||||||
|
|
||||||
return await models.user.findUnique({ where: { id: me.id } })
|
|
||||||
},
|
},
|
||||||
user: async (parent, { name }, { models }) => {
|
user: async (parent, { name }, { models }) => {
|
||||||
return await models.user.findUnique({ where: { name } })
|
return await models.user.findUnique({ where: { name } })
|
||||||
|
@ -48,7 +44,9 @@ export default {
|
||||||
throw new AuthenticationError('you must be logged in')
|
throw new AuthenticationError('you must be logged in')
|
||||||
}
|
}
|
||||||
|
|
||||||
return me.name?.toUpperCase() === name?.toUpperCase() || !(await models.user.findUnique({ where: { name } }))
|
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
|
||||||
|
return user.name?.toUpperCase() === name?.toUpperCase() || !(await models.user.findUnique({ where: { name } }))
|
||||||
},
|
},
|
||||||
topUsers: async (parent, { cursor, within, userType }, { models, me }) => {
|
topUsers: async (parent, { cursor, within, userType }, { models, me }) => {
|
||||||
const decodedCursor = decodeCursor(cursor)
|
const decodedCursor = decodeCursor(cursor)
|
||||||
|
@ -136,7 +134,7 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
([item] = await serialize(models,
|
([item] = await serialize(models,
|
||||||
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3) AS "Item"`,
|
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3) AS "Item"`,
|
||||||
`@${me.name}'s bio`, bio, Number(me.id))))
|
`@${user.name}'s bio`, bio, Number(me.id))))
|
||||||
}
|
}
|
||||||
|
|
||||||
await createMentions(item, models)
|
await createMentions(item, models)
|
||||||
|
|
|
@ -178,9 +178,11 @@ export default {
|
||||||
throw new UserInputError('amount must be positive', { argumentName: 'amount' })
|
throw new UserInputError('amount must be positive', { argumentName: 'amount' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
|
||||||
// set expires at to 3 hours into future
|
// set expires at to 3 hours into future
|
||||||
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
|
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
|
||||||
const description = `${amount} sats for @${me.name} on stacker.news`
|
const description = `${amount} sats for @${user.name} on stacker.news`
|
||||||
try {
|
try {
|
||||||
const invoice = await createInvoice({
|
const invoice = await createInvoice({
|
||||||
description,
|
description,
|
||||||
|
@ -271,10 +273,12 @@ async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd
|
||||||
|
|
||||||
const msatsFee = Number(maxFee) * 1000
|
const msatsFee = Number(maxFee) * 1000
|
||||||
|
|
||||||
|
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
|
||||||
// create withdrawl transactionally (id, bolt11, amount, fee)
|
// create withdrawl transactionally (id, bolt11, amount, fee)
|
||||||
const [withdrawl] = await serialize(models,
|
const [withdrawl] = await serialize(models,
|
||||||
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
|
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
|
||||||
${Number(decoded.mtokens)}, ${msatsFee}, ${me.name})`)
|
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name})`)
|
||||||
|
|
||||||
payViaPaymentRequest({
|
payViaPaymentRequest({
|
||||||
lnd,
|
lnd,
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Modal } from 'react-bootstrap'
|
||||||
|
|
||||||
|
export default function ModalButton ({ children, clicker }) {
|
||||||
|
const [show, setShow] = useState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
show={show}
|
||||||
|
onHide={() => setShow(false)}
|
||||||
|
>
|
||||||
|
<div className='modal-close' onClick={() => setShow(false)}>X</div>
|
||||||
|
<Modal.Body>
|
||||||
|
{children}
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
<div className='pointer' onClick={() => setShow(true)}>{clicker}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -10,9 +10,11 @@ import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||||
import styles from './user-header.module.css'
|
import styles from './user-header.module.css'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { NAME_MUTATION, NAME_QUERY } from '../fragments/users'
|
import { NAME_MUTATION, NAME_QUERY } from '../fragments/users'
|
||||||
import Image from 'next/image'
|
// import Image from 'next/image'
|
||||||
import QRCode from 'qrcode.react'
|
import QRCode from 'qrcode.react'
|
||||||
import LightningIcon from '../svgs/bolt.svg'
|
import LightningIcon from '../svgs/bolt.svg'
|
||||||
|
import ModalButton from './modal-button'
|
||||||
|
import { encodeLNUrl } from '../lib/lnurl'
|
||||||
|
|
||||||
export default function UserHeader ({ user }) {
|
export default function UserHeader ({ user }) {
|
||||||
const [editting, setEditting] = useState(false)
|
const [editting, setEditting] = useState(false)
|
||||||
|
@ -22,7 +24,7 @@ export default function UserHeader ({ user }) {
|
||||||
const [setName] = useMutation(NAME_MUTATION)
|
const [setName] = useMutation(NAME_MUTATION)
|
||||||
|
|
||||||
const isMe = me?.name === user.name
|
const isMe = me?.name === user.name
|
||||||
const Satistics = () => <div className={`mb-4 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div>
|
const Satistics = () => <div className={`mb-2 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div>
|
||||||
|
|
||||||
const UserSchema = Yup.object({
|
const UserSchema = Yup.object({
|
||||||
name: Yup.string()
|
name: Yup.string()
|
||||||
|
@ -40,14 +42,16 @@ export default function UserHeader ({ user }) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const lnurlp = encodeLNUrl(new URL(`https://stacker.news/.well-known/lnurlp/${user.name}`))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='d-flex align-items-center mt-2 flex-wrap'>
|
<div className='d-flex align-items-center mt-2 flex-wrap'>
|
||||||
<Image
|
{/* <Image
|
||||||
src='/dorian400.jpg' width='200' height='166' layout='fixed'
|
src='/dorian400.jpg' width='135' height='135' layout='fixed'
|
||||||
className={styles.userimg}
|
className={styles.userimg}
|
||||||
/>
|
/> */}
|
||||||
<div className='ml-3'>
|
<div>
|
||||||
{editting
|
{editting
|
||||||
? (
|
? (
|
||||||
<Form
|
<Form
|
||||||
|
@ -85,7 +89,7 @@ export default function UserHeader ({ user }) {
|
||||||
setEditting(false)
|
setEditting(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='d-flex align-items-center mb-1'>
|
<div className='d-flex align-items-center mb-2'>
|
||||||
<Input
|
<Input
|
||||||
prepend=<InputGroup.Text>@</InputGroup.Text>
|
prepend=<InputGroup.Text>@</InputGroup.Text>
|
||||||
name='name'
|
name='name'
|
||||||
|
@ -98,22 +102,30 @@ export default function UserHeader ({ user }) {
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div className='d-flex align-items-center mb-1'>
|
<div className='d-flex align-items-center mb-2'>
|
||||||
<div className={styles.username}>@{user.name}</div>
|
<div className={styles.username}>@{user.name}</div>
|
||||||
{isMe &&
|
{isMe &&
|
||||||
<Button className='py-0' variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
|
<Button className='py-0' variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Satistics user={user} />
|
<Satistics user={user} />
|
||||||
<Button className='font-weight-bold'>
|
<ModalButton
|
||||||
<LightningIcon
|
clicker={
|
||||||
width={20}
|
<Button className='font-weight-bold'>
|
||||||
height={20}
|
<LightningIcon
|
||||||
className='mr-1'
|
width={20}
|
||||||
/>{user.name}@stacker.news
|
height={20}
|
||||||
</Button>
|
className='mr-1'
|
||||||
|
/>{user.name}@stacker.news
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a className='d-flex m-auto p-3' style={{ background: 'white', width: 'fit-content' }} href={`lightning:${lnurlp}`}>
|
||||||
|
<QRCode className='d-flex m-auto' value={lnurlp} renderAs='svg' size={300} />
|
||||||
|
</a>
|
||||||
|
<div className='text-center font-weight-bold text-muted mt-3'>click or scan</div>
|
||||||
|
</ModalButton>
|
||||||
</div>
|
</div>
|
||||||
<QRCode className='ml-auto' value='fdsajfkldsajlkfjdlksajfkldjsalkjfdklsa' renderAs='svg' size={166} />
|
|
||||||
</div>
|
</div>
|
||||||
<Nav
|
<Nav
|
||||||
className={styles.nav}
|
className={styles.nav}
|
||||||
|
|
16
lib/lnurl.js
16
lib/lnurl.js
|
@ -1,19 +1,9 @@
|
||||||
import { randomBytes, createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import { bech32 } from 'bech32'
|
import { bech32 } from 'bech32'
|
||||||
|
|
||||||
export function lnurlAuth (params) {
|
export function encodeLNUrl (url) {
|
||||||
// generate secret (32 random bytes)
|
|
||||||
const secret = Buffer.from(randomBytes(32), 'hex')
|
|
||||||
// create url
|
|
||||||
const url = new URL(process.env.LNAUTH_URL)
|
|
||||||
url.searchParams = new URLSearchParams({
|
|
||||||
...params,
|
|
||||||
k1: secret
|
|
||||||
})
|
|
||||||
// bech32 encode url
|
|
||||||
const words = bech32.toWords(Buffer.from(url.toString(), 'utf8'))
|
const words = bech32.toWords(Buffer.from(url.toString(), 'utf8'))
|
||||||
const encodedUrl = bech32.encode('lnurl', words, 1023)
|
return bech32.encode('lnurl', words, 1023)
|
||||||
return { secret, encodedUrl }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lnurlPayMetadataString (username) {
|
export function lnurlPayMetadataString (username) {
|
||||||
|
|
|
@ -22,13 +22,6 @@ const options = {
|
||||||
token.id = user.id
|
token.id = user.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX We need to update the user name incase they update it ... kind of hacky
|
|
||||||
// better if we use user id everywhere an ignore the username ...
|
|
||||||
if (token?.id) {
|
|
||||||
const { name } = await prisma.user.findUnique({ where: { id: token.id } })
|
|
||||||
token.name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign them up for the newsletter
|
// sign them up for the newsletter
|
||||||
if (isNewUser && profile.email) {
|
if (isNewUser && profile.email) {
|
||||||
fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
|
fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
|
||||||
|
@ -52,7 +45,6 @@ const options = {
|
||||||
async session (session, token) {
|
async session (session, token) {
|
||||||
// we need to add additional session params here
|
// we need to add additional session params here
|
||||||
session.user.id = token.id
|
session.user.id = token.id
|
||||||
session.user.name = token.name
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,7 @@ const apolloServer = new ApolloServer({
|
||||||
models,
|
models,
|
||||||
lnd,
|
lnd,
|
||||||
me: session
|
me: session
|
||||||
? await models.user.findUnique({ where: { id: session.user?.id } })
|
? session.user
|
||||||
: null,
|
: null,
|
||||||
search
|
search
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue