Compare commits

..

18 Commits

Author SHA1 Message Date
ekzyis 18fbd17025
CTRL+U for uploads (#1423) 2024-09-23 20:08:37 -05:00
k00b 2b5a1cbfe9 more reply placeholders 2024-09-22 12:05:03 -05:00
k00b b6dcee4f26 add booosts to newsletter script 2024-09-22 12:05:03 -05:00
ekzyis d30dace266
Fix missing dependency for useZap (#1420)
This didn't cause any bugs because useWallet returns everything we need on first render.

This caused a bug with E2EE device sync branch though since there the wallet is loaded async.

This meant that during payment, the wallet config was undefined.
2024-09-22 11:03:38 -05:00
k00b 894d02a196 allow top sorting by boost 2024-09-21 14:58:25 -05:00
k00b 83458fdc9e add amas to newsletter script 2024-09-21 14:44:17 -05:00
k00b 101574b605 fix territory revenue notification 2024-09-20 11:07:15 -05:00
k00b 59d5fd60f2 wider top boost on rewards page 2024-09-20 10:46:07 -05:00
k00b f90a9905ba unzapped bolt shouldn't glow 2024-09-20 10:41:46 -05:00
k00b 8447a4a8b2 boost icon refinement 2024-09-20 10:15:44 -05:00
k00b 4981d572bb show boost for when forwardee 2024-09-20 09:58:53 -05:00
ekzyis 4e7b4ee571
Fix upvote hover style not showing for first zap (#1418) 2024-09-20 09:44:15 -05:00
k00b 8c9d4aa59b fix auction based ranking 2024-09-19 17:07:36 -05:00
k00b ad9a65ce78 fix expire boost unit 2024-09-19 16:10:04 -05:00
ekzyis b4e143460b
Fix nwc error message (#1417)
* Fix this.error undefined on relay error

* Also use arrow function for ws.onmessage
2024-09-19 15:53:42 -05:00
k00b 731df5fc67 better err message ek suggestion 2024-09-19 15:32:18 -05:00
k00b 2f191e04f9 ek boost hint suggestions 2024-09-19 15:27:09 -05:00
k00b 09be42844e boosted items aren't freebies 2024-09-19 15:18:07 -05:00
20 changed files with 118 additions and 55 deletions

View File

@ -190,7 +190,7 @@ export async function retryPaidAction (actionType, args, context) {
const failedInvoice = await models.invoice.findUnique({ where: { id: invoiceId, actionState: 'FAILED' } }) const failedInvoice = await models.invoice.findUnique({ where: { id: invoiceId, actionState: 'FAILED' } })
if (!failedInvoice) { if (!failedInvoice) {
throw new Error(`retryPaidAction - invoice not found or not in failed state ${actionType}`) throw new Error(`retryPaidAction ${actionType} - invoice ${invoiceId} not found or not in failed state`)
} }
const { msatsRequested, actionId } = failedInvoice const { msatsRequested, actionId } = failedInvoice

View File

@ -105,6 +105,8 @@ const orderByClause = (by, me, models, type) => {
return 'ORDER BY "Item".msats DESC' return 'ORDER BY "Item".msats DESC'
case 'zaprank': case 'zaprank':
return topOrderByWeightedSats(me, models) return topOrderByWeightedSats(me, models)
case 'boost':
return 'ORDER BY "Item".boost DESC'
case 'random': case 'random':
return 'ORDER BY RANDOM()' return 'ORDER BY RANDOM()'
default: default:
@ -378,6 +380,7 @@ export default {
activeOrMine(me), activeOrMine(me),
nsfwClause(showNsfw), nsfwClause(showNsfw),
typeClause(type), typeClause(type),
by === 'boost' && '"Item".boost > 0',
whenClause(when || 'forever', table))} whenClause(when || 'forever', table))}
${orderByClause(by, me, models, type)} ${orderByClause(by, me, models, type)}
OFFSET $4 OFFSET $4
@ -421,6 +424,7 @@ export default {
typeClause(type), typeClause(type),
whenClause(when, 'Item'), whenClause(when, 'Item'),
await filterClause(me, models, type), await filterClause(me, models, type),
by === 'boost' && '"Item".boost > 0',
muteClause(me))} muteClause(me))}
${orderByClause(by || 'zaprank', me, models, type)} ${orderByClause(by || 'zaprank', me, models, type)}
OFFSET $3 OFFSET $3
@ -479,7 +483,7 @@ export default {
'"pinId" IS NULL', '"pinId" IS NULL',
subClause(sub, 4) subClause(sub, 4)
)} )}
ORDER BY group_rank, rank ORDER BY group_rank DESC, rank
OFFSET $2 OFFSET $2
LIMIT $3`, LIMIT $3`,
orderBy: 'ORDER BY group_rank DESC, rank' orderBy: 'ORDER BY group_rank DESC, rank'
@ -690,7 +694,8 @@ export default {
boost: { gte: boost }, boost: { gte: boost },
status: 'ACTIVE', status: 'ACTIVE',
deletedAt: null, deletedAt: null,
outlawed: false outlawed: false,
parentId: null
} }
if (id) { if (id) {
where.id = { not: Number(id) } where.id = { not: Number(id) }
@ -698,7 +703,7 @@ export default {
return { return {
home: await models.item.count({ where }) === 0, home: await models.item.count({ where }) === 0,
sub: await models.item.count({ where: { ...where, subName: sub } }) === 0 sub: sub ? await models.item.count({ where: { ...where, subName: sub } }) === 0 : false
} }
} }
}, },
@ -1139,7 +1144,7 @@ export default {
return item.weightedVotes - item.weightedDownVotes > 0 return item.weightedVotes - item.weightedDownVotes > 0
}, },
freebie: async (item) => { freebie: async (item) => {
return item.cost === 0 return item.cost === 0 && item.boost === 0
}, },
meSats: async (item, args, { me, models }) => { meSats: async (item, args, { me, models }) => {
if (!me) return 0 if (!me) return 0

View File

@ -90,8 +90,8 @@ export function BoostItemInput ({ item, sub, act = false, ...props }) {
const [boost, setBoost] = useState(Number(item?.boost) + (act ? BOOST_MULT : 0)) const [boost, setBoost] = useState(Number(item?.boost) + (act ? BOOST_MULT : 0))
const [getBoostPosition, { data }] = useLazyQuery(gql` const [getBoostPosition, { data }] = useLazyQuery(gql`
query BoostPosition($id: ID, $boost: Int) { query BoostPosition($sub: String, $id: ID, $boost: Int) {
boostPosition(sub: "${item?.subName || sub?.name}", id: $id, boost: $boost) { boostPosition(sub: $sub, id: $id, boost: $boost) {
home home
sub sub
} }
@ -101,10 +101,10 @@ export function BoostItemInput ({ item, sub, act = false, ...props }) {
const getPositionDebounce = useDebounceCallback((...args) => getBoostPosition(...args), 1000, [getBoostPosition]) const getPositionDebounce = useDebounceCallback((...args) => getBoostPosition(...args), 1000, [getBoostPosition])
useEffect(() => { useEffect(() => {
if (boost) { if (boost >= 0 && !item?.parentId) {
getPositionDebounce({ variables: { boost: Number(boost), id: item?.id } }) getPositionDebounce({ variables: { sub: item?.subName || sub?.name, boost: Number(boost), id: item?.id } })
} }
}, [boost, item?.id]) }, [boost, item?.id, !item?.parentId, item?.subName || sub?.name])
const boostMessage = useMemo(() => { const boostMessage = useMemo(() => {
if (!item?.parentId) { if (!item?.parentId) {

View File

@ -2,9 +2,9 @@ import { useShowModal } from './modal'
import { useToast } from './toast' 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 { useMemo } from 'react' import { useMemo, useState } from 'react'
import getColor from '@/lib/rainbow' import getColor from '@/lib/rainbow'
import UpBolt from '@/svgs/bolt.svg' import BoostIcon from '@/svgs/arrow-up-double-line.svg'
import styles from './upvote.module.css' import styles from './upvote.module.css'
import { BoostHelp } from './adv-post-form' import { BoostHelp } from './adv-post-form'
import { BOOST_MULT } from '@/lib/constants' import { BOOST_MULT } from '@/lib/constants'
@ -12,15 +12,17 @@ import classNames from 'classnames'
export default function Boost ({ item, className, ...props }) { export default function Boost ({ item, className, ...props }) {
const { boost } = item const { boost } = item
const style = useMemo(() => (boost const [hover, setHover] = useState(false)
const [color, nextColor] = useMemo(() => [getColor(boost), getColor(boost + BOOST_MULT)], [boost])
const style = useMemo(() => (hover || boost
? { ? {
fill: getColor(boost), fill: hover ? nextColor : color,
filter: `drop-shadow(0 0 6px ${getColor(boost)}90)`, filter: `drop-shadow(0 0 6px ${hover ? nextColor : color}90)`
transform: 'scaleX(-1)'
} }
: { : undefined), [boost, hover])
transform: 'scaleX(-1)'
}), [boost])
return ( return (
<Booster <Booster
item={item} As={({ ...oprops }) => item={item} As={({ ...oprops }) =>
@ -28,11 +30,14 @@ export default function Boost ({ item, className, ...props }) {
<div <div
className={styles.upvoteWrapper} className={styles.upvoteWrapper}
> >
<UpBolt <BoostIcon
{...props} {...oprops} style={style} {...props} {...oprops} style={style}
width={26} width={26}
height={26} height={26}
className={classNames(styles.upvote, className, boost && styles.voted)} onPointerEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onTouchEnd={() => setHover(false)}
className={classNames(styles.boost, className, boost && styles.voted)}
/> />
</div> </div>
</div>} </div>}

View File

@ -255,6 +255,11 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
e.preventDefault() e.preventDefault()
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange) insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
} }
if (e.key === 'u') {
// some browsers might use CTRL+U to do something else so prevent that behavior too
e.preventDefault()
imageUploadRef.current?.click()
}
if (e.key === 'Tab' && e.altKey) { if (e.key === 'Tab' && e.altKey) {
e.preventDefault() e.preventDefault()
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange) insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)

View File

@ -277,7 +277,7 @@ export function useZap () {
const reason = error?.message || error?.toString?.() const reason = error?.message || error?.toString?.()
toaster.danger(reason) toaster.danger(reason)
} }
}, [me?.id, strike]) }, [act, me?.id, strike])
} }
export class ActCanceledError extends Error { export class ActCanceledError extends Error {

View File

@ -113,7 +113,6 @@ function TopLevelItem ({ item, noReply, ...props }) {
<Reply <Reply
item={item} item={item}
replyOpen replyOpen
placeholder={item.ncomments > 3 ? 'fractions of a penny for your thoughts?' : 'early comments get more zaps'}
onCancelQuote={cancelQuote} onCancelQuote={cancelQuote}
onQuoteReply={quoteReply} onQuoteReply={quoteReply}
quote={quote} quote={quote}

View File

@ -106,7 +106,7 @@ export default function Item ({
<div className={classNames(styles.item, itemClassName)}> <div className={classNames(styles.item, itemClassName)}>
{item.position && (pinnable || !item.subName) {item.position && (pinnable || !item.subName)
? <Pin width={24} height={24} className={styles.pin} /> ? <Pin width={24} height={24} className={styles.pin} />
: item.mine : item.mine || item.meForward
? <Boost item={item} className={styles.upvote} /> ? <Boost item={item} className={styles.upvote} />
: item.meDontLikeSats > item.meSats : item.meDontLikeSats > item.meSats
? <DownZap width={24} height={24} className={styles.dontLike} item={item} /> ? <DownZap width={24} height={24} className={styles.dontLike} item={item} />

View File

@ -242,12 +242,12 @@ function RevenueNotification ({ n }) {
return ( return (
<div className='d-flex'> <div className='d-flex'>
<BountyIcon className='align-self-center fill-success mx-1' width={24} height={24} style={{ flex: '0 0 24px' }} /> <BountyIcon className='align-self-center fill-success mx-1' width={24} height={24} style={{ flex: '0 0 24px' }} />
<div className=' pb-1'> <div className='ms-2'>
<div className='fw-bold text-success'> <NoteHeader color='success' big>
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in territory revenue<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small> you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in territory revenue<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
</div> </NoteHeader>
<div style={{ lineHeight: '140%' }}> <div style={{ lineHeight: '140%' }}>
As the founder of territory <Link href={`/~${n.subName}`}>~{n.subName}</Link>, you receive 50% of the revenue it generates and the other 50% go to <Link href='/rewards'>rewards</Link>. As the founder of territory <Link href={`/~${n.subName}`}>~{n.subName}</Link>, you receive 70% of the post, comment, boost, and zap fees. The other 30% go to <Link href='/rewards'>rewards</Link>.
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@ import { Form, MarkdownInput } from '@/components/form'
import styles from './reply.module.css' import styles from './reply.module.css'
import { COMMENTS } from '@/fragments/comments' import { COMMENTS } from '@/fragments/comments'
import { useMe } from './me' import { useMe } from './me'
import { forwardRef, useCallback, useEffect, useState, useRef } from 'react' import { forwardRef, useCallback, useEffect, useState, useRef, useMemo } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button' import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
import { commentsViewedAfterComment } from '@/lib/new-comments' import { commentsViewedAfterComment } from '@/lib/new-comments'
@ -34,7 +34,6 @@ export default forwardRef(function Reply ({
item, item,
replyOpen, replyOpen,
children, children,
placeholder,
onQuoteReply, onQuoteReply,
onCancelQuote, onCancelQuote,
quote quote
@ -53,6 +52,14 @@ export default forwardRef(function Reply ({
} }
}, [replyOpen, quote, parentId]) }, [replyOpen, quote, parentId])
const placeholder = useMemo(() => {
return [
'comment for currency?',
'fractions of a penny for your thoughts?',
'put your money where your mouth is?'
][parentId % 3]
}, [parentId])
const onSubmit = useItemSubmit(CREATE_COMMENT, { const onSubmit = useItemSubmit(CREATE_COMMENT, {
extraValues: { parentId }, extraValues: { parentId },
paidMutationOptions: { paidMutationOptions: {

View File

@ -227,14 +227,15 @@ export default function UpVote ({ item, className }) {
} }
} }
const fillColor = meSats && (hover || pending ? nextColor : color) const style = useMemo(() => {
const fillColor = pending || hover ? nextColor : color
const style = useMemo(() => (fillColor return meSats || hover || pending
? { ? {
fill: fillColor, fill: fillColor,
filter: `drop-shadow(0 0 6px ${fillColor}90)` filter: `drop-shadow(0 0 6px ${fillColor}90)`
} }
: undefined), [fillColor]) : undefined
}, [hover, pending, nextColor, color, meSats])
return ( return (
<div ref={ref} className='upvoteParent'> <div ref={ref} className='upvoteParent'>

View File

@ -5,6 +5,13 @@
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
.boost {
fill: var(--theme-clickToContextColor);
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
.upvoteWrapper { .upvoteWrapper {
position: relative; position: relative;
padding-right: .2rem; padding-right: .2rem;

View File

@ -62,7 +62,7 @@ export const ITEM_FILTER_THRESHOLD = 1.2
export const DONT_LIKE_THIS_COST = 1 export const DONT_LIKE_THIS_COST = 1
export const COMMENT_TYPE_QUERY = ['comments', 'freebies', 'outlawed', 'borderland', 'all', 'bookmarks'] export const COMMENT_TYPE_QUERY = ['comments', 'freebies', 'outlawed', 'borderland', 'all', 'bookmarks']
export const USER_SORTS = ['value', 'stacking', 'spending', 'comments', 'posts', 'referrals'] export const USER_SORTS = ['value', 'stacking', 'spending', 'comments', 'posts', 'referrals']
export const ITEM_SORTS = ['zaprank', 'comments', 'sats'] export const ITEM_SORTS = ['zaprank', 'comments', 'sats', 'boost']
export const SUB_SORTS = ['stacking', 'revenue', 'spending', 'posts', 'comments'] export const SUB_SORTS = ['stacking', 'revenue', 'spending', 'posts', 'comments']
export const WHENS = ['day', 'week', 'month', 'year', 'forever', 'custom'] export const WHENS = ['day', 'week', 'month', 'year', 'forever', 'custom']
export const ITEM_TYPES_USER = ['all', 'posts', 'comments', 'bounties', 'links', 'discussions', 'polls', 'freebies', 'jobs', 'bookmarks'] export const ITEM_TYPES_USER = ['all', 'posts', 'comments', 'bounties', 'links', 'discussions', 'polls', 'freebies', 'jobs', 'bookmarks']

View File

@ -22,14 +22,14 @@ export class Relay {
constructor (relayUrl) { constructor (relayUrl) {
const ws = new WebSocket(relayUrl) const ws = new WebSocket(relayUrl)
ws.onmessage = function (msg) { ws.onmessage = (msg) => {
const [type, notice] = JSON.parse(msg.data) const [type, notice] = JSON.parse(msg.data)
if (type === 'NOTICE') { if (type === 'NOTICE') {
console.log('relay notice:', notice) console.log('relay notice:', notice)
} }
} }
ws.onerror = function (err) { ws.onerror = (err) => {
console.error('websocket error:', err.message) console.error('websocket error:', err.message)
this.error = err.message this.error = err.message
} }

View File

@ -1,5 +1,9 @@
export default function getColor (meSats) { export default function getColor (meSats) {
if (!meSats || meSats <= 10) { if (!meSats || meSats === 0) {
return '#a5a5a5'
}
if (meSats <= 10) {
return 'var(--bs-secondary)' return 'var(--bs-secondary)'
} }

View File

@ -131,7 +131,7 @@ export default function Rewards ({ ssrData }) {
return ( return (
<Layout footerLinks> <Layout footerLinks>
{ad && {ad &&
<div className='pt-3 align-self-center' style={{ maxWidth: '480px', width: '100%' }}> <div className='pt-3 align-self-center' style={{ maxWidth: '500px', width: '100%' }}>
<div className='fw-bold text-muted pb-2'> <div className='fw-bold text-muted pb-2'>
top boost this month top boost this month
</div> </div>

View File

@ -0,0 +1,18 @@
CREATE OR REPLACE FUNCTION expire_boost_jobs()
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
BEGIN
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
SELECT 'expireBoost', jsonb_build_object('id', "Item".id), 21, true, now(), interval '1 days'
FROM "Item"
WHERE "Item".boost > 0 ON CONFLICT DO NOTHING;
return 0;
EXCEPTION WHEN OTHERS THEN
return 0;
END;
$$;
SELECT expire_boost_jobs();
DROP FUNCTION IF EXISTS expire_boost_jobs;

View File

@ -1,8 +1,8 @@
const { ApolloClient, InMemoryCache, HttpLink, gql } = require('@apollo/client') const { ApolloClient, InMemoryCache, HttpLink, gql } = require('@apollo/client')
const ITEMS = gql` const ITEMS = gql`
query items ($sort: String, $when: String, $sub: String) { query items ($sort: String, $when: String, $sub: String, $by: String) {
items (sort: $sort, when: $when, sub: $sub) { items (sort: $sort, when: $when, sub: $sub, by: $by) {
cursor cursor
items { items {
id id
@ -15,6 +15,7 @@ const ITEMS = gql`
location location
remote remote
boost boost
subName
user { user {
id id
name name
@ -152,15 +153,15 @@ async function main () {
variables: { sort: 'top', when: 'week', sub: 'meta' } variables: { sort: 'top', when: 'week', sub: 'meta' }
}) })
const jobs = await client.query({ const ama = await client.query({
query: ITEMS, query: ITEMS,
variables: { sub: 'jobs' } variables: { sort: 'top', when: 'week', sub: 'ama' }
}) })
// const thisDay = await client.query({ const boosts = await client.query({
// query: SEARCH, query: ITEMS,
// variables: { q: 'This Day in Stacker News @Undisciplined', sort: 'recent', what: 'posts', when: 'week' } variables: { sort: 'top', when: 'forever', by: 'boost' }
// }) })
const topMeme = await bountyWinner('meme monday') const topMeme = await bountyWinner('meme monday')
const topFact = await bountyWinner('fun fact') const topFact = await bountyWinner('fun fact')
@ -179,6 +180,13 @@ ${top.data.items.items.map((item, i) =>
`${i + 1}. [${item.title}](https://stacker.news/items/${item.id}) `${i + 1}. [${item.title}](https://stacker.news/items/${item.id})
- ${abbrNum(item.sats)} sats${item.boost ? ` \\ ${abbrNum(item.boost)} boost` : ''} \\ ${item.ncomments} comments \\ [@${item.user.name}](https://stacker.news/${item.user.name})\n`).join('')} - ${abbrNum(item.sats)} sats${item.boost ? ` \\ ${abbrNum(item.boost)} boost` : ''} \\ ${item.ncomments} comments \\ [@${item.user.name}](https://stacker.news/${item.user.name})\n`).join('')}
##### Top AMAs
${ama.data.items.items.slice(0, 3).map((item, i) =>
`${i + 1}. [${item.title}](https://stacker.news/items/${item.id})
- ${abbrNum(item.sats)} sats${item.boost ? ` \\ ${abbrNum(item.boost)} boost` : ''} \\ ${item.ncomments} comments \\ [@${item.user.name}](https://stacker.news/${item.user.name})\n`).join('')}
[**all AMAs**](https://stacker.news/~meta/top/posts/forever)
##### Don't miss ##### Don't miss
${top.data.items.items.map((item, i) => ${top.data.items.items.map((item, i) =>
`- [${item.title}](https://stacker.news/items/${item.id})\n`).join('')} `- [${item.title}](https://stacker.news/items/${item.id})\n`).join('')}
@ -230,9 +238,12 @@ ${topCowboys.map((user, i) =>
------ ------
##### Promoted jobs ##### Top Boosts
${jobs.data.items.items.filter(i => i.boost > 0).slice(0, 5).map((item, i) => ${boosts.data.items.items.map((item, i) =>
`${i + 1}. [${item.title.trim()} \\ ${item.company} \\ ${item.location}${item.remote ? ' or Remote' : ''}](https://stacker.news/items/${item.id})\n`).join('')} item.subName === 'jobs'
? `${i + 1}. [${item.title.trim()} \\ ${item.company} \\ ${item.location}${item.remote ? ' or Remote' : ''}](https://stacker.news/items/${item.id})\n`
: `${i + 1}. [${item.title.trim()}](https://stacker.news/items/${item.id})\n`
).join('')}
[**all jobs**](https://stacker.news/~jobs) [**all jobs**](https://stacker.news/~jobs)

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 4.83582L5.79291 11.0429L7.20712 12.4571L12 7.66424L16.7929 12.4571L18.2071 11.0429L12 4.83582ZM12 10.4857L5.79291 16.6928L7.20712 18.107L12 13.3141L16.7929 18.107L18.2071 16.6928L12 10.4857Z"></path></svg>

After

Width:  |  Height:  |  Size: 298 B

View File

@ -16,7 +16,7 @@ export async function expireBoost ({ data: { id }, models }) {
AND "itemId" = ${Number(id)}::INTEGER AND "itemId" = ${Number(id)}::INTEGER
) )
UPDATE "Item" UPDATE "Item"
SET boost = COALESCE(boost.cur_msats, 0), "oldBoost" = COALESCE(boost.old_msats, 0) SET boost = COALESCE(boost.cur_msats, 0) / 1000, "oldBoost" = COALESCE(boost.old_msats, 0) / 1000
FROM boost FROM boost
WHERE "Item".id = ${Number(id)}::INTEGER` WHERE "Item".id = ${Number(id)}::INTEGER`
], ],