Compare commits

...

5 Commits

Author SHA1 Message Date
Keyan
8329da1f56
Update awards.csv 2024-06-13 16:40:16 -05:00
keyan
b1f850ee0e clear email hash when email is unlinked 2024-06-13 12:14:08 -05:00
ekzyis
8a19fc0905
Use <Alert> for auth banner in /settings (#1238) 2024-06-12 18:16:54 -05:00
ekzyis
286f53f2b3
Update zap undos info (#1237) 2024-06-12 18:16:41 -05:00
ekzyis
cbcae1d128
Fix downzaps (#1236)
* Add optimistic update for downzaps

* Add optimistic update for zaps via dropdown

* Also use lightning strike for downzaps
2024-06-12 13:24:04 -05:00
9 changed files with 97 additions and 33 deletions

View File

@ -720,7 +720,7 @@ export default {
} else if (authType === 'nostr') { } else if (authType === 'nostr') {
user = await models.user.update({ where: { id: me.id }, data: { hideNostr: true, nostrAuthPubkey: null } }) user = await models.user.update({ where: { id: me.id }, data: { hideNostr: true, nostrAuthPubkey: null } })
} else if (authType === 'email') { } else if (authType === 'email') {
user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null } }) user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null, emailHash: null } })
} else { } else {
throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } }) throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } })
} }

View File

@ -108,3 +108,5 @@ tsmith123,pr,#1207,#837,easy,high,1,,180k,stickymarch60@walletofsatoshi.com,2024
SatsAllDay,pr,#1214,#1199,good-first-issue,,,,20k,weareallsatoshi@getalby.com,2024-06-03 SatsAllDay,pr,#1214,#1199,good-first-issue,,,,20k,weareallsatoshi@getalby.com,2024-06-03
SatsAllDay,pr,#1197,#1192,medium,,,,250k,weareallsatoshi@getalby.com,2024-06-03 SatsAllDay,pr,#1197,#1192,medium,,,,250k,weareallsatoshi@getalby.com,2024-06-03
tsmith123,pr,#1216,#1213,easy,,1,,90k,stickymarch60@walletofsatoshi.com,2024-06-03 tsmith123,pr,#1216,#1213,easy,,1,,90k,stickymarch60@walletofsatoshi.com,2024-06-03
tsmith123,pr,#1231,#1230,good-first-issue,,,,20k,stickymarch60@walletofsatoshi.com,2024-06-13
felipebueno,issue,#1231,#1230,good-first-issue,,,,2k,felipebueno@getalby.com,2024-06-13

1 name type pr id issue ids difficulty priority changes requested notes amount receive method date paid
108 SatsAllDay pr #1214 #1199 good-first-issue 20k weareallsatoshi@getalby.com 2024-06-03
109 SatsAllDay pr #1197 #1192 medium 250k weareallsatoshi@getalby.com 2024-06-03
110 tsmith123 pr #1216 #1213 easy 1 90k stickymarch60@walletofsatoshi.com 2024-06-03
111 tsmith123 pr #1231 #1230 good-first-issue 20k stickymarch60@walletofsatoshi.com 2024-06-13
112 felipebueno issue #1231 #1230 good-first-issue 2k felipebueno@getalby.com 2024-06-13

View File

@ -138,3 +138,11 @@ export function WalletSecurityBanner () {
</Alert> </Alert>
) )
} }
export function AuthBanner () {
return (
<Alert className={`${styles.banner} mt-0`} key='info' variant='danger'>
Please add a second auth method to avoid losing access to your account.
</Alert>
)
}

View File

