stacker.news/lib/comments.js
soxa f0e3516cf0
Refactor live comments and comment injection (#2462)
* Fix duplicate comment on pessimistic creation

- comment creation checks for comment's ID existence in cache
- invoice.confirmedAt included in useCanEdit deps for anons live comments

* switch to some as sets are not worth it

* only check for duplicates if a pessimistic payment method has been used

* default to empty array

* add comment about side-effects

* record ownership of an item to avoid injecting it via live comments

* trigger check only if the incoming comment is ours, cleanup

* correct conditions, correct comments, light cleanup

* fix: add defensive condition to ownership recorder, better name

* refactor: unified comment injection logic with deduplication, useCommentsView hook; revert sessionStorage-based fix

* adjust live comments naming around the codebase

* listen for hmac presence for anon edits

* always return the injected comment createdAt to bump live comments

* refactor: improve live comments hook readability

- latest comment createdAt persistence helper
- preserveScroll returns the returning value of the callback
- compact conditional logic
- refresh code comments
- refresh naming
- group constants
- reorder imports

* flat comment injection, fetch flat comments instead of the entire subtree that would've been deduplicated anyway, cleanup

* always align new comment fragment to the comments query structure

* generic useCommentsView hook

* update comment counts if live injecting into fragments without comments field

* fix: pass parentId, if a comment has a top level parent it always has the comments field

* fix: update CommentsViewAt only if we actually injected a comment into cache

* correct injectComment result usage

* pass markViewedAt to further centralize side effects, remove live from Item server typedefs

* fix: don't update counts for ancestors that are already up to date, update commentsViewedAt per batch not per comment

* port: fix coalesce, useCommentsView hook and outline changes

* update hmac field in cache on paid invoice, hmac as useCanEdit effect dependency

* comments and light cleanup, update useCommentsView

* efficient hasComments logic for live comments, establish a gql fragment

* fix: typo on topLevel evaluation

* limit extra evaluations to live comments scenarios

* update comments

* support live comments ncomments increments for anon view tracking
2025-09-07 16:04:34 -05:00

88 lines
2.8 KiB
JavaScript

import { COMMENTS, HAS_COMMENTS } from '../fragments/comments'
// adds a comment to the cache, under its parent item
function cacheComment (cache, newComment, { live = false }) {
return cache.modify({
id: `Item:${newComment.parentId}`,
fields: {
comments: (existingComments = {}, { readField }) => {
// if the comment already exists, return
if (existingComments?.comments?.some(c => readField('id', c) === newComment.id)) return existingComments
// we need to make sure we're writing a fragment that matches the comments query (comments and count fields)
const newCommentRef = cache.writeFragment({
data: {
comments: {
comments: []
},
ncomments: 0,
nDirectComments: 0,
...newComment,
live
},
fragment: COMMENTS,
fragmentName: 'CommentsRecursive'
})
return {
cursor: existingComments.cursor,
comments: [newCommentRef, ...(existingComments?.comments || [])]
}
}
},
optimistic: true
})
}
// handles cache injection and side-effects for both live and non-live comments
export function injectComment (cache, newComment, { live = false, rootId } = {}) {
// if live and a reply (not top level), check if the parent has comments
const hasComments = live && !(Number(rootId) === Number(newComment.parentId))
? !!(cache.readFragment({
id: `Item:${newComment.parentId}`,
fragment: HAS_COMMENTS
}))
// if not live, we can assume the parent has the comments field since user replied to it
: true
const updated = hasComments && cacheComment(cache, newComment, { live })
// run side effects if injection succeeded or if injecting live comment into SSR item without comments field
if (updated || (live && !hasComments)) {
// update all ancestors comment count, excluding the comment itself
const ancestors = newComment.path.split('.').slice(0, -1)
updateAncestorsCommentCount(cache, ancestors)
return true
}
return false
}
// updates the ncomments and nDirectComments fields of all ancestors of an item/comment in the cache
function updateAncestorsCommentCount (cache, ancestors, { ncomments = 1, nDirectComments = 1 } = {}) {
// update nDirectComments of immediate parent
cache.modify({
id: `Item:${ancestors[ancestors.length - 1]}`,
fields: {
nDirectComments (existingNDirectComments = 0) {
return existingNDirectComments + nDirectComments
}
},
optimistic: true
})
// update ncomments of all ancestors
ancestors.forEach(id => {
cache.modify({
id: `Item:${id}`,
fields: {
ncomments (existingNComments = 0) {
return existingNComments + ncomments
}
},
optimistic: true
})
})
}