Compare commits
2 Commits
35be035850
...
93713b33df
Author | SHA1 | Date | |
---|---|---|---|
|
93713b33df | ||
|
569d0448c2 |
@ -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'
|
||||||
|
|
||||||
@ -121,7 +120,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
const imageUploadRef = useRef(null)
|
const imageUploadRef = useRef(null)
|
||||||
const previousTab = useRef(tab)
|
const previousTab = useRef(tab)
|
||||||
const { merge, setDisabled: setSubmitDisabled } = useFeeButton()
|
const { merge, setDisabled: setSubmitDisabled } = useFeeButton()
|
||||||
const toaster = useToast()
|
|
||||||
const [updateImageFeesInfo] = useLazyQuery(gql`
|
const [updateImageFeesInfo] = useLazyQuery(gql`
|
||||||
query imageFeesInfo($s3Keys: [Int]!) {
|
query imageFeesInfo($s3Keys: [Int]!) {
|
||||||
imageFeesInfo(s3Keys: $s3Keys) {
|
imageFeesInfo(s3Keys: $s3Keys) {
|
||||||
@ -135,7 +134,6 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
nextFetchPolicy: 'no-cache',
|
nextFetchPolicy: 'no-cache',
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
toaster.danger(`unabled to get image fees: ${err.message || err.toString?.()}`)
|
|
||||||
},
|
},
|
||||||
onCompleted: ({ imageFeesInfo }) => {
|
onCompleted: ({ imageFeesInfo }) => {
|
||||||
merge({
|
merge({
|
||||||
@ -805,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)
|
||||||
@ -845,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 })
|
||||||
|
|
||||||
@ -866,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
|
||||||
}
|
}
|
||||||
@ -881,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