@ -25,7 +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' import { ItemContextProvider, useItemContext } from './item'
function Parent ({ item, rootText }) { function Parent ({ item, rootText }) {
const root = useRoot() const root = useRoot()
@ -144,11 +144,7 @@ export default function Comment ({
onTouchStart={() => ref.current.classList.add('outline-new-comment-unset')} onTouchStart={() => ref.current.classList.add('outline-new-comment-unset')}
> >
<div className={`${itemStyles.item} ${styles.item}`}> <div className={`${itemStyles.item} ${styles.item}`}>
{item.outlawed && !me?.privates?.wildWestMode <ZapIcon item={item} pin={pin} me={me} />
? <Skull className={styles.dontLike} width={24} height={24} />
: item.meDontLikeSats > item.meSats
? <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={`${itemStyles.hunk} ${styles.hunk}`}>
<div className='d-flex align-items-center'> <div className='d-flex align-items-center'>
{item.user?.meMute && !includeParent && collapse === 'yep' {item.user?.meMute && !includeParent && collapse === 'yep'
@ -250,6 +246,20 @@ export default function Comment ({
) )
} }
function ZapIcon ({ item, pin }) {
const me = useMe()
const { pendingSats, pendingDownSats } = useItemContext()
const meSats = item.meSats + pendingSats
const downSats = item.meDontLikeSats + pendingDownSats
return item.outlawed && !me?.privates?.wildWestMode
? <Skull className={styles.dontLike} width={24} height={24} />
: downSats > meSats
? <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} />
}
export function CommentSkeleton ({ skeletonChildren }) { export function CommentSkeleton ({ skeletonChildren }) {
return ( return (
<div className={styles.comment}> <div className={styles.comment}>

View File

@ -4,18 +4,24 @@ import { useToast } from './toast'
import ItemAct from './item-act' import ItemAct from './item-act'
import AccordianItem from './accordian-item' import AccordianItem from './accordian-item'
import Flag from '@/svgs/flag-fill.svg' import Flag from '@/svgs/flag-fill.svg'
import { useMemo } from 'react' import { useCallback, useMemo } from 'react'
import getColor from '@/lib/rainbow' import getColor from '@/lib/rainbow'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
import { useItemContext } from './item'
import { useLightning } from './lightning'
export function DownZap ({ item, ...props }) { export function DownZap ({ item, ...props }) {
const { pendingDownSats } = useItemContext()
const { meDontLikeSats } = item const { meDontLikeSats } = item
const style = useMemo(() => (meDontLikeSats
const downSats = meDontLikeSats + pendingDownSats
const style = useMemo(() => (downSats
? { ? {
fill: getColor(meDontLikeSats), fill: getColor(downSats),
filter: `drop-shadow(0 0 6px ${getColor(meDontLikeSats)}90)` filter: `drop-shadow(0 0 6px ${getColor(downSats)}90)`
} }
: undefined), [meDontLikeSats]) : undefined), [downSats])
return ( return (
<DownZapper item={item} As={({ ...oprops }) => <Flag {...props} {...oprops} style={style} />} /> <DownZapper item={item} As={({ ...oprops }) => <Flag {...props} {...oprops} style={style} />} />
) )
@ -24,6 +30,17 @@ export function DownZap ({ item, ...props }) {
function DownZapper ({ item, As, children }) { function DownZapper ({ item, As, children }) {
const toaster = useToast() const toaster = useToast()
const showModal = useShowModal() const showModal = useShowModal()
const strike = useLightning()
const { setPendingDownSats } = useItemContext()
const optimisticUpdate = useCallback((sats, { onClose } = {}) => {
setPendingDownSats(pendingSats => pendingSats + sats)
strike()
onClose?.()
return () => {
setPendingDownSats(pendingSats => pendingSats - sats)
}
}, [])
return ( return (
<As <As
@ -31,7 +48,7 @@ function DownZapper ({ item, As, children }) {
try { try {
showModal(onClose => showModal(onClose =>
<ItemAct <ItemAct
onClose={onClose} item={item} down onClose={onClose} item={item} down optimisticUpdate={optimisticUpdate}
> >
<AccordianItem <AccordianItem
header='what is a downzap?' body={ header='what is a downzap?' body={

View File

@ -37,7 +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 { pendingSats, pendingCommentSats, pendingDownSats } = useItemContext()
const sub = item?.sub || root?.sub const sub = item?.sub || root?.sub
useEffect(() => { useEffect(() => {
@ -58,6 +58,8 @@ export default function ItemInfo ({
const rootReply = item.path.split('.').length === 2 const rootReply = item.path.split('.').length === 2
const canPin = (isPost && mySub) || (myPost && rootReply) const canPin = (isPost && mySub) || (myPost && rootReply)
const downSats = item.meDontLikeSats + pendingDownSats
return ( return (
<div className={className || `${styles.other}`}> <div className={className || `${styles.other}`}>
{!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) && {!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) &&
@ -68,8 +70,8 @@ export default function ItemInfo ({
unitPlural: 'stackers' unitPlural: 'stackers'
})} ${item.mine })} ${item.mine
? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post` ? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
: `(${numWithUnits(meTotalSats, { abbreviate: false })}${item.meDontLikeSats : `(${numWithUnits(meTotalSats, { abbreviate: false })}${downSats
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}` ? ` & ${numWithUnits(downSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
: ''} from me)`} `} : ''} from me)`} `}
> >
{numWithUnits(item.sats + pendingSats)} {numWithUnits(item.sats + pendingSats)}
@ -179,7 +181,7 @@ export default function ItemInfo ({
<CrosspostDropdownItem item={item} />} <CrosspostDropdownItem item={item} />}
{me && !item.position && {me && !item.position &&
!item.mine && !item.deletedAt && !item.mine && !item.deletedAt &&
(item.meDontLikeSats > meTotalSats (downSats > meTotalSats
? <DropdownItemUpVote item={item} /> ? <DropdownItemUpVote item={item} />
: <DontLikeThisDropdownItem item={item} />)} : <DontLikeThisDropdownItem item={item} />)}
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated && {me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&

View File

@ -49,7 +49,9 @@ const ItemContext = createContext({
pendingSats: 0, pendingSats: 0,
setPendingSats: undefined, setPendingSats: undefined,
pendingVote: undefined, pendingVote: undefined,
setPendingVote: undefined setPendingVote: undefined,
pendingDownSats: 0,
setPendingDownSats: undefined
}) })
export const ItemContextProvider = ({ children }) => { export const ItemContextProvider = ({ children }) => {
@ -57,6 +59,7 @@ export const ItemContextProvider = ({ children }) => {
const [pendingSats, innerSetPendingSats] = useState(0) const [pendingSats, innerSetPendingSats] = useState(0)
const [pendingCommentSats, innerSetPendingCommentSats] = useState(0) const [pendingCommentSats, innerSetPendingCommentSats] = useState(0)
const [pendingVote, setPendingVote] = useState() const [pendingVote, setPendingVote] = useState()
const [pendingDownSats, setPendingDownSats] = useState(0)
// cascade comment sats up to root context // cascade comment sats up to root context
const setPendingSats = useCallback((sats) => { const setPendingSats = useCallback((sats) => {
@ -76,9 +79,11 @@ export const ItemContextProvider = ({ children }) => {
pendingCommentSats, pendingCommentSats,
setPendingCommentSats, setPendingCommentSats,
pendingVote, pendingVote,
setPendingVote setPendingVote,
pendingDownSats,
setPendingDownSats
}), }),
[pendingSats, setPendingSats, pendingCommentSats, setPendingCommentSats, pendingVote, setPendingVote]) [pendingSats, setPendingSats, pendingCommentSats, setPendingCommentSats, pendingVote, setPendingVote, pendingDownSats, setPendingDownSats])
return <ItemContext.Provider value={value}>{children}</ItemContext.Provider> return <ItemContext.Provider value={value}>{children}</ItemContext.Provider>
} }
@ -101,13 +106,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
</div>) </div>)
: <div />} : <div />}
<div className={`${styles.item} ${siblingComments ? 'pt-3' : ''}`}> <div className={`${styles.item} ${siblingComments ? 'pt-3' : ''}`}>
{item.position && (pinnable || !item.subName) <ZapIcon item={item} pinnable={pinnable} />
? <Pin width={24} height={24} className={styles.pin} />
: item.meDontLikeSats > item.meSats
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
: Number(item.user?.id) === USER_ID.ad
? <AdIcon width={24} height={24} className={styles.ad} />
: <UpVote item={item} className={styles.upvote} />}
<div className={styles.hunk}> <div className={styles.hunk}>
<div className={`${styles.main} flex-wrap`}> <div className={`${styles.main} flex-wrap`}>
<Link <Link
@ -229,6 +228,21 @@ export function ItemSkeleton ({ rank, children, showUpvote = true }) {
) )
} }
function ZapIcon ({ item, pinnable }) {
const { pendingSats, pendingDownSats } = useItemContext()
const meSats = item.meSats + pendingSats
const downSats = item.meDontLikeSats + pendingDownSats
return item.position && (pinnable || !item.subName)
? <Pin width={24} height={24} className={styles.pin} />
: downSats > meSats
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
: Number(item.user?.id) === USER_ID.ad
? <AdIcon width={24} height={24} className={styles.ad} />
: <UpVote item={item} className={styles.upvote} />
}
function PollIndicator ({ item }) { function PollIndicator ({ item }) {
const hasExpiration = !!item.pollExpiresAt const hasExpiration = !!item.pollExpiresAt
const timeRemaining = timeLeft(new Date(item.pollExpiresAt)) const timeRemaining = timeLeft(new Date(item.pollExpiresAt))

View File

@ -56,12 +56,23 @@ const TipPopover = ({ target, show, handleClose }) => (
export function DropdownItemUpVote ({ item }) { export function DropdownItemUpVote ({ item }) {
const showModal = useShowModal() const showModal = useShowModal()
const { setPendingSats } = useItemContext()
const strike = useLightning()
const optimisticUpdate = useCallback((sats, { onClose } = {}) => {
setPendingSats(pendingSats => pendingSats + sats)
strike()
onClose?.()
return () => {
setPendingSats(pendingSats => pendingSats - sats)
}
}, [])
return ( return (
<Dropdown.Item <Dropdown.Item
onClick={async () => { onClick={async () => {
showModal(onClose => showModal(onClose =>
<ItemAct onClose={onClose} item={item} />) <ItemAct onClose={onClose} item={item} optimisticUpdate={optimisticUpdate} />)
}} }}
> >
<span className='text-success'>zap</span> <span className='text-success'>zap</span>

View File

@ -30,6 +30,7 @@ import { INVOICE_RETENTION_DAYS, ZAP_UNDO_DELAY_MS } from '@/lib/constants'
import { OverlayTrigger, Tooltip } from 'react-bootstrap' import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import { useField } from 'formik' import { useField } from 'formik'
import styles from './settings.module.css' import styles from './settings.module.css'
import { AuthBanner } from '@/components/banners'
export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true }) export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })
@ -106,7 +107,7 @@ export default function Settings ({ ssrData }) {
return ( return (
<Layout> <Layout>
<div className='pb-3 w-100 mt-2' style={{ maxWidth: '600px' }}> <div className='pb-3 w-100 mt-2' style={{ maxWidth: '600px' }}>
{hasOnlyOneAuthMethod(settings?.authMethods) && <div className={styles.alert}>Please add a second auth method to avoid losing access to your account.</div>} {hasOnlyOneAuthMethod(settings?.authMethods) && <AuthBanner />}
<SettingsHeader /> <SettingsHeader />
<Form <Form
initial={{ initial={{
@ -1007,10 +1008,9 @@ const ZapUndosField = () => {
zap undos zap undos
<Info> <Info>
<ul className='fw-bold'> <ul className='fw-bold'>
<li>An undo button is shown after every zap that exceeds or is equal to the threshold</li> <li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li>
<li>The button is shown for {ZAP_UNDO_DELAY_MS / 1000} seconds</li> <li>You can undo the zap if you click the bolt while it's pulsing</li>
<li>The button is only shown for zaps from the custodial wallet</li> <li>The bolt will pulse for {ZAP_UNDO_DELAY_MS / 1000} seconds</li>
<li>Use a budget or manual approval with attached wallets</li>
</ul> </ul>
</Info> </Info>
</div> </div>