Skip to content

Commit

Permalink
Merge branch 'master' into backend-optimism
Browse files Browse the repository at this point in the history
  • Loading branch information
huumn authored May 28, 2024
2 parents e6c5149 + 94cce91 commit 6b3a9af
Show file tree
Hide file tree
Showing 45 changed files with 1,096 additions and 938 deletions.
2 changes: 1 addition & 1 deletion api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@ export default {
WHERE act IN ('TIP', 'FEE')
AND "itemId" = ${Number(id)}::INTEGER
AND "userId" = ${me.id}::INTEGER)::INTEGER)`,
{ models }
{ models, lnd, hash, hmac }
)
} else {
await serialize(
Expand Down
5 changes: 5 additions & 0 deletions awards.csv
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,8 @@ tsmith123,pr,#1179,#790,good-first-issue,high,,,40k,stickymarch60@walletofsatosh
SatsAllDay,pr,#1159,#510,medium-hard,,1,,450k,[email protected],2024-05-22
Darth-Coin,issue,#1159,#510,medium-hard,,1,,45k,[email protected],2024-05-22
OneOneSeven117,issue,#1187,#1164,easy,,,,10k,[email protected],2024-05-23
tsmith123,pr,#1191,#134,medium,,,required small fix,225k,[email protected],2024-05-28
benalleng,helpfulness,#1191,#134,medium,,,did most of this before,100k,[email protected],2024-05-28
cointastical,issue,#1191,#134,medium,,,,22k,[email protected],2024-05-28
kravhen,pr,#1198,#1180,good-first-issue,,,required linting,18k,???,???
OneOneSeven117,issue,#1198,#1180,good-first-issue,,,required linting,2k,[email protected],2024-05-28
3 changes: 2 additions & 1 deletion components/bounty-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export function BountyForm ({
...SubSelectInitial({ sub: item?.subName || sub?.name })
}}
schema={schema}
invoiceable={{ requireSession: true }}
requireSession
prepaid
onSubmit={
handleSubmit ||
onSubmit
Expand Down
187 changes: 187 additions & 0 deletions components/client-notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { useApolloClient } from '@apollo/client'
import { useMe } from './me'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { datePivot, timeSince } from '@/lib/time'
import { ANON_USER_ID, JIT_INVOICE_TIMEOUT_MS } from '@/lib/constants'
import { HAS_NOTIFICATIONS } from '@/fragments/notifications'
import Item from './item'
import { RootProvider } from './root'
import Comment from './comment'

const toType = t => ({ ERROR: `${t}_ERROR`, PENDING: `${t}_PENDING` })

export const Types = {
Zap: toType('ZAP'),
Reply: toType('REPLY'),
Bounty: toType('BOUNTY'),
PollVote: toType('POLL_VOTE')
}

const ClientNotificationContext = createContext()

export function ClientNotificationProvider ({ children }) {
const [notifications, setNotifications] = useState([])
const client = useApolloClient()
const me = useMe()
// anons don't have access to /notifications
// but we'll store client notifications anyway for simplicity's sake
const storageKey = `client-notifications:${me?.id || ANON_USER_ID}`

useEffect(() => {
const loaded = loadNotifications(storageKey, client)
setNotifications(loaded)
}, [storageKey])

const notify = useCallback((type, props) => {
const id = crypto.randomUUID()
const sortTime = new Date()
const expiresAt = +datePivot(sortTime, { milliseconds: JIT_INVOICE_TIMEOUT_MS })
const isError = type.endsWith('ERROR')
const n = { __typename: type, id, sortTime: +sortTime, pending: !isError, expiresAt, ...props }

setNotifications(notifications => [n, ...notifications])
saveNotification(storageKey, n)

if (isError) {
client?.writeQuery({
query: HAS_NOTIFICATIONS,
data: {
hasNewNotes: true
}
})
}

return id
}, [storageKey, client])

const unnotify = useCallback((id) => {
setNotifications(notifications => notifications.filter(n => n.id !== id))
removeNotification(storageKey, id)
}, [storageKey])

const value = useMemo(() => ({ notifications, notify, unnotify }), [notifications, notify, unnotify])
return (
<ClientNotificationContext.Provider value={value}>
{children}
</ClientNotificationContext.Provider>
)
}

export function ClientNotifyProvider ({ children, additionalProps }) {
const ctx = useClientNotifications()

const notify = useCallback((type, props) => {
return ctx.notify(type, { ...props, ...additionalProps })
}, [ctx.notify])

const value = useMemo(() => ({ ...ctx, notify }), [ctx, notify])
return (
<ClientNotificationContext.Provider value={value}>
{children}
</ClientNotificationContext.Provider>
)
}

export function useClientNotifications () {
return useContext(ClientNotificationContext)
}

function ClientNotification ({ n, message }) {
if (n.pending) {
const expired = n.expiresAt < +new Date()
if (!expired) return null
n.reason = 'invoice expired'
}

// remove payment hashes due to x-overflow
n.reason = n.reason.replace(/(: )?[a-f0-9]{64}/, '')

return (
<div className='ms-2'>
<small className='fw-bold text-danger'>
{n.reason ? `${message}: ${n.reason}` : message}
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
</small>
{!n.item
? null
: n.item.title
? <Item item={n.item} />
: (
<div className='pb-2'>
<RootProvider root={n.item.root}>
<Comment item={n.item} noReply includeParent noComments clickToContext />
</RootProvider>
</div>
)}
</div>
)
}

export function ClientZap ({ n }) {
const message = `failed to zap ${n.sats || n.amount} sats`
return <ClientNotification n={n} message={message} />
}

export function ClientReply ({ n }) {
const message = 'failed to submit reply'
return <ClientNotification n={n} message={message} />
}

export function ClientBounty ({ n }) {
const message = 'failed to pay bounty'
return <ClientNotification n={n} message={message} />
}

export function ClientPollVote ({ n }) {
const message = 'failed to submit poll vote'
return <ClientNotification n={n} message={message} />
}

function loadNotifications (storageKey, client) {
const stored = window.localStorage.getItem(storageKey)
if (!stored) return []

const filtered = JSON.parse(stored).filter(({ sortTime }) => {
// only keep notifications younger than 24 hours
return new Date(sortTime) >= datePivot(new Date(), { hours: -24 })
})

let hasNewNotes = false
const mapped = filtered.map((n) => {
if (!n.pending) return n
// anything that is still pending when we load the page was interrupted
// so we immediately mark it as failed instead of waiting until it expired
const type = n.__typename.replace('PENDING', 'ERROR')
const reason = 'payment was interrupted'
hasNewNotes = true
return { ...n, __typename: type, pending: false, reason }
})

if (hasNewNotes) {
client?.writeQuery({
query: HAS_NOTIFICATIONS,
data: {
hasNewNotes: true
}
})
}

window.localStorage.setItem(storageKey, JSON.stringify(mapped))
return filtered
}

function saveNotification (storageKey, n) {
const stored = window.localStorage.getItem(storageKey)
if (stored) {
window.localStorage.setItem(storageKey, JSON.stringify([...JSON.parse(stored), n]))
} else {
window.localStorage.setItem(storageKey, JSON.stringify([n]))
}
}

function removeNotification (storageKey, id) {
const stored = window.localStorage.getItem(storageKey)
if (stored) {
window.localStorage.setItem(storageKey, JSON.stringify(JSON.parse(stored).filter(n => n.id !== id)))
}
}
2 changes: 1 addition & 1 deletion components/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export default function Comment ({
{item.outlawed && !me?.privates?.wildWestMode
? <Skull className={styles.dontLike} width={24} height={24} />
: item.meDontLikeSats > item.meSats
? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} />
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
: pin ? <Pin width={22} height={22} className={styles.pin} /> : <UpVote item={item} className={styles.upvote} />}
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
<div className='d-flex align-items-center'>
Expand Down
2 changes: 1 addition & 1 deletion components/discussion-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function DiscussionForm ({
...SubSelectInitial({ sub: item?.subName || sub?.name })
}}
schema={schema}
invoiceable
prepaid
onSubmit={handleSubmit || onSubmit}
storageKeyPrefix={storageKeyPrefix}
>
Expand Down
22 changes: 8 additions & 14 deletions components/dont-link-this.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
import Dropdown from 'react-bootstrap/Dropdown'
import { useShowModal } from './modal'
import { useToast } from './toast'
import ItemAct, { zapUndosThresholdReached } from './item-act'
import ItemAct from './item-act'
import AccordianItem from './accordian-item'
import Flag from '@/svgs/flag-fill.svg'
import { useMemo } from 'react'
import getColor from '@/lib/rainbow'
import { gql, useMutation } from '@apollo/client'
import { useMe } from './me'

export function DownZap ({ id, meDontLikeSats, ...props }) {
export function DownZap ({ item, ...props }) {
const { meDontLikeSats } = item
const style = useMemo(() => (meDontLikeSats
? {
fill: getColor(meDontLikeSats),
filter: `drop-shadow(0 0 6px ${getColor(meDontLikeSats)}90)`
}
: undefined), [meDontLikeSats])
return (
<DownZapper id={id} As={({ ...oprops }) => <Flag {...props} {...oprops} style={style} />} />
<DownZapper item={item} As={({ ...oprops }) => <Flag {...props} {...oprops} style={style} />} />
)
}

function DownZapper ({ id, As, children }) {
function DownZapper ({ item, As, children }) {
const toaster = useToast()
const showModal = useShowModal()
const me = useMe()

return (
<As
onClick={async () => {
try {
showModal(onClose =>
<ItemAct
onClose={(amount) => {
onClose()
// undo prompt was toasted before closing modal if zap undos are enabled
// so an additional success toast would be confusing
if (!zapUndosThresholdReached(me, amount)) toaster.success('item downzapped')
}} itemId={id} down
onClose={onClose} item={item} down
>
<AccordianItem
header='what is a downzap?' body={
Expand All @@ -59,11 +53,11 @@ function DownZapper ({ id, As, children }) {
)
}

export default function DontLikeThisDropdownItem ({ id }) {
export default function DontLikeThisDropdownItem ({ item }) {
return (
<DownZapper
As={Dropdown.Item}
id={id}
item={item}
>
<span className='text-danger'>downzap</span>
</DownZapper>
Expand Down
15 changes: 9 additions & 6 deletions components/fee-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function postCommentUseRemoteLineItems ({ parentId } = {}) {
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
const [lineItems, setLineItems] = useState({})
const [disabled, setDisabled] = useState(false)
const me = useMe()

const remoteLineItems = useRemoteLineItems()

Expand All @@ -76,14 +77,18 @@ export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = ()

const value = useMemo(() => {
const lines = { ...baseLineItems, ...lineItems, ...remoteLineItems }
const total = Object.values(lines).reduce((acc, { modifier }) => modifier(acc), 0)
// freebies: there's only a base cost and we don't have enough sats
const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total
return {
lines,
merge: mergeLineItems,
total: Object.values(lines).reduce((acc, { modifier }) => modifier(acc), 0),
total,
disabled,
setDisabled
setDisabled,
free
}
}, [baseLineItems, lineItems, remoteLineItems, mergeLineItems, disabled, setDisabled])
}, [me?.privates?.sats, baseLineItems, lineItems, remoteLineItems, mergeLineItems, disabled, setDisabled])

return (
<FeeButtonContext.Provider value={value}>
Expand Down Expand Up @@ -111,9 +116,7 @@ function FreebieDialog () {

export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
const me = useMe()
const { lines, total, disabled: ctxDisabled } = useFeeButton()
// freebies: there's only a base cost and we don't have enough sats
const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total
const { lines, total, disabled: ctxDisabled, free } = useFeeButton()
const feeText = free
? 'free'
: total > 1
Expand Down
Loading

0 comments on commit 6b3a9af

Please sign in to comment.