undo zap/downzap and improve downzap ux

This commit is contained in:
keyan 2023-12-19 19:55:19 -06:00
parent 7e0da18878
commit 65744364f1
13 changed files with 228 additions and 85 deletions

View File

@ -141,7 +141,7 @@ async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args)
return await models.$queryRawUnsafe(` return await models.$queryRawUnsafe(`
SELECT "Item".*, to_jsonb(users.*) || jsonb_build_object('meMute', "Mute"."mutedId" IS NOT NULL) as user, SELECT "Item".*, to_jsonb(users.*) || jsonb_build_object('meMute', "Mute"."mutedId" IS NOT NULL) as user,
COALESCE("ItemAct"."meMsats", 0) as "meMsats", COALESCE("ItemAct"."meMsats", 0) as "meMsats",
COALESCE("ItemAct"."meDontLike", false) as "meDontLike", b."itemId" IS NOT NULL AS "meBookmark", COALESCE("ItemAct"."meDontLikeMsats", 0) as "meDontLikeMsats", b."itemId" IS NOT NULL AS "meBookmark",
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward" "ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward"
FROM ( FROM (
${query} ${query}
@ -153,7 +153,7 @@ async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args)
LEFT JOIN "ItemForward" ON "ItemForward"."itemId" = "Item".id AND "ItemForward"."userId" = ${me.id} LEFT JOIN "ItemForward" ON "ItemForward"."itemId" = "Item".id AND "ItemForward"."userId" = ${me.id}
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT "itemId", sum("ItemAct".msats) FILTER (WHERE act = 'FEE' OR act = 'TIP') AS "meMsats", SELECT "itemId", sum("ItemAct".msats) FILTER (WHERE act = 'FEE' OR act = 'TIP') AS "meMsats",
bool_or(act = 'DONT_LIKE_THIS') AS "meDontLike" sum("ItemAct".msats) FILTER (WHERE act = 'DONT_LIKE_THIS') AS "meDontLikeMsats"
FROM "ItemAct" FROM "ItemAct"
WHERE "ItemAct"."userId" = ${me.id} WHERE "ItemAct"."userId" = ${me.id}
AND "ItemAct"."itemId" = "Item".id AND "ItemAct"."itemId" = "Item".id
@ -805,7 +805,7 @@ export default {
{ me, models, lnd, hash, hmac } { me, models, lnd, hash, hmac }
) )
return true return sats
} }
}, },
Item: { Item: {
@ -933,11 +933,16 @@ export default {
return (msats && msatsToSats(msats)) || 0 return (msats && msatsToSats(msats)) || 0
}, },
meDontLike: async (item, args, { me, models }) => { meDontLikeSats: async (item, args, { me, models }) => {
if (!me) return false if (!me) return false
if (typeof item.meDontLike !== 'undefined') return item.meDontLike if (typeof item.meMsats !== 'undefined') {
return msatsToSats(item.meDontLikeMsats)
}
const dontLike = await models.itemAct.findFirst({ const { _sum: { msats } } = await models.itemAct.aggregate({
_sum: {
msats: true
},
where: { where: {
itemId: Number(item.id), itemId: Number(item.id),
userId: me.id, userId: me.id,
@ -945,7 +950,7 @@ export default {
} }
}) })
return !!dontLike return (msats && msatsToSats(msats)) || 0
}, },
meBookmark: async (item, args, { me, models }) => { meBookmark: async (item, args, { me, models }) => {
if (!me) return false if (!me) return false

View File

@ -34,7 +34,7 @@ export default gql`
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item! upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item!
updateNoteId(id: ID!, noteId: String!): Item! updateNoteId(id: ID!, noteId: String!): Item!
upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item! upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item!
dontLikeThis(id: ID!, sats: Int, hash: String, hmac: String): Boolean! dontLikeThis(id: ID!, sats: Int, hash: String, hmac: String): Int!
act(id: ID!, sats: Int, hash: String, hmac: String): ItemActResult! act(id: ID!, sats: Int, hash: String, hmac: String): ItemActResult!
pollVote(id: ID!, hash: String, hmac: String): ID! pollVote(id: ID!, hash: String, hmac: String): ID!
} }
@ -90,7 +90,7 @@ export default gql`
lastCommentAt: Date lastCommentAt: Date
upvotes: Int! upvotes: Int!
meSats: Int! meSats: Int!
meDontLike: Boolean! meDontLikeSats: Int!
meBookmark: Boolean! meBookmark: Boolean!
meSubscription: Boolean! meSubscription: Boolean!
meForward: Boolean meForward: Boolean

View File

@ -14,7 +14,6 @@ import { ignoreClick } from '../lib/clicks'
import PayBounty from './pay-bounty' import PayBounty from './pay-bounty'
import BountyIcon from '../svgs/bounty-bag.svg' import BountyIcon from '../svgs/bounty-bag.svg'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
import Flag from '../svgs/flag-fill.svg'
import { numWithUnits } from '../lib/format' import { numWithUnits } from '../lib/format'
import Share from './share' import Share from './share'
import ItemInfo from './item-info' import ItemInfo from './item-info'
@ -22,6 +21,7 @@ import Badge from 'react-bootstrap/Badge'
import { RootProvider, useRoot } from './root' import { RootProvider, useRoot } from './root'
import { useMe } from './me' import { useMe } from './me'
import { useQuoteReply } from './use-quote-reply' import { useQuoteReply } from './use-quote-reply'
import { DownZap } from './dont-link-this'
function Parent ({ item, rootText }) { function Parent ({ item, rootText }) {
const root = useRoot() const root = useRoot()
@ -146,8 +146,8 @@ 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.meDontLike {item.meDontLikeSats > item.meSats
? <Flag width={24} height={24} className={styles.dontLike} /> ? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} />
: <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />} : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
<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'>

View File

@ -16,6 +16,7 @@
padding: 2px; padding: 2px;
margin-left: 1px; margin-left: 1px;
margin-top: 9px; margin-top: 9px;
cursor: pointer;
} }
.text { .text {

View File

@ -4,8 +4,23 @@ 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 Flag from '../svgs/flag-fill.svg'
import { useMemo } from 'react'
import getColor from '../lib/rainbow'
export default function DontLikeThisDropdownItem ({ id }) { export function DownZap ({ id, meDontLikeSats, ...props }) {
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} />} />
)
}
function DownZapper ({ id, As, children }) {
const toaster = useToast() const toaster = useToast()
const showModal = useShowModal() const showModal = useShowModal()
@ -14,12 +29,12 @@ export default function DontLikeThisDropdownItem ({ id }) {
mutation dontLikeThis($id: ID!, $sats: Int, $hash: String, $hmac: String) { mutation dontLikeThis($id: ID!, $sats: Int, $hash: String, $hmac: String) {
dontLikeThis(id: $id, sats: $sats, hash: $hash, hmac: $hmac) dontLikeThis(id: $id, sats: $sats, hash: $hash, hmac: $hmac)
}`, { }`, {
update (cache) { update (cache, { data: { dontLikeThis } }) {
cache.modify({ cache.modify({
id: `Item:${id}`, id: `Item:${id}`,
fields: { fields: {
meDontLike () { meDontLikeSats (existingSats = 0) {
return true return existingSats + dontLikeThis
} }
} }
}) })
@ -28,7 +43,7 @@ export default function DontLikeThisDropdownItem ({ id }) {
) )
return ( return (
<Dropdown.Item <As
onClick={async () => { onClick={async () => {
try { try {
showModal(onClose => showModal(onClose =>
@ -53,7 +68,18 @@ export default function DontLikeThisDropdownItem ({ id }) {
} }
}} }}
> >
<span className='text-danger'>downzap</span> {children}
</Dropdown.Item> </As>
)
}
export default function DontLikeThisDropdownItem ({ id }) {
return (
<DownZapper
As={Dropdown.Item}
id={id}
>
<span className='text-danger'>downzap</span>
</DownZapper>
) )
} }

View File

@ -18,6 +18,7 @@ import Hat from './hat'
import { AD_USER_ID } from '../lib/constants' import { AD_USER_ID } from '../lib/constants'
import ActionDropdown from './action-dropdown' import ActionDropdown from './action-dropdown'
import MuteDropdownItem from './mute' import MuteDropdownItem from './mute'
import { DropdownItemUpVote } from './upvote'
export default function ItemInfo ({ export default function ItemInfo ({
item, pendingSats, full, commentsText = 'comments', item, pendingSats, full, commentsText = 'comments',
@ -39,7 +40,7 @@ export default function ItemInfo ({
}, [item]) }, [item])
useEffect(() => { useEffect(() => {
if (item) setMeTotalSats(item.meSats + item.meAnonSats + pendingSats) if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0) + (pendingSats || 0))
}, [item?.meSats, item?.meAnonSats, pendingSats]) }, [item?.meSats, item?.meAnonSats, pendingSats])
return ( return (
@ -52,7 +53,9 @@ 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 })} from me)`} `} : `(${numWithUnits(meTotalSats, { abbreviate: false })}${item.meDontLikeSats
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
: ''} from me)`} `}
> >
{numWithUnits(item.sats + pendingSats)} {numWithUnits(item.sats + pendingSats)}
</span> </span>
@ -143,8 +146,11 @@ export default function ItemInfo ({
<Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'> <Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
opentimestamp opentimestamp
</Link>} </Link>}
{me && !item.meSats && !item.position && {me && !item.position &&
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />} !item.mine && !item.deletedAt &&
(item.meDontLikeSats > meTotalSats
? <DropdownItemUpVote item={item} />
: <DontLikeThisDropdownItem id={item.id} />)}
{me && item?.noteId && ( {me && item?.noteId && (
<Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener')}> <Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener')}>
nostr note nostr note

View File

@ -8,7 +8,6 @@ import reactStringReplace from 'react-string-replace'
import PollIcon from '../svgs/bar-chart-horizontal-fill.svg' import PollIcon from '../svgs/bar-chart-horizontal-fill.svg'
import BountyIcon from '../svgs/bounty-bag.svg' import BountyIcon from '../svgs/bounty-bag.svg'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
import Flag from '../svgs/flag-fill.svg'
import ImageIcon from '../svgs/image-fill.svg' import ImageIcon from '../svgs/image-fill.svg'
import { numWithUnits } from '../lib/format' import { numWithUnits } from '../lib/format'
import ItemInfo from './item-info' import ItemInfo from './item-info'
@ -17,6 +16,7 @@ import { commentsViewedAt } from '../lib/new-comments'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Badge } from 'react-bootstrap' import { Badge } from 'react-bootstrap'
import AdIcon from '../svgs/advertisement-fill.svg' import AdIcon from '../svgs/advertisement-fill.svg'
import { DownZap } from './dont-link-this'
export function SearchTitle ({ title }) { export function SearchTitle ({ title }) {
return reactStringReplace(title, /\*\*\*([^*]+)\*\*\*/g, (match, i) => { return reactStringReplace(title, /\*\*\*([^*]+)\*\*\*/g, (match, i) => {
@ -43,8 +43,8 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
<div className={`${styles.item} ${siblingComments ? 'pt-3' : ''}`}> <div className={`${styles.item} ${siblingComments ? 'pt-3' : ''}`}>
{item.position {item.position
? <Pin width={24} height={24} className={styles.pin} /> ? <Pin width={24} height={24} className={styles.pin} />
: item.meDontLike : item.meDontLikeSats > item.meSats
? <Flag width={24} height={24} className={styles.dontLike} /> ? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} />
: Number(item.user?.id) === AD_USER_ID : Number(item.user?.id) === AD_USER_ID
? <AdIcon width={24} height={24} className={styles.ad} /> ? <AdIcon width={24} height={24} className={styles.ad} />
: <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />} : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}

View File

@ -65,6 +65,7 @@ a.title:visited {
margin-right: .35rem; margin-right: .35rem;
margin-left: -.2rem; margin-left: -.2rem;
flex-shrink: 0; flex-shrink: 0;
cursor: pointer;
} }
.case { .case {

View File

@ -4,7 +4,7 @@ import { gql, useMutation } from '@apollo/client'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
import ItemAct from './item-act' import ItemAct from './item-act'
import { useMe } from './me' import { useMe } from './me'
import Rainbow from '../lib/rainbow' import getColor from '../lib/rainbow'
import { useCallback, useMemo, useRef, useState } from 'react' import { useCallback, useMemo, useRef, useState } from 'react'
import LongPressable from 'react-longpressable' import LongPressable from 'react-longpressable'
import Overlay from 'react-bootstrap/Overlay' import Overlay from 'react-bootstrap/Overlay'
@ -15,17 +15,7 @@ import { numWithUnits } from '../lib/format'
import { payOrLoginError, useInvoiceModal } from './invoice' import { payOrLoginError, useInvoiceModal } from './invoice'
import useDebounceCallback from './use-debounce-callback' import useDebounceCallback from './use-debounce-callback'
import { useToast } from './toast' import { useToast } from './toast'
import { Dropdown } from 'react-bootstrap'
const getColor = (meSats) => {
if (!meSats || meSats <= 10) {
return 'var(--bs-secondary)'
}
const idx = Math.min(
Math.floor((Math.log(meSats) / Math.log(10000)) * (Rainbow.length - 1)),
Rainbow.length - 1)
return Rainbow[idx]
}
const UpvotePopover = ({ target, show, handleClose }) => { const UpvotePopover = ({ target, show, handleClose }) => {
const me = useMe() const me = useMe()
@ -66,6 +56,72 @@ const TipPopover = ({ target, show, handleClose }) => (
</Overlay> </Overlay>
) )
function useAct ({ item, setVoteShow = () => {}, setTipShow = () => {} }) {
const me = useMe()
return useMutation(
gql`
mutation act($id: ID!, $sats: Int!, $hash: String, $hmac: String) {
act(id: $id, sats: $sats, hash: $hash, hmac: $hmac) {
sats
}
}`, {
update (cache, { data: { act: { sats } } }) {
cache.modify({
id: `Item:${item.id}`,
fields: {
sats (existingSats = 0) {
return existingSats + sats
},
meSats: me
? (existingSats = 0) => {
if (sats <= me.privates?.sats) {
if (existingSats === 0) {
setVoteShow(true)
} else {
setTipShow(true)
}
}
return existingSats + sats
}
: undefined
}
})
// update all ancestors
item.path.split('.').forEach(id => {
if (Number(id) === Number(item.id)) return
cache.modify({
id: `Item:${id}`,
fields: {
commentSats (existingCommentSats = 0) {
return existingCommentSats + sats
}
}
})
})
}
}
)
}
export function DropdownItemUpVote ({ item }) {
const showModal = useShowModal()
const [act] = useAct({ item })
return (
<Dropdown.Item
onClick={async () => {
showModal(onClose =>
<ItemAct onClose={onClose} itemId={item.id} act={act} />)
}}
>
<span className='text-success'>zap</span>
</Dropdown.Item>
)
}
export default function UpVote ({ item, className, pendingSats, setPendingSats }) { export default function UpVote ({ item, className, pendingSats, setPendingSats }) {
const showModal = useShowModal() const showModal = useShowModal()
const [voteShow, _setVoteShow] = useState(false) const [voteShow, _setVoteShow] = useState(false)
@ -110,51 +166,8 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
} }
}, [me, tipShow, setWalkthrough]) }, [me, tipShow, setWalkthrough])
const [act] = useMutation( const [act] = useAct({ item, setVoteShow, setTipShow })
gql`
mutation act($id: ID!, $sats: Int!, $hash: String, $hmac: String) {
act(id: $id, sats: $sats, hash: $hash, hmac: $hmac) {
sats
}
}`, {
update (cache, { data: { act: { sats } } }) {
cache.modify({
id: `Item:${item.id}`,
fields: {
sats (existingSats = 0) {
return existingSats + sats
},
meSats: me
? (existingSats = 0) => {
if (sats <= me.privates?.sats) {
if (existingSats === 0) {
setVoteShow(true)
} else {
setTipShow(true)
}
}
return existingSats + sats
}
: undefined
}
})
// update all ancestors
item.path.split('.').forEach(id => {
if (Number(id) === Number(item.id)) return
cache.modify({
id: `Item:${id}`,
fields: {
commentSats (existingCommentSats = 0) {
return existingCommentSats + sats
}
}
})
})
}
}
)
const showInvoiceModal = useInvoiceModal( const showInvoiceModal = useInvoiceModal(
async ({ hash, hmac }, { variables }) => { async ({ hash, hmac }, { variables }) => {
await act({ variables: { ...variables, hash, hmac } }) await act({ variables: { ...variables, hash, hmac } })

View File

@ -21,7 +21,7 @@ export const COMMENT_FIELDS = gql`
freedFreebie freedFreebie
boost boost
meSats meSats
meDontLike meDontLikeSats
meBookmark meBookmark
meSubscription meSubscription
outlawed outlawed

View File

@ -28,7 +28,7 @@ export const ITEM_FIELDS = gql`
path path
upvotes upvotes
meSats meSats
meDontLike meDontLikeSats
meBookmark meBookmark
meSubscription meSubscription
meForward meForward

View File

@ -1,4 +1,15 @@
export default [ export default function getColor (meSats) {
if (!meSats || meSats <= 10) {
return 'var(--bs-secondary)'
}
const idx = Math.min(
Math.floor((Math.log(meSats) / Math.log(10000)) * (Rainbow.length - 1)),
Rainbow.length - 1)
return Rainbow[idx]
}
const Rainbow = [
'#f6911d', '#f6911d',
'#f6921e', '#f6921e',
'#f6931f', '#f6931f',

View File

@ -0,0 +1,80 @@
CREATE OR REPLACE FUNCTION item_comments_zaprank_with_me(_item_id int, _global_seed int, _me_id int, _level int, _where text, _order_by text)
RETURNS jsonb
LANGUAGE plpgsql VOLATILE PARALLEL SAFE AS
$$
DECLARE
result jsonb;
BEGIN
IF _level < 1 THEN
RETURN '[]'::jsonb;
END IF;
EXECUTE 'CREATE TEMP TABLE IF NOT EXISTS t_item ON COMMIT DROP AS'
|| ' SELECT "Item".*, "Item".created_at at time zone ''UTC'' AS "createdAt", "Item".updated_at at time zone ''UTC'' AS "updatedAt", '
|| ' to_jsonb(users.*) || jsonb_build_object(''meMute'', "Mute"."mutedId" IS NOT NULL) AS user, '
|| ' COALESCE("ItemAct"."meMsats", 0) AS "meMsats", COALESCE("ItemAct"."meDontLikeMsats", 0) AS "meDontLikeMsats", '
|| ' "Bookmark"."itemId" IS NOT NULL AS "meBookmark", "ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", '
|| ' GREATEST(g.tf_hot_score, l.tf_hot_score) AS personal_hot_score, GREATEST(g.tf_top_score, l.tf_top_score) AS personal_top_score '
|| ' FROM "Item" '
|| ' JOIN users ON users.id = "Item"."userId" '
|| ' LEFT JOIN "Mute" ON "Mute"."muterId" = $5 AND "Mute"."mutedId" = "Item"."userId"'
|| ' LEFT JOIN "Bookmark" ON "Bookmark"."userId" = $5 AND "Bookmark"."itemId" = "Item".id '
|| ' LEFT JOIN "ThreadSubscription" ON "ThreadSubscription"."userId" = $5 AND "ThreadSubscription"."itemId" = "Item".id '
|| ' LEFT JOIN LATERAL ( '
|| ' SELECT "itemId", sum("ItemAct".msats) FILTER (WHERE act = ''FEE'' OR act = ''TIP'') AS "meMsats", '
|| ' sum("ItemAct".msats) FILTER (WHERE act = ''DONT_LIKE_THIS'') AS "meDontLikeMsats" '
|| ' FROM "ItemAct" '
|| ' WHERE "ItemAct"."userId" = $5 '
|| ' AND "ItemAct"."itemId" = "Item".id '
|| ' GROUP BY "ItemAct"."itemId" '
|| ' ) "ItemAct" ON true '
|| ' LEFT JOIN zap_rank_personal_view g ON g."viewerId" = $6 AND g.id = "Item".id '
|| ' LEFT JOIN zap_rank_personal_view l ON l."viewerId" = $5 AND l.id = g.id '
|| ' WHERE "Item".path <@ (SELECT path FROM "Item" WHERE id = $1) ' || _where || ' '
USING _item_id, _level, _where, _order_by, _me_id, _global_seed;
EXECUTE ''
|| 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
|| 'FROM ( '
|| ' SELECT "Item".*, item_comments_zaprank_with_me("Item".id, $6, $5, $2 - 1, $3, $4) AS comments '
|| ' FROM t_item "Item" '
|| ' WHERE "Item"."parentId" = $1 '
|| _order_by
|| ' ) sub'
INTO result USING _item_id, _level, _where, _order_by, _me_id, _global_seed;
RETURN result;
END
$$;
CREATE OR REPLACE FUNCTION item_comments(_item_id int, _level int, _where text, _order_by text)
RETURNS jsonb
LANGUAGE plpgsql VOLATILE PARALLEL SAFE AS
$$
DECLARE
result jsonb;
BEGIN
IF _level < 1 THEN
RETURN '[]'::jsonb;
END IF;
EXECUTE 'CREATE TEMP TABLE IF NOT EXISTS t_item ON COMMIT DROP AS'
|| ' SELECT "Item".*, "Item".created_at at time zone ''UTC'' AS "createdAt", "Item".updated_at at time zone ''UTC'' AS "updatedAt", '
|| ' to_jsonb(users.*) as user '
|| ' FROM "Item" '
|| ' JOIN users ON users.id = "Item"."userId" '
|| ' WHERE "Item".path <@ (SELECT path FROM "Item" WHERE id = $1) ' || _where
USING _item_id, _level, _where, _order_by;
EXECUTE ''
|| 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
|| 'FROM ( '
|| ' SELECT "Item".*, item_comments("Item".id, $2 - 1, $3, $4) AS comments '
|| ' FROM t_item "Item"'
|| ' WHERE "Item"."parentId" = $1 '
|| _order_by
|| ' ) sub'
INTO result USING _item_id, _level, _where, _order_by;
RETURN result;
END
$$;