Optimistic updates via pending sats in item context (#1229)
* Use context for pending sats * Fix sats going negative on zap undo We already handle undoing pending sats by wrapping the payment+mutation with try/finally. * Remove unnecessary ItemContextProvider * Rename to parentCtx * Fix hierarchy of ItemContextProvider If a comment was root and it was zapped, the pending sats contributed to the sats shown in <CommentsHeader>. This was caused by <CommentsHeader> accessing the root item context for all comments, even for the root comment. So even if the root comment was zapped, the pending sats contributed to the sats for the comment section. This wasn't the case for posts since their item context was above the context used by <CommentsHeader>. This was fixed by moving <ItemProviderContext> down into <Comments> and <Item> instead of declaring it at <ItemFull> which wraps the root item and all comments. * Optimistic update for poll votes * prevent twice optimistic zap * enhance client notifications with skeleton and no redudant queries * enlarge nwc amount limits * Disable max amount and daily limit in NWC container --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
parent
569d0448c2
commit
93713b33df
@ -4,7 +4,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState }
|
|||||||
import { datePivot, timeSince } from '@/lib/time'
|
import { datePivot, timeSince } from '@/lib/time'
|
||||||
import { USER_ID, JIT_INVOICE_TIMEOUT_MS } from '@/lib/constants'
|
import { USER_ID, JIT_INVOICE_TIMEOUT_MS } from '@/lib/constants'
|
||||||
import { HAS_NOTIFICATIONS } from '@/fragments/notifications'
|
import { HAS_NOTIFICATIONS } from '@/fragments/notifications'
|
||||||
import Item from './item'
|
import Item, { ItemSkeleton } from './item'
|
||||||
import { RootProvider } from './root'
|
import { RootProvider } from './root'
|
||||||
import Comment from './comment'
|
import Comment from './comment'
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ function ClientNotification ({ n, message }) {
|
|||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</small>
|
</small>
|
||||||
{!n.item
|
{!n.item
|
||||||
? null
|
? <ItemSkeleton />
|
||||||
: n.item.title
|
: n.item.title
|
||||||
? <Item item={n.item} />
|
? <Item item={n.item} />
|
||||||
: (
|
: (
|
||||||
|
@ -25,6 +25,7 @@ import Skull from '@/svgs/death-skull.svg'
|
|||||||
import { commentSubTreeRootId } from '@/lib/item'
|
import { commentSubTreeRootId } from '@/lib/item'
|
||||||
import Pin from '@/svgs/pushpin-fill.svg'
|
import Pin from '@/svgs/pushpin-fill.svg'
|
||||||
import LinkToContext from './link-to-context'
|
import LinkToContext from './link-to-context'
|
||||||
|
import { ItemContextProvider } from './item'
|
||||||
|
|
||||||
function Parent ({ item, rootText }) {
|
function Parent ({ item, rootText }) {
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
@ -136,114 +137,116 @@ export default function Comment ({
|
|||||||
const bountyPaid = root.bountyPaidTo?.includes(Number(item.id))
|
const bountyPaid = root.bountyPaidTo?.includes(Number(item.id))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ItemContextProvider>
|
||||||
ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse === 'yep' ? styles.collapsed : ''}`}
|
<div
|
||||||
onMouseEnter={() => ref.current.classList.add('outline-new-comment-unset')}
|
ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse === 'yep' ? styles.collapsed : ''}`}
|
||||||
onTouchStart={() => ref.current.classList.add('outline-new-comment-unset')}
|
onMouseEnter={() => ref.current.classList.add('outline-new-comment-unset')}
|
||||||
>
|
onTouchStart={() => ref.current.classList.add('outline-new-comment-unset')}
|
||||||
<div className={`${itemStyles.item} ${styles.item}`}>
|
>
|
||||||
{item.outlawed && !me?.privates?.wildWestMode
|
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||||
? <Skull className={styles.dontLike} width={24} height={24} />
|
{item.outlawed && !me?.privates?.wildWestMode
|
||||||
: item.meDontLikeSats > item.meSats
|
? <Skull className={styles.dontLike} width={24} height={24} />
|
||||||
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
|
: item.meDontLikeSats > item.meSats
|
||||||
: pin ? <Pin width={22} height={22} className={styles.pin} /> : <UpVote item={item} className={styles.upvote} />}
|
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
|
||||||
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
: pin ? <Pin width={22} height={22} className={styles.pin} /> : <UpVote item={item} className={styles.upvote} />}
|
||||||
<div className='d-flex align-items-center'>
|
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
||||||
{item.user?.meMute && !includeParent && collapse === 'yep'
|
<div className='d-flex align-items-center'>
|
||||||
? (
|
{item.user?.meMute && !includeParent && collapse === 'yep'
|
||||||
<span
|
? (
|
||||||
className={`${itemStyles.other} ${styles.other} pointer`} onClick={() => {
|
<span
|
||||||
setCollapse('nope')
|
className={`${itemStyles.other} ${styles.other} pointer`} onClick={() => {
|
||||||
window.localStorage.setItem(`commentCollapse:${item.id}`, 'nope')
|
setCollapse('nope')
|
||||||
}}
|
window.localStorage.setItem(`commentCollapse:${item.id}`, 'nope')
|
||||||
>reply from someone you muted
|
}}
|
||||||
</span>)
|
>reply from someone you muted
|
||||||
: <ItemInfo
|
</span>)
|
||||||
item={item}
|
: <ItemInfo
|
||||||
commentsText='replies'
|
item={item}
|
||||||
commentTextSingular='reply'
|
commentsText='replies'
|
||||||
className={`${itemStyles.other} ${styles.other}`}
|
commentTextSingular='reply'
|
||||||
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
|
className={`${itemStyles.other} ${styles.other}`}
|
||||||
onQuoteReply={quoteReply}
|
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
|
||||||
nested={!includeParent}
|
onQuoteReply={quoteReply}
|
||||||
extraInfo={
|
nested={!includeParent}
|
||||||
<>
|
extraInfo={
|
||||||
{includeParent && <Parent item={item} rootText={rootText} />}
|
<>
|
||||||
{bountyPaid &&
|
{includeParent && <Parent item={item} rootText={rootText} />}
|
||||||
<ActionTooltip notForm overlayText={`${numWithUnits(root.bounty)} paid`}>
|
{bountyPaid &&
|
||||||
<BountyIcon className={`${styles.bountyIcon} ${'fill-success vertical-align-middle'}`} height={16} width={16} />
|
<ActionTooltip notForm overlayText={`${numWithUnits(root.bounty)} paid`}>
|
||||||
</ActionTooltip>}
|
<BountyIcon className={`${styles.bountyIcon} ${'fill-success vertical-align-middle'}`} height={16} width={16} />
|
||||||
</>
|
</ActionTooltip>}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
onEdit={e => { setEdit(!edit) }}
|
onEdit={e => { setEdit(!edit) }}
|
||||||
editText={edit ? 'cancel' : 'edit'}
|
editText={edit ? 'cancel' : 'edit'}
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
{!includeParent && (collapse === 'yep'
|
{!includeParent && (collapse === 'yep'
|
||||||
? <Eye
|
? <Eye
|
||||||
className={styles.collapser} height={10} width={10} onClick={() => {
|
className={styles.collapser} height={10} width={10} onClick={() => {
|
||||||
setCollapse('nope')
|
setCollapse('nope')
|
||||||
window.localStorage.setItem(`commentCollapse:${item.id}`, 'nope')
|
window.localStorage.setItem(`commentCollapse:${item.id}`, 'nope')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
: <EyeClose
|
||||||
|
className={styles.collapser} height={10} width={10} onClick={() => {
|
||||||
|
setCollapse('yep')
|
||||||
|
window.localStorage.setItem(`commentCollapse:${item.id}`, 'yep')
|
||||||
|
}}
|
||||||
|
/>)}
|
||||||
|
{topLevel && (
|
||||||
|
<span className='d-flex ms-auto align-items-center'>
|
||||||
|
<Share title={item?.title} path={`/items/${item?.id}`} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{edit
|
||||||
|
? (
|
||||||
|
<CommentEdit
|
||||||
|
comment={item}
|
||||||
|
onSuccess={() => {
|
||||||
|
setEdit(!edit)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
: <EyeClose
|
)
|
||||||
className={styles.collapser} height={10} width={10} onClick={() => {
|
: (
|
||||||
setCollapse('yep')
|
<div className={styles.text} ref={textRef}>
|
||||||
window.localStorage.setItem(`commentCollapse:${item.id}`, 'yep')
|
{item.searchText
|
||||||
}}
|
? <SearchText text={item.searchText} />
|
||||||
/>)}
|
: (
|
||||||
{topLevel && (
|
<Text itemId={item.id} topLevel={topLevel} rel={item.rel ?? UNKNOWN_LINK_REL} outlawed={item.outlawed} imgproxyUrls={item.imgproxyUrls}>
|
||||||
<span className='d-flex ms-auto align-items-center'>
|
{item.outlawed && !me?.privates?.wildWestMode
|
||||||
<Share title={item?.title} path={`/items/${item?.id}`} />
|
? '*stackers have outlawed this. turn on wild west mode in your [settings](/settings) to see outlawed content.*'
|
||||||
</span>
|
: truncate ? truncateString(item.text) : item.text}
|
||||||
)}
|
</Text>)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{edit
|
|
||||||
? (
|
|
||||||
<CommentEdit
|
|
||||||
comment={item}
|
|
||||||
onSuccess={() => {
|
|
||||||
setEdit(!edit)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div className={styles.text} ref={textRef}>
|
|
||||||
{item.searchText
|
|
||||||
? <SearchText text={item.searchText} />
|
|
||||||
: (
|
|
||||||
<Text itemId={item.id} topLevel={topLevel} rel={item.rel ?? UNKNOWN_LINK_REL} outlawed={item.outlawed} imgproxyUrls={item.imgproxyUrls}>
|
|
||||||
{item.outlawed && !me?.privates?.wildWestMode
|
|
||||||
? '*stackers have outlawed this. turn on wild west mode in your [settings](/settings) to see outlawed content.*'
|
|
||||||
: truncate ? truncateString(item.text) : item.text}
|
|
||||||
</Text>)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{collapse !== 'yep' && (
|
||||||
{collapse !== 'yep' && (
|
bottomedOut
|
||||||
bottomedOut
|
? <div className={styles.children}><ReplyOnAnotherPage item={item} /></div>
|
||||||
? <div className={styles.children}><ReplyOnAnotherPage item={item} /></div>
|
: (
|
||||||
: (
|
<div className={styles.children}>
|
||||||
<div className={styles.children}>
|
{item.outlawed && !me?.privates?.wildWestMode
|
||||||
{item.outlawed && !me?.privates?.wildWestMode
|
? <div className='py-2' />
|
||||||
? <div className='py-2' />
|
: !noReply &&
|
||||||
: !noReply &&
|
<Reply depth={depth + 1} item={item} replyOpen={replyOpen} onCancelQuote={cancelQuote} onQuoteReply={quoteReply} quote={quote}>
|
||||||
<Reply depth={depth + 1} item={item} replyOpen={replyOpen} onCancelQuote={cancelQuote} onQuoteReply={quoteReply} quote={quote}>
|
{root.bounty && !bountyPaid && <PayBounty item={item} />}
|
||||||
{root.bounty && !bountyPaid && <PayBounty item={item} />}
|
</Reply>}
|
||||||
</Reply>}
|
{children}
|
||||||
{children}
|
<div className={styles.comments}>
|
||||||
<div className={styles.comments}>
|
{item.comments && !noComments
|
||||||
{item.comments && !noComments
|
? item.comments.map((item) => (
|
||||||
? item.comments.map((item) => (
|
<Comment depth={depth + 1} key={item.id} item={item} />
|
||||||
<Comment depth={depth + 1} key={item.id} item={item} />
|
))
|
||||||
))
|
: null}
|
||||||
: null}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</ItemContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,10 +6,12 @@ import Navbar from 'react-bootstrap/Navbar'
|
|||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
import { defaultCommentSort } from '@/lib/item'
|
import { defaultCommentSort } from '@/lib/item'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { ItemContextProvider, useItemContext } from './item'
|
||||||
|
|
||||||
export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, commentSats }) {
|
export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, commentSats }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const sort = router.query.sort || defaultCommentSort(pinned, bio, parentCreatedAt)
|
const sort = router.query.sort || defaultCommentSort(pinned, bio, parentCreatedAt)
|
||||||
|
const { pendingCommentSats } = useItemContext()
|
||||||
|
|
||||||
const getHandleClick = sort => {
|
const getHandleClick = sort => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -24,7 +26,7 @@ export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, comm
|
|||||||
activeKey={sort}
|
activeKey={sort}
|
||||||
>
|
>
|
||||||
<Nav.Item className='text-muted'>
|
<Nav.Item className='text-muted'>
|
||||||
{numWithUnits(commentSats)}
|
{numWithUnits(commentSats + pendingCommentSats)}
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<div className='ms-auto d-flex'>
|
<div className='ms-auto d-flex'>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
@ -66,7 +68,7 @@ export default function Comments ({ parentId, pinned, bio, parentCreatedAt, comm
|
|||||||
const pins = comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position)
|
const pins = comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ItemContextProvider>
|
||||||
{comments?.length > 0
|
{comments?.length > 0
|
||||||
? <CommentsHeader
|
? <CommentsHeader
|
||||||
commentSats={commentSats} parentCreatedAt={parentCreatedAt}
|
commentSats={commentSats} parentCreatedAt={parentCreatedAt}
|
||||||
@ -91,7 +93,7 @@ export default function Comments ({ parentId, pinned, bio, parentCreatedAt, comm
|
|||||||
{comments.filter(({ position }) => !position).map(item => (
|
{comments.filter(({ position }) => !position).map(item => (
|
||||||
<Comment depth={1} key={item.id} item={item} {...props} />
|
<Comment depth={1} key={item.id} item={item} {...props} />
|
||||||
))}
|
))}
|
||||||
</>
|
</ItemContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ import EyeClose from '@/svgs/eye-close-line.svg'
|
|||||||
import Info from './info'
|
import Info from './info'
|
||||||
import { InvoiceCanceledError, usePayment } from './payment'
|
import { InvoiceCanceledError, usePayment } from './payment'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { optimisticUpdate } from '@/lib/apollo'
|
|
||||||
import { useClientNotifications } from './client-notifications'
|
import { useClientNotifications } from './client-notifications'
|
||||||
import { ActCanceledError } from './item-act'
|
import { ActCanceledError } from './item-act'
|
||||||
|
|
||||||
@ -804,7 +803,7 @@ const StorageKeyPrefixContext = createContext()
|
|||||||
export function Form ({
|
export function Form ({
|
||||||
initial, schema, onSubmit, children, initialError, validateImmediately,
|
initial, schema, onSubmit, children, initialError, validateImmediately,
|
||||||
storageKeyPrefix, validateOnChange = true, prepaid, requireSession, innerRef,
|
storageKeyPrefix, validateOnChange = true, prepaid, requireSession, innerRef,
|
||||||
optimisticUpdate: optimisticUpdateArgs, clientNotification, signal, ...props
|
optimisticUpdate, clientNotification, signal, ...props
|
||||||
}) {
|
}) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const initialErrorToasted = useRef(false)
|
const initialErrorToasted = useRef(false)
|
||||||
@ -844,9 +843,7 @@ export function Form ({
|
|||||||
throw new SessionRequiredError()
|
throw new SessionRequiredError()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optimisticUpdateArgs) {
|
revert = optimisticUpdate?.(variables)
|
||||||
revert = optimisticUpdate(optimisticUpdateArgs(variables))
|
|
||||||
}
|
|
||||||
|
|
||||||
await signal?.pause({ me, amount })
|
await signal?.pause({ me, amount })
|
||||||
|
|
||||||
@ -865,8 +862,6 @@ export function Form ({
|
|||||||
clearLocalStorage(values)
|
clearLocalStorage(values)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
revert?.()
|
|
||||||
|
|
||||||
if (err instanceof InvoiceCanceledError || err instanceof ActCanceledError) {
|
if (err instanceof InvoiceCanceledError || err instanceof ActCanceledError) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -880,6 +875,7 @@ export function Form ({
|
|||||||
|
|
||||||
cancel?.()
|
cancel?.()
|
||||||
} finally {
|
} finally {
|
||||||
|
revert?.()
|
||||||
// if we reach this line, the submit either failed or was successful so we can remove the pending notification.
|
// if we reach this line, the submit either failed or was successful so we can remove the pending notification.
|
||||||
// if we don't reach this line, the page was probably reloaded and we can use the pending notification
|
// if we don't reach this line, the page was probably reloaded and we can use the pending notification
|
||||||
// stored in localStorage to handle this case.
|
// stored in localStorage to handle this case.
|
||||||
|
@ -10,9 +10,9 @@ import { useToast } from './toast'
|
|||||||
import { useLightning } from './lightning'
|
import { useLightning } from './lightning'
|
||||||
import { nextTip } from './upvote'
|
import { nextTip } from './upvote'
|
||||||
import { InvoiceCanceledError, usePayment } from './payment'
|
import { InvoiceCanceledError, usePayment } from './payment'
|
||||||
// import { optimisticUpdate } from '@/lib/apollo'
|
|
||||||
import { Types as ClientNotification, ClientNotifyProvider, useClientNotifications } from './client-notifications'
|
import { Types as ClientNotification, ClientNotifyProvider, useClientNotifications } from './client-notifications'
|
||||||
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
|
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
|
||||||
|
import { useItemContext } from './item'
|
||||||
|
|
||||||
const defaultTips = [100, 1000, 10_000, 100_000]
|
const defaultTips = [100, 1000, 10_000, 100_000]
|
||||||
|
|
||||||
@ -100,11 +100,10 @@ export const actUpdate = ({ me, onUpdate }) => (cache, args) => {
|
|||||||
onUpdate?.(cache, args)
|
onUpdate?.(cache, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ItemAct ({ onClose, item, down, children, abortSignal }) {
|
export default function ItemAct ({ onClose, item, down, children, abortSignal, optimisticUpdate }) {
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const [oValue, setOValue] = useState()
|
const [oValue, setOValue] = useState()
|
||||||
const strike = useLightning()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
@ -113,8 +112,6 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
|
|||||||
const act = useAct()
|
const act = useAct()
|
||||||
|
|
||||||
const onSubmit = useCallback(async ({ amount, hash, hmac }) => {
|
const onSubmit = useCallback(async ({ amount, hash, hmac }) => {
|
||||||
strike()
|
|
||||||
onClose()
|
|
||||||
await act({
|
await act({
|
||||||
variables: {
|
variables: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@ -123,28 +120,11 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
|
|||||||
hash,
|
hash,
|
||||||
hmac
|
hmac
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
|
||||||
act: { id: item.id, sats: Number(amount), act: down ? 'DONT_LIKE_THIS' : 'TIP', path: item.path }
|
|
||||||
},
|
|
||||||
update: actUpdate({ me })
|
update: actUpdate({ me })
|
||||||
})
|
})
|
||||||
if (!me) setItemMeAnonSats({ id: item.id, amount })
|
if (!me) setItemMeAnonSats({ id: item.id, amount })
|
||||||
addCustomTip(Number(amount))
|
addCustomTip(Number(amount))
|
||||||
}, [me, act, down, item.id, strike])
|
}, [me, act, down, item.id])
|
||||||
|
|
||||||
// XXX avoid manual optimistic updates until
|
|
||||||
// https://github.com/stackernews/stacker.news/issues/1218 is fixed
|
|
||||||
// const optimisticUpdate = useCallback(({ amount }) => {
|
|
||||||
// const variables = {
|
|
||||||
// id: item.id,
|
|
||||||
// sats: Number(amount),
|
|
||||||
// act: down ? 'DONT_LIKE_THIS' : 'TIP'
|
|
||||||
// }
|
|
||||||
// const optimisticResponse = { act: { ...variables, path: item.path } }
|
|
||||||
// strike()
|
|
||||||
// onClose()
|
|
||||||
// return { mutation: ACT_MUTATION, variables, optimisticResponse, update: actUpdate({ me }) }
|
|
||||||
// }, [item.id, down, !!me, strike])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClientNotifyProvider additionalProps={{ itemId: item.id }}>
|
<ClientNotifyProvider additionalProps={{ itemId: item.id }}>
|
||||||
@ -155,7 +135,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
|
|||||||
}}
|
}}
|
||||||
schema={amountSchema}
|
schema={amountSchema}
|
||||||
prepaid
|
prepaid
|
||||||
// optimisticUpdate={optimisticUpdate}
|
optimisticUpdate={({ amount }) => optimisticUpdate(amount, { onClose })}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
clientNotification={ClientNotification.Zap}
|
clientNotification={ClientNotification.Zap}
|
||||||
signal={abortSignal}
|
signal={abortSignal}
|
||||||
@ -260,9 +240,10 @@ export function useZap () {
|
|||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const strike = useLightning()
|
const strike = useLightning()
|
||||||
const payment = usePayment()
|
const payment = usePayment()
|
||||||
|
const { pendingSats } = useItemContext()
|
||||||
|
|
||||||
return useCallback(async ({ item, mem, abortSignal }) => {
|
return useCallback(async ({ item, abortSignal, optimisticUpdate }) => {
|
||||||
const meSats = (item?.meSats || 0)
|
const meSats = (item?.meSats || 0) + pendingSats
|
||||||
|
|
||||||
// add current sats to next tip since idempotent zaps use desired total zap not difference
|
// add current sats to next tip since idempotent zaps use desired total zap not difference
|
||||||
const sats = meSats + nextTip(meSats, { ...me?.privates })
|
const sats = meSats + nextTip(meSats, { ...me?.privates })
|
||||||
@ -270,14 +251,11 @@ export function useZap () {
|
|||||||
|
|
||||||
const variables = { id: item.id, sats, act: 'TIP' }
|
const variables = { id: item.id, sats, act: 'TIP' }
|
||||||
const notifyProps = { itemId: item.id, sats: satsDelta }
|
const notifyProps = { itemId: item.id, sats: satsDelta }
|
||||||
const optimisticResponse = { act: { path: item.path, ...variables } }
|
// const optimisticResponse = { act: { path: item.path, ...variables } }
|
||||||
|
|
||||||
let revert, cancel, nid
|
let revert, cancel, nid
|
||||||
try {
|
try {
|
||||||
// XXX avoid manual optimistic updates until
|
revert = optimisticUpdate?.(satsDelta)
|
||||||
// https://github.com/stackernews/stacker.news/issues/1218 is fixed
|
|
||||||
// revert = optimisticUpdate({ mutation: ZAP_MUTATION, variables, optimisticResponse, update })
|
|
||||||
// strike()
|
|
||||||
|
|
||||||
await abortSignal.pause({ me, amount: satsDelta })
|
await abortSignal.pause({ me, amount: satsDelta })
|
||||||
|
|
||||||
@ -288,13 +266,15 @@ export function useZap () {
|
|||||||
let hash, hmac;
|
let hash, hmac;
|
||||||
[{ hash, hmac }, cancel] = await payment.request(satsDelta)
|
[{ hash, hmac }, cancel] = await payment.request(satsDelta)
|
||||||
|
|
||||||
// XXX related to comment above
|
await zap({
|
||||||
// await zap({ variables: { ...variables, hash, hmac } })
|
variables: { ...variables, hash, hmac },
|
||||||
strike()
|
update: (...args) => {
|
||||||
await zap({ variables: { ...variables, hash, hmac }, optimisticResponse, update })
|
revert?.()
|
||||||
|
update(...args)
|
||||||
|
}
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
revert?.()
|
revert?.()
|
||||||
|
|
||||||
if (error instanceof InvoiceCanceledError || error instanceof ActCanceledError) {
|
if (error instanceof InvoiceCanceledError || error instanceof ActCanceledError) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -310,7 +290,7 @@ export function useZap () {
|
|||||||
} finally {
|
} finally {
|
||||||
if (nid) unnotify(nid)
|
if (nid) unnotify(nid)
|
||||||
}
|
}
|
||||||
}, [me?.id, strike, payment, notify, unnotify])
|
}, [me?.id, strike, payment, notify, unnotify, pendingSats])
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ActCanceledError extends Error {
|
export class ActCanceledError extends Error {
|
||||||
|
@ -22,6 +22,7 @@ import { DropdownItemUpVote } from './upvote'
|
|||||||
import { useRoot } from './root'
|
import { useRoot } from './root'
|
||||||
import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header'
|
import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header'
|
||||||
import UserPopover from './user-popover'
|
import UserPopover from './user-popover'
|
||||||
|
import { useItemContext } from './item'
|
||||||
|
|
||||||
export default function ItemInfo ({
|
export default function ItemInfo ({
|
||||||
item, full, commentsText = 'comments',
|
item, full, commentsText = 'comments',
|
||||||
@ -36,6 +37,7 @@ export default function ItemInfo ({
|
|||||||
const [hasNewComments, setHasNewComments] = useState(false)
|
const [hasNewComments, setHasNewComments] = useState(false)
|
||||||
const [meTotalSats, setMeTotalSats] = useState(0)
|
const [meTotalSats, setMeTotalSats] = useState(0)
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
|
const { pendingSats, pendingCommentSats } = useItemContext()
|
||||||
const sub = item?.sub || root?.sub
|
const sub = item?.sub || root?.sub
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -45,8 +47,8 @@ export default function ItemInfo ({
|
|||||||
}, [item])
|
}, [item])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0))
|
if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0) + (pendingSats))
|
||||||
}, [item?.meSats, item?.meAnonSats])
|
}, [item?.meSats, item?.meAnonSats, pendingSats])
|
||||||
|
|
||||||
// territory founders can pin any post in their territory
|
// territory founders can pin any post in their territory
|
||||||
// and OPs can pin any root reply in their post
|
// and OPs can pin any root reply in their post
|
||||||
@ -70,7 +72,7 @@ export default function ItemInfo ({
|
|||||||
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
||||||
: ''} from me)`} `}
|
: ''} from me)`} `}
|
||||||
>
|
>
|
||||||
{numWithUnits(item.sats)}
|
{numWithUnits(item.sats + pendingSats)}
|
||||||
</span>
|
</span>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
</>}
|
</>}
|
||||||
@ -88,7 +90,7 @@ export default function ItemInfo ({
|
|||||||
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
|
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
|
||||||
`/items/${item.id}`)
|
`/items/${item.id}`)
|
||||||
}
|
}
|
||||||
}} title={numWithUnits(item.commentSats)} className='text-reset position-relative'
|
}} title={numWithUnits(item.commentSats + pendingCommentSats)} className='text-reset position-relative'
|
||||||
>
|
>
|
||||||
{numWithUnits(item.ncomments, {
|
{numWithUnits(item.ncomments, {
|
||||||
abbreviate: false,
|
abbreviate: false,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import styles from './item.module.css'
|
import styles from './item.module.css'
|
||||||
import UpVote from './upvote'
|
import UpVote from './upvote'
|
||||||
import { useRef } from 'react'
|
import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'
|
||||||
import { USER_ID, UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { USER_ID, UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
import Pin from '@/svgs/pushpin-fill.svg'
|
import Pin from '@/svgs/pushpin-fill.svg'
|
||||||
import reactStringReplace from 'react-string-replace'
|
import reactStringReplace from 'react-string-replace'
|
||||||
@ -45,6 +45,47 @@ export function SearchTitle ({ title }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ItemContext = createContext({
|
||||||
|
pendingSats: 0,
|
||||||
|
setPendingSats: undefined,
|
||||||
|
pendingVote: undefined,
|
||||||
|
setPendingVote: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ItemContextProvider = ({ children }) => {
|
||||||
|
const parentCtx = useItemContext()
|
||||||
|
const [pendingSats, innerSetPendingSats] = useState(0)
|
||||||
|
const [pendingCommentSats, innerSetPendingCommentSats] = useState(0)
|
||||||
|
const [pendingVote, setPendingVote] = useState()
|
||||||
|
|
||||||
|
// cascade comment sats up to root context
|
||||||
|
const setPendingSats = useCallback((sats) => {
|
||||||
|
innerSetPendingSats(sats)
|
||||||
|
parentCtx?.setPendingCommentSats?.(sats)
|
||||||
|
}, [parentCtx?.setPendingCommentSats])
|
||||||
|
|
||||||
|
const setPendingCommentSats = useCallback((sats) => {
|
||||||
|
innerSetPendingCommentSats(sats)
|
||||||
|
parentCtx?.setPendingCommentSats?.(sats)
|
||||||
|
}, [parentCtx?.setPendingCommentSats])
|
||||||
|
|
||||||
|
const value = useMemo(() =>
|
||||||
|
({
|
||||||
|
pendingSats,
|
||||||
|
setPendingSats,
|
||||||
|
pendingCommentSats,
|
||||||
|
setPendingCommentSats,
|
||||||
|
pendingVote,
|
||||||
|
setPendingVote
|
||||||
|
}),
|
||||||
|
[pendingSats, setPendingSats, pendingCommentSats, setPendingCommentSats, pendingVote, setPendingVote])
|
||||||
|
return <ItemContext.Provider value={value}>{children}</ItemContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useItemContext = () => {
|
||||||
|
return useContext(ItemContext)
|
||||||
|
}
|
||||||
|
|
||||||
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, onQuoteReply, pinnable }) {
|
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, onQuoteReply, pinnable }) {
|
||||||
const titleRef = useRef()
|
const titleRef = useRef()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -52,7 +93,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
|||||||
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ItemContextProvider>
|
||||||
{rank
|
{rank
|
||||||
? (
|
? (
|
||||||
<div className={styles.rank}>
|
<div className={styles.rank}>
|
||||||
@ -110,7 +151,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</ItemContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,13 +36,8 @@ import { ITEM_FULL } from '@/fragments/items'
|
|||||||
function Notification ({ n, fresh }) {
|
function Notification ({ n, fresh }) {
|
||||||
const type = n.__typename
|
const type = n.__typename
|
||||||
|
|
||||||
// we need to resolve item id to item to show item for client notifications
|
|
||||||
const { data } = useQuery(ITEM_FULL, { variables: { id: n.itemId }, skip: !n.itemId })
|
|
||||||
const item = data?.item
|
|
||||||
const itemN = { item, ...n }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationLayout nid={nid(n)} {...defaultOnClick(itemN)} fresh={fresh}>
|
<NotificationLayout nid={nid(n)} {...defaultOnClick(n)} fresh={fresh}>
|
||||||
{
|
{
|
||||||
(type === 'Earn' && <EarnNotification n={n} />) ||
|
(type === 'Earn' && <EarnNotification n={n} />) ||
|
||||||
(type === 'Revenue' && <RevenueNotification n={n} />) ||
|
(type === 'Revenue' && <RevenueNotification n={n} />) ||
|
||||||
@ -62,15 +57,26 @@ function Notification ({ n, fresh }) {
|
|||||||
(type === 'TerritoryPost' && <TerritoryPost n={n} />) ||
|
(type === 'TerritoryPost' && <TerritoryPost n={n} />) ||
|
||||||
(type === 'TerritoryTransfer' && <TerritoryTransfer n={n} />) ||
|
(type === 'TerritoryTransfer' && <TerritoryTransfer n={n} />) ||
|
||||||
(type === 'Reminder' && <Reminder n={n} />) ||
|
(type === 'Reminder' && <Reminder n={n} />) ||
|
||||||
([ClientTypes.Zap.ERROR, ClientTypes.Zap.PENDING].includes(type) && <ClientZap n={itemN} />) ||
|
<ClientNotification n={n} />
|
||||||
([ClientTypes.Reply.ERROR, ClientTypes.Reply.PENDING].includes(type) && <ClientReply n={itemN} />) ||
|
|
||||||
([ClientTypes.Bounty.ERROR, ClientTypes.Bounty.PENDING].includes(type) && <ClientBounty n={itemN} />) ||
|
|
||||||
([ClientTypes.PollVote.ERROR, ClientTypes.PollVote.PENDING].includes(type) && <ClientPollVote n={itemN} />)
|
|
||||||
}
|
}
|
||||||
</NotificationLayout>
|
</NotificationLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ClientNotification ({ n }) {
|
||||||
|
// we need to resolve item id to item to show item for client notifications
|
||||||
|
const { data } = useQuery(ITEM_FULL, { variables: { id: n.itemId }, skip: !n.itemId })
|
||||||
|
const item = data?.item
|
||||||
|
const itemN = { item, ...n }
|
||||||
|
|
||||||
|
return (
|
||||||
|
([ClientTypes.Zap.ERROR, ClientTypes.Zap.PENDING].includes(n.__typename) && <ClientZap n={itemN} />) ||
|
||||||
|
([ClientTypes.Reply.ERROR, ClientTypes.Reply.PENDING].includes(n.__typename) && <ClientReply n={itemN} />) ||
|
||||||
|
([ClientTypes.Bounty.ERROR, ClientTypes.Bounty.PENDING].includes(n.__typename) && <ClientBounty n={itemN} />) ||
|
||||||
|
([ClientTypes.PollVote.ERROR, ClientTypes.PollVote.PENDING].includes(n.__typename) && <ClientPollVote n={itemN} />)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function NotificationLayout ({ children, nid, href, as, fresh }) {
|
function NotificationLayout ({ children, nid, href, as, fresh }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if (!href) return <div className={fresh ? styles.fresh : ''}>{children}</div>
|
if (!href) return <div className={fresh ? styles.fresh : ''}>{children}</div>
|
||||||
|
@ -10,6 +10,7 @@ import { POLL_COST } from '@/lib/constants'
|
|||||||
import { InvoiceCanceledError, usePayment } from './payment'
|
import { InvoiceCanceledError, usePayment } from './payment'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { Types as ClientNotification, useClientNotifications } from './client-notifications'
|
import { Types as ClientNotification, useClientNotifications } from './client-notifications'
|
||||||
|
import { useItemContext } from './item'
|
||||||
|
|
||||||
export default function Poll ({ item }) {
|
export default function Poll ({ item }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
@ -20,6 +21,7 @@ export default function Poll ({ item }) {
|
|||||||
const [pollVote] = useMutation(POLL_VOTE_MUTATION)
|
const [pollVote] = useMutation(POLL_VOTE_MUTATION)
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const { notify, unnotify } = useClientNotifications()
|
const { notify, unnotify } = useClientNotifications()
|
||||||
|
const { pendingVote, setPendingVote } = useItemContext()
|
||||||
|
|
||||||
const update = (cache, { data: { pollVote } }) => {
|
const update = (cache, { data: { pollVote } }) => {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
@ -56,6 +58,8 @@ export default function Poll ({ item }) {
|
|||||||
const optimisticResponse = { pollVote: v.id }
|
const optimisticResponse = { pollVote: v.id }
|
||||||
let cancel, nid
|
let cancel, nid
|
||||||
try {
|
try {
|
||||||
|
setPendingVote(v.id)
|
||||||
|
|
||||||
if (me) {
|
if (me) {
|
||||||
nid = notify(ClientNotification.PollVote.PENDING, notifyProps)
|
nid = notify(ClientNotification.PollVote.PENDING, notifyProps)
|
||||||
}
|
}
|
||||||
@ -78,6 +82,7 @@ export default function Poll ({ item }) {
|
|||||||
|
|
||||||
cancel?.()
|
cancel?.()
|
||||||
} finally {
|
} finally {
|
||||||
|
setPendingVote(undefined)
|
||||||
if (nid) unnotify(nid)
|
if (nid) unnotify(nid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,7 +97,8 @@ export default function Poll ({ item }) {
|
|||||||
const hasExpiration = !!item.pollExpiresAt
|
const hasExpiration = !!item.pollExpiresAt
|
||||||
const timeRemaining = timeLeft(new Date(item.pollExpiresAt))
|
const timeRemaining = timeLeft(new Date(item.pollExpiresAt))
|
||||||
const mine = item.user.id === me?.id
|
const mine = item.user.id === me?.id
|
||||||
const showPollButton = (!hasExpiration || timeRemaining) && !item.poll.meVoted && !mine
|
const showPollButton = (!hasExpiration || timeRemaining) && !item.poll.meVoted && !mine && !pendingVote
|
||||||
|
const pollCount = item.poll.count + (pendingVote ? 1 : 0)
|
||||||
return (
|
return (
|
||||||
<div className={styles.pollBox}>
|
<div className={styles.pollBox}>
|
||||||
{item.poll.options.map(v =>
|
{item.poll.options.map(v =>
|
||||||
@ -100,10 +106,12 @@ export default function Poll ({ item }) {
|
|||||||
? <PollButton key={v.id} v={v} />
|
? <PollButton key={v.id} v={v} />
|
||||||
: <PollResult
|
: <PollResult
|
||||||
key={v.id} v={v}
|
key={v.id} v={v}
|
||||||
progress={item.poll.count ? fixedDecimal(v.count * 100 / item.poll.count, 1) : 0}
|
progress={pollCount
|
||||||
|
? fixedDecimal((v.count + (pendingVote === v.id ? 1 : 0)) * 100 / pollCount, 1)
|
||||||
|
: 0}
|
||||||
/>)}
|
/>)}
|
||||||
<div className='text-muted mt-1'>
|
<div className='text-muted mt-1'>
|
||||||
{numWithUnits(item.poll.count, { unitSingular: 'vote', unitPlural: 'votes' })}
|
{numWithUnits(pollCount, { unitSingular: 'vote', unitPlural: 'votes' })}
|
||||||
{hasExpiration && ` \\ ${timeRemaining ? `${timeRemaining} left` : 'poll ended'}`}
|
{hasExpiration && ` \\ ${timeRemaining ? `${timeRemaining} left` : 'poll ended'}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,8 @@ import Popover from 'react-bootstrap/Popover'
|
|||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
import { useLightning } from './lightning'
|
||||||
|
import { useItemContext } from './item'
|
||||||
|
|
||||||
const UpvotePopover = ({ target, show, handleClose }) => {
|
const UpvotePopover = ({ target, show, handleClose }) => {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
@ -96,8 +98,9 @@ export default function UpVote ({ item, className }) {
|
|||||||
setWalkthrough(upvotePopover: $upvotePopover, tipPopover: $tipPopover)
|
setWalkthrough(upvotePopover: $upvotePopover, tipPopover: $tipPopover)
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
const strike = useLightning()
|
||||||
const [controller, setController] = useState(null)
|
const [controller, setController] = useState()
|
||||||
|
const { pendingSats, setPendingSats } = useItemContext()
|
||||||
const pending = controller?.started && !controller.done
|
const pending = controller?.started && !controller.done
|
||||||
|
|
||||||
const setVoteShow = useCallback((yes) => {
|
const setVoteShow = useCallback((yes) => {
|
||||||
@ -134,7 +137,7 @@ export default function UpVote ({ item, className }) {
|
|||||||
[item?.mine, item?.meForward, item?.deletedAt])
|
[item?.mine, item?.meForward, item?.deletedAt])
|
||||||
|
|
||||||
const [meSats, overlayText, color, nextColor] = useMemo(() => {
|
const [meSats, overlayText, color, nextColor] = useMemo(() => {
|
||||||
const meSats = (item?.meSats || item?.meAnonSats || 0)
|
const meSats = (item?.meSats || item?.meAnonSats || 0) + pendingSats
|
||||||
|
|
||||||
// what should our next tip be?
|
// what should our next tip be?
|
||||||
const sats = nextTip(meSats, { ...me?.privates })
|
const sats = nextTip(meSats, { ...me?.privates })
|
||||||
@ -142,7 +145,16 @@ export default function UpVote ({ item, className }) {
|
|||||||
return [
|
return [
|
||||||
meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it',
|
meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it',
|
||||||
getColor(meSats), getColor(meSats + sats)]
|
getColor(meSats), getColor(meSats + sats)]
|
||||||
}, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault])
|
}, [item?.meSats, item?.meAnonSats, pendingSats, me?.privates?.tipDefault, me?.privates?.turboDefault])
|
||||||
|
|
||||||
|
const optimisticUpdate = useCallback((sats, { onClose } = {}) => {
|
||||||
|
setPendingSats(pendingSats => pendingSats + sats)
|
||||||
|
strike()
|
||||||
|
onClose?.()
|
||||||
|
return () => {
|
||||||
|
setPendingSats(pendingSats => pendingSats - sats)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleModalClosed = () => {
|
const handleModalClosed = () => {
|
||||||
setHover(false)
|
setHover(false)
|
||||||
@ -167,7 +179,9 @@ export default function UpVote ({ item, className }) {
|
|||||||
setController(c)
|
setController(c)
|
||||||
|
|
||||||
showModal(onClose =>
|
showModal(onClose =>
|
||||||
<ItemAct onClose={onClose} item={item} abortSignal={c.signal} />, { onClose: handleModalClosed })
|
<ItemAct
|
||||||
|
onClose={onClose} item={item} abortSignal={c.signal} optimisticUpdate={optimisticUpdate}
|
||||||
|
/>, { onClose: handleModalClosed })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShortPress = async () => {
|
const handleShortPress = async () => {
|
||||||
@ -193,9 +207,9 @@ export default function UpVote ({ item, className }) {
|
|||||||
const c = new ZapUndoController()
|
const c = new ZapUndoController()
|
||||||
setController(c)
|
setController(c)
|
||||||
|
|
||||||
await zap({ item, me, abortSignal: c.signal })
|
await zap({ item, me, abortSignal: c.signal, optimisticUpdate })
|
||||||
} else {
|
} else {
|
||||||
showModal(onClose => <ItemAct onClose={onClose} item={item} />, { onClose: handleModalClosed })
|
showModal(onClose => <ItemAct onClose={onClose} item={item} optimisticUpdate={optimisticUpdate} />, { onClose: handleModalClosed })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,6 +498,10 @@ services:
|
|||||||
- 'stacker_lnd'
|
- 'stacker_lnd'
|
||||||
- '--lnd-port'
|
- '--lnd-port'
|
||||||
- '10009'
|
- '10009'
|
||||||
|
- '--max-amount'
|
||||||
|
- '0'
|
||||||
|
- '--daily-limit'
|
||||||
|
- '0'
|
||||||
lnbits:
|
lnbits:
|
||||||
image: lnbits/lnbits:0.12.5
|
image: lnbits/lnbits:0.12.5
|
||||||
container_name: lnbits
|
container_name: lnbits
|
||||||
|
@ -255,17 +255,3 @@ function getClient (uri) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function optimisticUpdate ({ mutation, variables, optimisticResponse, update }) {
|
|
||||||
const { cache, queryManager } = getApolloClient()
|
|
||||||
|
|
||||||
const mutationId = String(queryManager.mutationIdCounter++)
|
|
||||||
queryManager.markMutationOptimistic(optimisticResponse, {
|
|
||||||
mutationId,
|
|
||||||
document: mutation,
|
|
||||||
variables,
|
|
||||||
update
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => cache.removeOptimistic(mutationId)
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user