improve comment performance
This commit is contained in:
parent
1a5d8880dd
commit
347a6a54d2
|
@ -6,7 +6,7 @@ import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
|
|||
import domino from 'domino'
|
||||
import {
|
||||
BOOST_MIN, ITEM_SPAM_INTERVAL,
|
||||
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST
|
||||
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT
|
||||
} from '../../lib/constants'
|
||||
import { msatsToSats } from '../../lib/format'
|
||||
import { parse } from 'tldts'
|
||||
|
@ -27,20 +27,9 @@ async function comments (me, models, id, sort, root) {
|
|||
break
|
||||
}
|
||||
|
||||
const flat = await models.$queryRaw(`
|
||||
WITH RECURSIVE base AS (
|
||||
${SELECT}, ARRAY[row_number() OVER (${orderBy}, "Item".path)] AS sort_path
|
||||
FROM "Item"
|
||||
WHERE "parentId" = $1
|
||||
${await filterClause(me, models)}
|
||||
UNION ALL
|
||||
${SELECT}, p.sort_path || row_number() OVER (${orderBy}, "Item".path)
|
||||
FROM base p
|
||||
JOIN "Item" ON "Item"."parentId" = p.id
|
||||
WHERE true
|
||||
${await filterClause(me, models)})
|
||||
SELECT * FROM base ORDER BY sort_path`, Number(id))
|
||||
return nestComments(flat, id, root)[0]
|
||||
const filter = await filterClause(me, models)
|
||||
const [{ item_comments: comments }] = await models.$queryRaw('SELECT item_comments($1, $2, $3, $4)', Number(id), COMMENT_DEPTH_LIMIT, orderBy, filter)
|
||||
return comments
|
||||
}
|
||||
|
||||
export async function getItem (parent, { id }, { me, models }) {
|
||||
|
@ -1120,32 +1109,6 @@ const createItem = async (parent, { sub, title, url, text, boost, forward, bount
|
|||
return item
|
||||
}
|
||||
|
||||
function nestComments (flat, parentId, root) {
|
||||
const result = []
|
||||
let added = 0
|
||||
for (let i = 0; i < flat.length;) {
|
||||
flat[i].root = root
|
||||
if (!flat[i].comments) flat[i].comments = []
|
||||
if (Number(flat[i].parentId) === Number(parentId)) {
|
||||
result.push(flat[i])
|
||||
added++
|
||||
i++
|
||||
} else if (result.length > 0) {
|
||||
const item = result[result.length - 1]
|
||||
const [nested, newAdded] = nestComments(flat.slice(i), item.id, root)
|
||||
if (newAdded === 0) {
|
||||
break
|
||||
}
|
||||
item.comments.push(...nested)
|
||||
i += newAdded
|
||||
added += newAdded
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return [result, added]
|
||||
}
|
||||
|
||||
// we have to do our own query because ltree is unsupported
|
||||
export const SELECT =
|
||||
`SELECT "Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title,
|
||||
|
|
|
@ -19,8 +19,11 @@ import { abbrNum } from '../lib/format'
|
|||
import Share from './share'
|
||||
import ItemInfo from './item-info'
|
||||
import { Badge } from 'react-bootstrap'
|
||||
import { RootProvider, useRoot } from './root'
|
||||
|
||||
function Parent ({ item, rootText }) {
|
||||
const root = useRoot()
|
||||
|
||||
const ParentFrag = () => (
|
||||
<>
|
||||
<span> \ </span>
|
||||
|
@ -30,20 +33,16 @@ function Parent ({ item, rootText }) {
|
|||
</>
|
||||
)
|
||||
|
||||
if (!item.root) {
|
||||
return <ParentFrag />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Number(item.root.id) !== Number(item.parentId) && <ParentFrag />}
|
||||
{Number(root.id) !== Number(item.parentId) && <ParentFrag />}
|
||||
<span> \ </span>
|
||||
<Link href={`/items/${item.root.id}`} passHref>
|
||||
<a className='text-reset'>{rootText || 'on:'} {item.root.title}</a>
|
||||
<Link href={`/items/${root.id}`} passHref>
|
||||
<a className='text-reset'>{rootText || 'on:'} {root?.title}</a>
|
||||
</Link>
|
||||
{item.root.subName &&
|
||||
<Link href={`/~${item.root.subName}`}>
|
||||
<a>{' '}<Badge className={itemStyles.newComment} variant={null}>{item.root.subName}</Badge></a>
|
||||
{root.subName &&
|
||||
<Link href={`/~${root.subName}`}>
|
||||
<a>{' '}<Badge className={itemStyles.newComment} variant={null}>{root.subName}</Badge></a>
|
||||
</Link>}
|
||||
</>
|
||||
)
|
||||
|
@ -76,7 +75,9 @@ export function CommentFlat ({ item, ...props }) {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Comment item={item} {...props} />
|
||||
<RootProvider root={item.root}>
|
||||
<Comment item={item} {...props} />
|
||||
</RootProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -89,6 +90,7 @@ export default function Comment ({
|
|||
const [collapse, setCollapse] = useState(false)
|
||||
const ref = useRef(null)
|
||||
const router = useRouter()
|
||||
const root = useRoot()
|
||||
|
||||
useEffect(() => {
|
||||
if (Number(router.query.commentId) === Number(item.id)) {
|
||||
|
@ -103,9 +105,8 @@ export default function Comment ({
|
|||
}, [item])
|
||||
|
||||
const bottomedOut = depth === COMMENT_DEPTH_LIMIT
|
||||
|
||||
const op = item.root?.user.name === item.user.name
|
||||
const bountyPaid = item.root?.bountyPaidTo?.includes(Number(item.id))
|
||||
const op = root.user.name === item.user.name
|
||||
const bountyPaid = root.bountyPaidTo?.includes(Number(item.id))
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -126,7 +127,7 @@ export default function Comment ({
|
|||
<>
|
||||
{includeParent && <Parent item={item} rootText={rootText} />}
|
||||
{bountyPaid &&
|
||||
<ActionTooltip notForm overlayText={`${abbrNum(item.root.bounty)} sats paid`}>
|
||||
<ActionTooltip notForm overlayText={`${abbrNum(root.bounty)} sats paid`}>
|
||||
<BountyIcon className={`${styles.bountyIcon} ${'fill-success vertical-align-middle'}`} height={16} width={16} />
|
||||
</ActionTooltip>}
|
||||
</>
|
||||
|
@ -177,7 +178,7 @@ export default function Comment ({
|
|||
<div className={`${styles.children}`}>
|
||||
{!noReply &&
|
||||
<Reply depth={depth + 1} item={item} replyOpen={replyOpen}>
|
||||
{item.root?.bounty && !bountyPaid && <PayBounty item={item} />}
|
||||
{root.bounty && !bountyPaid && <PayBounty item={item} />}
|
||||
</Reply>}
|
||||
{children}
|
||||
<div className={`${styles.comments} ml-sm-1 ml-md-3`}>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react'
|
||||
import { Component } from 'react'
|
||||
import LayoutStatic from './layout-static'
|
||||
import styles from '../styles/404.module.css'
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
class ErrorBoundary extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import Check from '../svgs/check-double-line.svg'
|
|||
import Share from './share'
|
||||
import Toc from './table-of-contents'
|
||||
import Link from 'next/link'
|
||||
import { RootProvider } from './root'
|
||||
|
||||
function BioItem ({ item, handleClick }) {
|
||||
const me = useMe()
|
||||
|
@ -157,7 +158,7 @@ export default function ItemFull ({ item, bio, ...props }) {
|
|||
}, [item.lastCommentAt])
|
||||
|
||||
return (
|
||||
<>
|
||||
<RootProvider root={item.root || item}>
|
||||
{item.parentId
|
||||
? <Comment topLevel item={item} replyOpen includeParent noComments {...props} />
|
||||
: (
|
||||
|
@ -171,6 +172,6 @@ export default function ItemFull ({ item, bio, ...props }) {
|
|||
<div className={styles.comments}>
|
||||
<Comments parentId={item.id} pinned={item.position} commentSats={item.commentSats} comments={item.comments} />
|
||||
</div>}
|
||||
</>
|
||||
</RootProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { ignoreClick } from '../lib/clicks'
|
||||
import Comment from './comment'
|
||||
import { Fragment } from 'react'
|
||||
import { CommentFlat } from './comment'
|
||||
import Item from './item'
|
||||
import ItemJob from './item-job'
|
||||
import { ItemsSkeleton } from './items'
|
||||
|
@ -9,33 +7,22 @@ import styles from './items.module.css'
|
|||
import MoreFooter from './more-footer'
|
||||
|
||||
export default function MixedItems ({ rank, items, cursor, fetchMore }) {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<>
|
||||
<div className={styles.grid}>
|
||||
{items.map((item, i) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<Fragment key={item.id}>
|
||||
{item.parentId
|
||||
? (
|
||||
<><div />
|
||||
<div
|
||||
className='pb-1 mb-1 clickToContext' onClick={e => {
|
||||
if (ignoreClick(e)) {
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
pathname: '/items/[id]',
|
||||
query: { id: item.root.id, commentId: item.id }
|
||||
}, `/items/${item.root.id}`)
|
||||
}}
|
||||
>
|
||||
<Comment item={item} noReply includeParent clickToContext />
|
||||
<div className='pb-1 mb-1'>
|
||||
<CommentFlat item={item} noReply includeParent clickToContext />
|
||||
</div>
|
||||
</>)
|
||||
: (item.isJob
|
||||
? <ItemJob item={item} rank={rank && i + 1} />
|
||||
: <Item item={item} rank={rank && i + 1} />)}
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<MoreFooter
|
||||
|
|
|
@ -4,8 +4,8 @@ import ItemJob from './item-job'
|
|||
import styles from './items.module.css'
|
||||
import { ITEMS } from '../fragments/items'
|
||||
import MoreFooter from './more-footer'
|
||||
import React from 'react'
|
||||
import Comment from './comment'
|
||||
import { Fragment } from 'react'
|
||||
import { CommentFlat } from './comment'
|
||||
|
||||
export default function Items ({ variables = {}, query, destructureData, rank, items, pins, cursor }) {
|
||||
const { data, fetchMore } = useQuery(query || ITEMS, { variables })
|
||||
|
@ -28,19 +28,19 @@ export default function Items ({ variables = {}, query, destructureData, rank, i
|
|||
<>
|
||||
<div className={styles.grid}>
|
||||
{items.map((item, i) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<Fragment key={item.id}>
|
||||
{pinMap && pinMap[i + 1] && <Item item={pinMap[i + 1]} />}
|
||||
{item.parentId
|
||||
? <><div /><div className='pb-3'><Comment item={item} noReply includeParent /></div></>
|
||||
? <><div /><div className='pb-3'><CommentFlat item={item} noReply includeParent /></div></>
|
||||
: (item.isJob
|
||||
? <ItemJob item={item} rank={rank && i + 1} />
|
||||
: (item.title
|
||||
? <Item item={item} rank={rank && i + 1} />
|
||||
: (
|
||||
<div className='pb-2'>
|
||||
<Comment item={item} noReply includeParent clickToContext />
|
||||
<CommentFlat item={item} noReply includeParent clickToContext />
|
||||
</div>)))}
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<MoreFooter
|
||||
|
|
|
@ -14,6 +14,7 @@ import HandCoin from '../svgs/hand-coin-fill.svg'
|
|||
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
||||
import CowboyHatIcon from '../svgs/cowboy.svg'
|
||||
import BaldIcon from '../svgs/bald.svg'
|
||||
import { RootProvider } from './root'
|
||||
|
||||
// TODO: oh man, this is a mess ... each notification type should just be a component ...
|
||||
function Notification ({ n }) {
|
||||
|
@ -132,7 +133,9 @@ function Notification ({ n }) {
|
|||
? <Item item={n.item} />
|
||||
: (
|
||||
<div className='pb-2'>
|
||||
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
||||
<RootProvider root={n.item.root}>
|
||||
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
||||
</RootProvider>
|
||||
</div>)}
|
||||
</div>
|
||||
</>)}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import AccordianItem from './accordian-item'
|
||||
import Item, { ItemSkeleton } from './item'
|
||||
|
|
|
@ -8,10 +8,12 @@ import { useMe } from './me'
|
|||
import { abbrNum } from '../lib/format'
|
||||
import { useShowModal } from './modal'
|
||||
import FundError from './fund-error'
|
||||
import { useRoot } from './root'
|
||||
|
||||
export default function PayBounty ({ children, item }) {
|
||||
const me = useMe()
|
||||
const showModal = useShowModal()
|
||||
const root = useRoot()
|
||||
|
||||
const [act] = useMutation(
|
||||
gql`
|
||||
|
@ -48,7 +50,7 @@ export default function PayBounty ({ children, item }) {
|
|||
|
||||
// update root bounty status
|
||||
cache.modify({
|
||||
id: `Item:${item.root.id}`,
|
||||
id: `Item:${root.id}`,
|
||||
fields: {
|
||||
bountyPaidTo (existingPaidTo = []) {
|
||||
return [...(existingPaidTo || []), Number(item.id)]
|
||||
|
@ -62,11 +64,11 @@ export default function PayBounty ({ children, item }) {
|
|||
const handlePayBounty = async () => {
|
||||
try {
|
||||
await act({
|
||||
variables: { id: item.id, sats: item.root.bounty },
|
||||
variables: { id: item.id, sats: root.bounty },
|
||||
optimisticResponse: {
|
||||
act: {
|
||||
id: `Item:${item.id}`,
|
||||
sats: item.root.bounty
|
||||
sats: root.bounty
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -81,14 +83,14 @@ export default function PayBounty ({ children, item }) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!me || item.mine || item.root.user.name !== me.name) {
|
||||
if (!me || item.mine || root.user.name !== me.name) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionTooltip
|
||||
notForm
|
||||
overlayText={`${item.root.bounty} sats`}
|
||||
overlayText={`${root.bounty} sats`}
|
||||
>
|
||||
<ModalButton
|
||||
clicker={
|
||||
|
@ -102,7 +104,7 @@ export default function PayBounty ({ children, item }) {
|
|||
</div>
|
||||
<div className='text-center'>
|
||||
<Button className='mt-4' variant='primary' onClick={() => handlePayBounty()}>
|
||||
pay <small>{abbrNum(item.root.bounty)} sats</small>
|
||||
pay <small>{abbrNum(root.bounty)} sats</small>
|
||||
</Button>
|
||||
</div>
|
||||
</ModalButton>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { useContext, createContext } from 'react'
|
||||
|
||||
export const RootContext = createContext()
|
||||
|
||||
export function RootProvider ({ root, children }) {
|
||||
return (
|
||||
<RootContext.Provider value={root}>
|
||||
{children}
|
||||
</RootContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useRoot () {
|
||||
return useContext(RootContext)
|
||||
}
|
|
@ -3,8 +3,8 @@ import { ItemSkeleton } from './item'
|
|||
import styles from './items.module.css'
|
||||
import { ITEM_SEARCH } from '../fragments/items'
|
||||
import MoreFooter from './more-footer'
|
||||
import React from 'react'
|
||||
import Comment from './comment'
|
||||
import { Fragment } from 'react'
|
||||
import { CommentFlat } from './comment'
|
||||
import ItemFull from './item-full'
|
||||
|
||||
export default function SearchItems ({ variables, items, pins, cursor }) {
|
||||
|
@ -22,11 +22,11 @@ export default function SearchItems ({ variables, items, pins, cursor }) {
|
|||
<>
|
||||
<div className={styles.grid}>
|
||||
{items.map((item, i) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<Fragment key={item.id}>
|
||||
{item.parentId
|
||||
? <><div /><div className='pb-3'><Comment item={item} noReply includeParent /></div></>
|
||||
? <><div /><CommentFlat item={item} noReply includeParent /></>
|
||||
: <><div /><div className={item.text ? 'pb-3' : ''}><ItemFull item={item} noReply /></div></>}
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<MoreFooter
|
||||
|
|
|
@ -26,19 +26,6 @@ export const COMMENT_FIELDS = gql`
|
|||
mine
|
||||
otsHash
|
||||
ncomments
|
||||
root {
|
||||
id
|
||||
title
|
||||
bounty
|
||||
bountyPaidTo
|
||||
subName
|
||||
user {
|
||||
name
|
||||
streak
|
||||
hideCowboyHat
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -50,6 +37,19 @@ export const MORE_FLAT_COMMENTS = gql`
|
|||
cursor
|
||||
comments {
|
||||
...CommentFields
|
||||
root {
|
||||
id
|
||||
title
|
||||
bounty
|
||||
bountyPaidTo
|
||||
subName
|
||||
user {
|
||||
name
|
||||
streak
|
||||
hideCowboyHat
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,19 @@ export const TOP_COMMENTS = gql`
|
|||
cursor
|
||||
comments {
|
||||
...CommentFields
|
||||
root {
|
||||
id
|
||||
title
|
||||
bounty
|
||||
bountyPaidTo
|
||||
subName
|
||||
user {
|
||||
name
|
||||
streak
|
||||
hideCowboyHat
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,6 +72,19 @@ export const SUB_FLAT_COMMENTS = gql`
|
|||
cursor
|
||||
comments {
|
||||
...CommentFields
|
||||
root {
|
||||
id
|
||||
title
|
||||
bounty
|
||||
bountyPaidTo
|
||||
subName
|
||||
user {
|
||||
name
|
||||
streak
|
||||
hideCowboyHat
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ import ThumbDown from '../svgs/thumb-down-fill.svg'
|
|||
import { Checkbox, Form } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import Item from '../components/item'
|
||||
import Comment from '../components/comment'
|
||||
import React from 'react'
|
||||
import { CommentFlat } from '../components/comment'
|
||||
import { Fragment } from 'react'
|
||||
import ItemJob from '../components/item-job'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps(WALLET_HISTORY)
|
||||
|
@ -134,7 +134,7 @@ function Detail ({ fact }) {
|
|||
return <div className={styles.itemWrapper}><Item item={fact.item} /></div>
|
||||
}
|
||||
|
||||
return <div className={styles.commentWrapper}><Comment item={fact.item} includeParent noReply truncate /></div>
|
||||
return <div className={styles.commentWrapper}><CommentFlat item={fact.item} includeParent noReply truncate /></div>
|
||||
}
|
||||
|
||||
export default function Satistics ({ data: { walletHistory: { facts, cursor } } }) {
|
||||
|
@ -231,7 +231,7 @@ export default function Satistics ({ data: { walletHistory: { facts, cursor } }
|
|||
<tbody>
|
||||
{facts.map((f, i) => {
|
||||
const uri = href(f)
|
||||
const Wrapper = uri ? Link : ({ href, ...props }) => <React.Fragment {...props} />
|
||||
const Wrapper = uri ? Link : ({ href, ...props }) => <Fragment {...props} />
|
||||
return (
|
||||
<Wrapper href={uri} key={f.id}>
|
||||
<tr className={styles.row}>
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
CREATE OR REPLACE FUNCTION item_comments(_item_id int, _level int, _where text, _order_by text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql STABLE PARALLEL SAFE AS
|
||||
$$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
BEGIN
|
||||
IF _level < 1 THEN
|
||||
RETURN '[]'::jsonb;
|
||||
END IF;
|
||||
|
||||
EXECUTE ''
|
||||
|| 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
|
||||
|| 'FROM ( '
|
||||
|| ' SELECT "Item".*, "Item".created_at AS "createdAt", "Item".updated_at AS "updatedAt", '
|
||||
|| ' item_comments("Item".id, $2 - 1, $3, $4) AS comments '
|
||||
|| ' FROM "Item" p '
|
||||
|| ' JOIN "Item" ON "Item"."parentId" = p.id '
|
||||
|| ' WHERE p.id = $1 '
|
||||
|| _where || ' '
|
||||
|| _order_by
|
||||
|| ' ) sub'
|
||||
INTO result USING _item_id, _level, _where, _order_by;
|
||||
RETURN result;
|
||||
END
|
||||
$$;
|
Loading…
Reference in New Issue