diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 7ed1b3a9..6fe0d88a 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -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, diff --git a/components/comment.js b/components/comment.js index bb21390a..4f48f416 100644 --- a/components/comment.js +++ b/components/comment.js @@ -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 = () => ( <> \ @@ -30,20 +33,16 @@ function Parent ({ item, rootText }) { ) - if (!item.root) { - return - } - return ( <> - {Number(item.root.id) !== Number(item.parentId) && } + {Number(root.id) !== Number(item.parentId) && } \ - - {rootText || 'on:'} {item.root.title} + + {rootText || 'on:'} {root?.title} - {item.root.subName && - - {' '}{item.root.subName} + {root.subName && + + {' '}{root.subName} } ) @@ -76,7 +75,9 @@ export function CommentFlat ({ item, ...props }) { } }} > - + + + ) } @@ -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 (
{includeParent && } {bountyPaid && - + } @@ -177,7 +178,7 @@ export default function Comment ({
{!noReply && - {item.root?.bounty && !bountyPaid && } + {root.bounty && !bountyPaid && } } {children}
diff --git a/components/error-boundary.js b/components/error-boundary.js index 5ae01f06..d411f605 100644 --- a/components/error-boundary.js +++ b/components/error-boundary.js @@ -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) diff --git a/components/item-full.js b/components/item-full.js index b1282ba6..fc76a332 100644 --- a/components/item-full.js +++ b/components/item-full.js @@ -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 ( - <> + {item.parentId ? : ( @@ -171,6 +172,6 @@ export default function ItemFull ({ item, bio, ...props }) {
} - +
) } diff --git a/components/items-mixed.js b/components/items-mixed.js index 678ec080..f95f9172 100644 --- a/components/items-mixed.js +++ b/components/items-mixed.js @@ -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 ( <>
{items.map((item, i) => ( - + {item.parentId ? ( <>
-
{ - if (ignoreClick(e)) { - return - } - router.push({ - pathname: '/items/[id]', - query: { id: item.root.id, commentId: item.id } - }, `/items/${item.root.id}`) - }} - > - +
+
) : (item.isJob ? : )} - + ))}
{items.map((item, i) => ( - + {pinMap && pinMap[i + 1] && } {item.parentId - ? <>
+ ? <>
: (item.isJob ? : (item.title ? : (
- +
)))} - + ))}
: (
- + + +
)}
)} diff --git a/components/past-bounties.js b/components/past-bounties.js index f687e3ce..fe7729b3 100644 --- a/components/past-bounties.js +++ b/components/past-bounties.js @@ -1,4 +1,3 @@ -import React from 'react' import { useQuery } from '@apollo/client' import AccordianItem from './accordian-item' import Item, { ItemSkeleton } from './item' diff --git a/components/pay-bounty.js b/components/pay-bounty.js index 64023050..b18fe806 100644 --- a/components/pay-bounty.js +++ b/components/pay-bounty.js @@ -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 (
diff --git a/components/root.js b/components/root.js new file mode 100644 index 00000000..f0cea78f --- /dev/null +++ b/components/root.js @@ -0,0 +1,15 @@ +import { useContext, createContext } from 'react' + +export const RootContext = createContext() + +export function RootProvider ({ root, children }) { + return ( + + {children} + + ) +} + +export function useRoot () { + return useContext(RootContext) +} diff --git a/components/search-items.js b/components/search-items.js index fb2598a3..4c6594dd 100644 --- a/components/search-items.js +++ b/components/search-items.js @@ -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 }) { <>
{items.map((item, i) => ( - + {item.parentId - ? <>
+ ? <>
: <>
} - + ))}
} - return
+ return
} export default function Satistics ({ data: { walletHistory: { facts, cursor } } }) { @@ -231,7 +231,7 @@ export default function Satistics ({ data: { walletHistory: { facts, cursor } } {facts.map((f, i) => { const uri = href(f) - const Wrapper = uri ? Link : ({ href, ...props }) => + const Wrapper = uri ? Link : ({ href, ...props }) => return ( diff --git a/prisma/migrations/20230506214933_comments_func/migration.sql b/prisma/migrations/20230506214933_comments_func/migration.sql new file mode 100644 index 00000000..aa5ab141 --- /dev/null +++ b/prisma/migrations/20230506214933_comments_func/migration.sql @@ -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 +$$; \ No newline at end of file