Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

comment pagination with limit/offset #1824

Merged
merged 13 commits into from
Jan 30, 2025
4 changes: 3 additions & 1 deletion api/paidAction/itemCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,9 @@ export async function onPaid ({ invoice, id }, context) {
SET ncomments = "Item".ncomments + 1,
"lastCommentAt" = GREATEST("Item"."lastCommentAt", comment.created_at),
"weightedComments" = "Item"."weightedComments" +
CASE WHEN comment."userId" = "Item"."userId" THEN 0 ELSE comment.trust END
CASE WHEN comment."userId" = "Item"."userId" THEN 0 ELSE comment.trust END,
"nDirectComments" = "Item"."nDirectComments" +
CASE WHEN comment."parentId" = "Item".id THEN 1 ELSE 0 END
FROM comment
WHERE "Item".path @> comment.path AND "Item".id <> comment.id
RETURNING "Item".*
Expand Down
98 changes: 76 additions & 22 deletions api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
USER_ID, POLL_COST, ADMIN_ITEMS, GLOBAL_SEED,
NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, SN_ADMIN_IDS,
BOOST_MULT,
ITEM_EDIT_SECONDS
ITEM_EDIT_SECONDS,
COMMENTS_LIMIT,
COMMENTS_OF_COMMENT_LIMIT,
FULL_COMMENTS_THRESHOLD
} from '@/lib/constants'
import { msatsToSats } from '@/lib/format'
import { parse } from 'tldts'
Expand All @@ -25,39 +28,76 @@ import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
import { verifyHmac } from './wallet'

function commentsOrderByClause (me, models, sort) {
const sharedSortsArray = []
sharedSortsArray.push('("Item"."pinId" IS NOT NULL) DESC')
sharedSortsArray.push('("Item"."deletedAt" IS NULL) DESC')
const sharedSorts = sharedSortsArray.join(', ')

if (sort === 'recent') {
return 'ORDER BY ("Item"."deletedAt" IS NULL) DESC, ("Item".cost > 0 OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0) DESC, COALESCE("Item"."invoicePaidAt", "Item".created_at) DESC, "Item".id DESC'
return `ORDER BY ${sharedSorts},
("Item".cost > 0 OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0) DESC,
COALESCE("Item"."invoicePaidAt", "Item".created_at) DESC, "Item".id DESC`
}

if (me && sort === 'hot') {
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, COALESCE(
personal_hot_score,
${orderByNumerator({ models, commentScaler: 0, considerBoost: true })}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3)) DESC NULLS LAST,
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
return `ORDER BY ${sharedSorts},
"personal_hot_score" DESC NULLS LAST,
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
} else {
if (sort === 'top') {
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, ${orderByNumerator({ models, commentScaler: 0 })} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
return `ORDER BY ${sharedSorts}, ${orderByNumerator({ models, commentScaler: 0 })} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
} else {
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, ${orderByNumerator({ models, commentScaler: 0, considerBoost: true })}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
return `ORDER BY ${sharedSorts}, ${orderByNumerator({ models, commentScaler: 0, considerBoost: true })}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
}
}
}

async function comments (me, models, id, sort) {
async function comments (me, models, item, sort, cursor) {
const orderBy = commentsOrderByClause(me, models, sort)

if (item.nDirectComments === 0) {
return {
comments: [],
cursor: null
}
}

const decodedCursor = decodeCursor(cursor)
const offset = decodedCursor.offset

// XXX what a mess
let comments
if (me) {
const filter = ` AND ("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID' OR "Item"."userId" = ${me.id}) `
const [{ item_comments_zaprank_with_me: comments }] = await models.$queryRawUnsafe(
'SELECT item_comments_zaprank_with_me($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::INTEGER, $5, $6)',
Number(id), GLOBAL_SEED, Number(me.id), COMMENT_DEPTH_LIMIT, filter, orderBy)
return comments
const filter = ` AND ("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID' OR "Item"."userId" = ${me.id}) AND "Item".created_at <= '${decodedCursor.time.toISOString()}'::TIMESTAMP(3) `
if (item.ncomments > FULL_COMMENTS_THRESHOLD) {
const [{ item_comments_zaprank_with_me_limited: limitedComments }] = await models.$queryRawUnsafe(
'SELECT item_comments_zaprank_with_me_limited($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8, $9)',
Number(item.id), GLOBAL_SEED, Number(me.id), COMMENTS_LIMIT, offset, COMMENTS_OF_COMMENT_LIMIT, COMMENT_DEPTH_LIMIT, filter, orderBy)
comments = limitedComments
} else {
const [{ item_comments_zaprank_with_me: fullComments }] = await models.$queryRawUnsafe(
'SELECT item_comments_zaprank_with_me($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::INTEGER, $5, $6)',
Number(item.id), GLOBAL_SEED, Number(me.id), COMMENT_DEPTH_LIMIT, filter, orderBy)
comments = fullComments
}
} else {
const filter = ` AND ("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = 'PAID') AND "Item".created_at <= '${decodedCursor.time.toISOString()}'::TIMESTAMP(3) `
if (item.ncomments > FULL_COMMENTS_THRESHOLD) {
const [{ item_comments_limited: limitedComments }] = await models.$queryRawUnsafe(
'SELECT item_comments_limited($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::INTEGER, $5::INTEGER, $6, $7)',
Number(item.id), COMMENTS_LIMIT, offset, COMMENTS_OF_COMMENT_LIMIT, COMMENT_DEPTH_LIMIT, filter, orderBy)
comments = limitedComments
} else {
const [{ item_comments: fullComments }] = await models.$queryRawUnsafe(
'SELECT item_comments($1::INTEGER, $2::INTEGER, $3, $4)', Number(item.id), COMMENT_DEPTH_LIMIT, filter, orderBy)
comments = fullComments
}
}

const filter = ' AND ("Item"."invoiceActionState" IS NULL OR "Item"."invoiceActionState" = \'PAID\') '
const [{ item_comments: comments }] = await models.$queryRawUnsafe(
'SELECT item_comments($1::INTEGER, $2::INTEGER, $3, $4)', Number(id), COMMENT_DEPTH_LIMIT, filter, orderBy)
return comments
return {
comments,
cursor: comments.length + offset < item.nDirectComments ? nextCursorEncoded(decodedCursor, COMMENTS_LIMIT) : null
}
}

