stacker.news/components/link-form.js
ekzyis ac45fdc234
Use HODL invoices (#432)
* Use HODL invoices

* Fix expiry check comparing string with Date

* Fix unconfirmed user balance for HODL invoices

This is done by syncing the data from LND to the Invoice table.

If the columns is_held and msatsReceived are set, the frontend is told that we're ready to execute the action.

We then update the user balance in the same tx as the action.

We need to still keep checking the invoice for expiration though.

* Fix worker acting upon deleted invoices

* Prevent usage of invoice after expiration

* Use onComplete from <Countdown> to show expired status

* Remove unused lnd argument

* Fix item destructuring from query

* Fix balance added to every stacker

* Fix hmac required

* Fix invoices not used when logged in

* refactor: move invoiceable code into form

* renamed invoiceHash, invoiceHmac to hash, hmac since it's less verbose all over the place
* form now supports `invoiceable` in its props
* form then wraps `onSubmit` with `useInvoiceable` and passes optional invoice options

* Show expired if expired and canceled

* Also use useCallback for zapping

* Always expire modal invoices after 3m

* little styling thing

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-30 21:48:49 -05:00

247 lines
7.6 KiB
JavaScript

import { useState, useEffect, useCallback } from 'react'
import { Form, Input, SubmitButton } from '../components/form'
import { useRouter } from 'next/router'
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
import Countdown from './countdown'
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
import { ITEM_FIELDS } from '../fragments/items'
import Item from './item'
import AccordianItem from './accordian-item'
import FeeButton, { EditFeeButton } from './fee-button'
import Delete from './delete'
import Button from 'react-bootstrap/Button'
import { linkSchema } from '../lib/validate'
import Moon from '../svgs/moon-fill.svg'
import { SubSelectInitial } from './sub-select-form'
import CancelButton from './cancel-button'
import { normalizeForwards } from '../lib/form'
export function LinkForm ({ item, sub, editThreshold, children }) {
const router = useRouter()
const client = useApolloClient()
const schema = linkSchema(client)
// if Web Share Target API was used
const shareUrl = router.query.url
const shareTitle = router.query.title
const [getPageTitleAndUnshorted, { data }] = useLazyQuery(gql`
query PageTitleAndUnshorted($url: String!) {
pageTitleAndUnshorted(url: $url) {
title
unshorted
}
}`)
const [getDupes, { data: dupesData, loading: dupesLoading }] = useLazyQuery(gql`
${ITEM_FIELDS}
query Dupes($url: String!) {
dupes(url: $url) {
...ItemFields
}
}`, {
onCompleted: () => setPostDisabled(false)
})
const [getRelated, { data: relatedData }] = useLazyQuery(gql`
${ITEM_FIELDS}
query related($title: String!) {
related(title: $title, minMatch: "75%", limit: 3) {
items {
...ItemFields
}
}
}`)
const related = []
for (const item of relatedData?.related?.items || []) {
let found = false
for (const ditem of dupesData?.dupes || []) {
if (ditem.id === item.id) {
found = true
break
}
}
if (!found) {
related.push(item)
}
}
const [upsertLink] = useMutation(
gql`
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: [ItemForwardInput], $hash: String, $hmac: String) {
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, hash: $hash, hmac: $hmac) {
id
}
}`
)
const onSubmit = useCallback(
async ({ boost, title, ...values }) => {
const { error } = await upsertLink({
variables: {
sub: item?.subName || sub?.name,
id: item?.id,
boost: boost ? Number(boost) : undefined,
title: title.trim(),
...values,
forward: normalizeForwards(values.forward)
}
})
if (error) {
throw new Error({ message: error.toString() })
}
if (item) {
await router.push(`/items/${item.id}`)
} else {
const prefix = sub?.name ? `/~${sub.name}` : ''
await router.push(prefix + '/recent')
}
}, [upsertLink, router]
)
useEffect(() => {
if (data?.pageTitleAndUnshorted?.title) {
setTitleOverride(data.pageTitleAndUnshorted.title)
}
}, [data?.pageTitleAndUnshorted?.title])
useEffect(() => {
if (data?.pageTitleAndUnshorted?.unshorted) {
getDupes({
variables: { url: data?.pageTitleAndUnshorted?.unshorted }
})
}
}, [data?.pageTitleAndUnshorted?.unshorted])
const [postDisabled, setPostDisabled] = useState(false)
const [titleOverride, setTitleOverride] = useState()
return (
<Form
initial={{
title: item?.title || shareTitle || '',
url: item?.url || shareUrl || '',
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
...SubSelectInitial({ sub: item?.subName || sub?.name })
}}
schema={schema}
invoiceable
onSubmit={onSubmit}
storageKeyPrefix={item ? undefined : 'link'}
>
{children}
<Input
label='title'
name='title'
overrideValue={titleOverride}
required
clear
onChange={async (formik, e) => {
if (e.target.value) {
getRelated({
variables: { title: e.target.value }
})
}
if (e.target.value === e.target.value.toUpperCase()) {
setTitleOverride(e.target.value.replace(/\w\S*/g, txt =>
txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()))
}
}}
/>
<Input
label='url'
name='url'
required
autoFocus
clear
autoComplete='off'
overrideValue={data?.pageTitleAndUnshorted?.unshorted}
hint={editThreshold
? <div className='text-muted fw-bold'><Countdown date={editThreshold} /></div>
: null}
onChange={async (formik, e) => {
if ((/^ *$/).test(formik?.values.title)) {
getPageTitleAndUnshorted({
variables: { url: e.target.value }
})
} else {
client.cache.modify({
fields: {
pageTitleAndUnshorted () {
return null
}
}
})
}
if (e.target.value) {
setPostDisabled(true)
setTimeout(() => setPostDisabled(false), 3000)
getDupes({
variables: { url: e.target.value }
})
}
}}
/>
<AdvPostForm edit={!!item} />
<div className='mt-3'>
{item
? (
<div className='d-flex justify-content-between'>
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
<Button variant='grey-medium'>delete</Button>
</Delete>
<div className='d-flex'>
<CancelButton />
<EditFeeButton
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
</div>
</div>)
: (
<div className='d-flex align-items-center'>
<FeeButton
baseFee={1} parentId={null} text='post' disabled={postDisabled}
ChildButton={SubmitButton} variant='secondary'
/>
{dupesLoading &&
<div className='d-flex ms-3 justify-content-center'>
<Moon className='spin fill-grey' />
<div className='ms-2 text-muted' style={{ fontWeight: '600' }}>searching for dupes</div>
</div>}
</div>
)}
</div>
{!item &&
<>
{dupesData?.dupes?.length > 0 &&
<div className='mt-3'>
<AccordianItem
show
headerColor='#c03221'
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>dupes</div>}
body={
<div>
{dupesData.dupes.map((item, i) => (
<Item item={item} key={item.id} />
))}
</div>
}
/>
</div>}
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
<AccordianItem
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>similar</div>}
body={
<div>
{related.map((item, i) => (
<Item item={item} key={item.id} />
))}
</div>
}
/>
</div>
</>}
</Form>
)
}