export async function getItem (parent, { id }, { me, models }) {
Expand Down Expand Up @@ -1173,11 +1213,25 @@ export default {
}
})
},
comments: async (item, { sort }, { me, models }) => {
if (typeof item.comments !== 'undefined') return item.comments
if (item.ncomments === 0) return []
comments: async (item, { sort, cursor }, { me, models }) => {
if (typeof item.comments !== 'undefined') {
if (Array.isArray(item.comments)) {
return {
comments: item.comments,
cursor: null
}
}
return item.comments
}

if (item.ncomments === 0) {
return {
comments: [],
cursor: null
}
}

return comments(me, models, item.id, sort || defaultCommentSort(item.pinId, item.bioId, item.createdAt))
return comments(me, models, item, sort || defaultCommentSort(item.pinId, item.bioId, item.createdAt), cursor)
},
freedFreebie: async (item) => {
return item.weightedVotes - item.weightedDownVotes > 0
Expand Down
3 changes: 2 additions & 1 deletion api/typeDefs/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ export default gql`
bio: Boolean!
paidImgLink: Boolean
ncomments: Int!
comments(sort: String): [Item!]!
nDirectComments: Int!
comments(sort: String, cursor: String): Comments!
path: String
position: Int
prior: Int
Expand Down
26 changes: 22 additions & 4 deletions components/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,17 @@ export default function Comment ({
</Reply>}
{children}
<div className={styles.comments}>
{item.comments && !noComments
? item.comments.map((item) => (
<Comment depth={depth + 1} key={item.id} item={item} />
))
{item.comments.comments && !noComments
? (
<>
{item.comments.comments.map((item) => (
<Comment depth={depth + 1} key={item.id} item={item} />
))}
{item.comments.comments.length < item.nDirectComments && <ViewAllReplies id={item.id} nshown={item.comments.comments.length} nhas={item.nDirectComments} />}
</>
)
: null}
{/* TODO: add link to more comments if they're limited */}
</div>
</div>
)
Expand All @@ -267,6 +273,18 @@ export default function Comment ({
)
}

export function ViewAllReplies ({ id, nshown, nhas }) {
const text = `view all ${nhas} replies`

return (
<div className={`d-block fw-bold ${styles.comment} pb-2 ps-3`}>
<Link href={`/items/${id}`} as={`/items/${id}`} className='text-muted'>
{text}
</Link>
</div>
)
}

export function CommentSkeleton ({ skeletonChildren }) {
return (
<div className={styles.comment}>
Expand Down
17 changes: 14 additions & 3 deletions components/comments.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Fragment } from 'react'
import { Fragment, useMemo } from 'react'
import Comment, { CommentSkeleton } from './comment'
import styles from './header.module.css'
import Nav from 'react-bootstrap/Nav'
import Navbar from 'react-bootstrap/Navbar'
import { numWithUnits } from '@/lib/format'
import { defaultCommentSort } from '@/lib/item'
import { useRouter } from 'next/router'
import MoreFooter from './more-footer'
import { FULL_COMMENTS_THRESHOLD } from '@/lib/constants'

export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, commentSats }) {
const router = useRouter()
Expand Down Expand Up @@ -60,10 +62,13 @@ export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, comm
)
}

export default function Comments ({ parentId, pinned, bio, parentCreatedAt, commentSats, comments, ...props }) {
export default function Comments ({
parentId, pinned, bio, parentCreatedAt,
commentSats, comments, commentsCursor, fetchMoreComments, ncomments, ...props
}) {
const router = useRouter()

const pins = comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position)
const pins = useMemo(() => comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position), [comments])

return (
<>
Expand Down Expand Up @@ -91,6 +96,12 @@ export default function Comments ({ parentId, pinned, bio, parentCreatedAt, comm
{comments.filter(({ position }) => !position).map(item => (
<Comment depth={1} key={item.id} item={item} {...props} />
))}
{ncomments > FULL_COMMENTS_THRESHOLD &&
<MoreFooter
cursor={commentsCursor} fetchMore={fetchMoreComments} noMoreText=' '
count={comments?.length}
Skeleton={CommentsSkeleton}
/>}
</>
)
}
Expand Down
8 changes: 6 additions & 2 deletions components/item-full.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function ItemText ({ item }) {
: <Text itemId={item.id} topLevel rel={item.rel ?? UNKNOWN_LINK_REL} outlawed={item.outlawed} imgproxyUrls={item.imgproxyUrls}>{item.text}</Text>
}

export default function ItemFull ({ item, bio, rank, ...props }) {
export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props }) {
useEffect(() => {
commentsViewed(item)
}, [item.lastCommentAt])
Expand All @@ -186,7 +186,11 @@ export default function ItemFull ({ item, bio, rank, ...props }) {
<div className={styles.comments}>
<Comments
parentId={item.id} parentCreatedAt={item.createdAt}
pinned={item.position} bio={bio} commentSats={item.commentSats} comments={item.comments}
pinned={item.position} bio={bio} commentSats={item.commentSats}
ncomments={item.ncomments}
comments={item.comments.comments}
commentsCursor={item.comments.cursor}
fetchMoreComments={fetchMoreComments}
/>
</div>}
</CarouselProvider>
Expand Down
11 changes: 7 additions & 4 deletions components/reply.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export default forwardRef(function Reply ({

const placeholder = useMemo(() => {
return [
'comment for currency?',
'comment for currency',
'fractions of a penny for your thoughts?',
'put your money where your mouth is?'
'put your money where your mouth is'
][parentId % 3]
}, [parentId])

Expand All @@ -70,13 +70,16 @@ export default forwardRef(function Reply ({
cache.modify({
id: `Item:${parentId}`,
fields: {
comments (existingCommentRefs = []) {
comments (existingComments = {}) {
const newCommentRef = cache.writeFragment({
data: result,
fragment: COMMENTS,
fragmentName: 'CommentsRecursive'
})
return [newCommentRef, ...existingCommentRefs]
return {
cursor: existingComments.cursor,
comments: [newCommentRef, ...(existingComments?.comments || [])]
}
}
},
optimistic: true
Expand Down
14 changes: 10 additions & 4 deletions fragments/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const COMMENT_FIELDS = gql`
mine
otsHash
ncomments
nDirectComments
imgproxyUrls
rel
apiKey
Expand All @@ -66,6 +67,7 @@ export const COMMENTS_ITEM_EXT_FIELDS = gql`
id
title
bounty
ncomments
bountyPaidTo
subName
sub {
Expand All @@ -89,19 +91,23 @@ export const COMMENTS = gql`
fragment CommentsRecursive on Item {
...CommentFields
comments {
...CommentFields
comments {
...CommentFields
comments {
...CommentFields
comments {
...CommentFields
comments {
...CommentFields
comments {
...CommentFields
comments {
...CommentFields
comments {
...CommentFields
comments {
comments {
...CommentFields
}
}
}
}
}
}
Expand Down
11 changes: 8 additions & 3 deletions fragments/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const ITEM_FIELDS = gql`
freebie
bio
ncomments
nDirectComments
commentSats
commentCredits
lastCommentAt
Expand Down Expand Up @@ -94,6 +95,7 @@ export const ITEM_FULL_FIELDS = gql`
bountyPaidTo
subName
mine
ncomments
user {
id
name
Expand Down Expand Up @@ -166,13 +168,16 @@ export const ITEM_FULL = gql`
${ITEM_FULL_FIELDS}
${POLL_FIELDS}
${COMMENTS}
query Item($id: ID!, $sort: String) {
query Item($id: ID!, $sort: String, $cursor: String) {
item(id: $id) {
...ItemFullFields
prior
...PollFields
comments(sort: $sort) {
...CommentsRecursive
comments(sort: $sort, cursor: $cursor) {
cursor
comments {
...CommentsRecursive
}
}
}
}`
Expand Down
4 changes: 3 additions & 1 deletion fragments/paidAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ const ITEM_PAID_ACTION_FIELDS = gql`
reminderScheduledAt
...CommentFields
comments {
...CommentsRecursive
comments {
...CommentsRecursive
}
}
}
}`
Expand Down
Loading