From 0e842e99159823ca68f32842a9999dc29fb404c1 Mon Sep 17 00:00:00 2001 From: soxa <6390896+Soxasora@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:04:54 +0200 Subject: [PATCH] live comments: auto show new comments (#2355) * enhance: FaviconProvider, keep track of new comment IDs to change favicon, remove new comment IDs per outline removal * don't track oneself comments * enhance: auto-show new comments, idempotency by ignoring already injected comments, preserveScroll utility * fadeIn animation on comment injection; cleanup: remove unused counts and thread handling; non-critical fix: always give rootLastCommentAt a value * reliably preserve scroll position by tracking a reference found at the center of the viewport; cleanup: add more comments, add cleanup function * mitigate fractional scrolling subtle layout shifts by rounding the new reference element position * enhanced outlining system, favicon context keeps track of new comments presence - de-outlining now happens only for outlined comments - enhanced outlining: add outline only if isNewComment - de-outlining will remove the new comments favicon - on unmount remove the new comments favicon * remove the new comments favicon on new comments injection * track only deduplicated new comments * fix typo * clearer unsetOutline conditions, fix typo in live comments hook * backport: remove the injectedComment class from injected comments after animation ends * set the new comments favicon on any new outlined comment * enhance: directly inject new comments; cleanup: dismantle ShowNewComments, remove newComments field * tweaks: slower injection animation, clear favicon on Comment section unmount * change nDirectComments bug strategy to avoiding updates on comment edit * cleanup: better naming, re-instate injected comments outline * injection: major cache utilities refactor, don't preserve scroll if no comments have been injected - don't preserve scroll if after deduplication we don't inject any comments - use manual read/write cache updates to control the flow -- allows to check if we are really injecting or not - reduce polling to 5 seconds instead of 10 - light cleanup -- removed update cache functions -- added 'injected' to typeDefs (gql consistency) * cleanup: detailed comments, refactor, remove clutter Refactor: + clearer variables + depth calculation utility function + use destructured Apollo cache + extract item object from item query + skip ignored comment instead of ending the loop CSS: + from-to fadeIn animation keyframes - floatingComments unused class Favicon: + provider exported by default * fix wrong merge * split: remove favicon context * split: remove favicon pngs * regression: revert to updateQuery for multiple comment fragments handling * reverse multiple reads for deduplication on comment injection * fix regression on apollo manipulations via fn; cleanup: remove wrong deps from outlining --- api/typeDefs/item.js | 2 +- components/comment.js | 62 +++++----- components/comment.module.css | 19 +-- components/comments.js | 11 +- components/nav/common.js | 4 - components/preserve-scroll.js | 53 +++++++++ components/show-new-comments.js | 204 -------------------------------- components/use-live-comments.js | 90 ++++++++++---- fragments/comments.js | 4 - fragments/items.js | 1 - lib/apollo.js | 6 +- lib/comments.js | 38 +++--- 12 files changed, 177 insertions(+), 317 deletions(-) create mode 100644 components/preserve-scroll.js delete mode 100644 components/show-new-comments.js diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 4d19c744..9fde3fa3 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -149,7 +149,7 @@ export default gql` ncomments: Int! nDirectComments: Int! comments(sort: String, cursor: String): Comments! - newComments(rootId: ID, after: Date): Comments! + injected: Boolean! path: String position: Int prior: Int diff --git a/components/comment.js b/components/comment.js index 4002d330..a060dd0d 100644 --- a/components/comment.js +++ b/components/comment.js @@ -28,7 +28,6 @@ import LinkToContext from './link-to-context' import Boost from './boost-button' import { gql, useApolloClient } from '@apollo/client' import classNames from 'classnames' -import { ShowNewComments } from './show-new-comments' function Parent ({ item, rootText }) { const root = useRoot() @@ -115,6 +114,17 @@ export default function Comment ({ const { cache } = useApolloClient() + const unsetOutline = () => { + if (!ref.current) return + const hasOutline = ref.current.classList.contains('outline-new-comment') || ref.current.classList.contains('outline-new-injected-comment') + const hasOutlineUnset = ref.current.classList.contains('outline-new-comment-unset') + + // don't try to unset the outline if the comment is not outlined or we already unset the outline + if (hasOutline && !hasOutlineUnset) { + ref.current.classList.add('outline-new-comment-unset') + } + } + useEffect(() => { const comment = cache.readFragment({ id: `Item:${router.query.commentId}`, @@ -142,17 +152,26 @@ export default function Comment ({ useEffect(() => { if (me?.id === item.user?.id) return - const itemCreatedAt = new Date(item.createdAt).getTime() - if (router.query.commentsViewedAt && - !item.injected && - itemCreatedAt > router.query.commentsViewedAt) { - ref.current.classList.add('outline-new-comment') - // newly injected comments have to use a different class to outline every new comment - } else if (rootLastCommentAt && - item.injected && - itemCreatedAt > new Date(rootLastCommentAt).getTime()) { + const itemCreatedAt = new Date(item.createdAt).getTime() + // it's a new comment if it was created after the last comment was viewed + // or, in the case of live comments, after the last comment was created + const isNewComment = (router.query.commentsViewedAt && itemCreatedAt > router.query.commentsViewedAt) || + (rootLastCommentAt && itemCreatedAt > new Date(rootLastCommentAt).getTime()) + if (!isNewComment) return + + if (item.injected) { + // newly injected comments (item.injected) have to use a different class to outline every new comment ref.current.classList.add('outline-new-injected-comment') + + // wait for the injection animation to end before removing its class + ref.current.addEventListener('animationend', () => { + ref.current.classList.remove(styles.injectedComment) + }, { once: true }) + // animate the live comment injection + ref.current.classList.add(styles.injectedComment) + } else { + ref.current.classList.add('outline-new-comment') } }, [item.id, rootLastCommentAt]) @@ -168,8 +187,8 @@ export default function Comment ({ return (
ref.current.classList.add('outline-new-comment-unset')} - onTouchStart={() => ref.current.classList.add('outline-new-comment-unset')} + onMouseEnter={unsetOutline} + onTouchStart={unsetOutline} >
{item.outlawed && !me?.privates?.wildWestMode @@ -269,9 +288,6 @@ export default function Comment ({ : !noReply && {root.bounty && !bountyPaid && } -
- -
} {children}
@@ -300,7 +316,6 @@ export default function Comment ({ export function ViewMoreReplies ({ item, navigateRoot = false }) { const root = useRoot() - const { cache } = useApolloClient() const id = navigateRoot ? commentSubTreeRootId(item, root) : item.id const href = `/items/${id}` + (navigateRoot ? '' : `?commentId=${item.id}`) @@ -314,23 +329,8 @@ export function ViewMoreReplies ({ item, navigateRoot = false }) { href={href} as={`/items/${id}`} className='fw-bold d-flex align-items-center gap-2 text-muted' - onClick={() => { - if (!item.newComments?.length) return - // clear new comments going to another page - cache.writeFragment({ - id: `Item:${item.id}`, - fragment: gql` - fragment NewComments on Item { - newComments - }`, - data: { - newComments: [] - } - }) - }} > {text} - {item.newComments?.length > 0 &&
} ) } diff --git a/components/comment.module.css b/components/comment.module.css index 4d4e3114..6fe49a1f 100644 --- a/components/comment.module.css +++ b/components/comment.module.css @@ -160,20 +160,11 @@ } } -.floatingComments { - position: fixed; - top: 72px; - left: 50%; - transform: translateX(-50%); - z-index: 1050; - animation: slideDown 0.3s ease-out; +.injectedComment { + animation: fadeIn 0.5s ease-out; } -@keyframes slideDown { - 0% { - transform: translateX(-50%) translateY(-100px); - } - 100% { - transform: translateX(-50%) translateY(0); - } +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } } diff --git a/components/comments.js b/components/comments.js index 6e79a1c1..19afbaff 100644 --- a/components/comments.js +++ b/components/comments.js @@ -9,7 +9,6 @@ import { useRouter } from 'next/router' import MoreFooter from './more-footer' import { FULL_COMMENTS_THRESHOLD } from '@/lib/constants' import useLiveComments from './use-live-comments' -import { ShowNewComments } from './show-new-comments' export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, commentSats }) { const router = useRouter() @@ -66,17 +65,17 @@ export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, comm export default function Comments ({ parentId, pinned, bio, parentCreatedAt, - commentSats, comments, commentsCursor, fetchMoreComments, ncomments, newComments, lastCommentAt, item, ...props + commentSats, comments, commentsCursor, fetchMoreComments, ncomments, lastCommentAt, item, ...props }) { const router = useRouter() - // fetch new comments that arrived after the lastCommentAt, and update the item.newComments field in cache + + // fetch new comments that arrived after the lastCommentAt, and update the item.comments field in cache useLiveComments(parentId, lastCommentAt || parentCreatedAt, router.query.sort) const pins = useMemo(() => comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position), [comments]) return ( <> - {comments?.length > 0 ? ( - + ))} {comments.filter(({ position }) => !position).map(item => ( - + ))} {ncomments > FULL_COMMENTS_THRESHOLD && - - - diff --git a/components/preserve-scroll.js b/components/preserve-scroll.js new file mode 100644 index 00000000..6a4ab093 --- /dev/null +++ b/components/preserve-scroll.js @@ -0,0 +1,53 @@ +export default function preserveScroll (callback) { + // preserve the actual scroll position + const scrollTop = window.scrollY + + // if the scroll position is at the top, we don't need to preserve it, just call the callback + if (scrollTop <= 0) { + callback() + return + } + + // get a reference element at the center of the viewport to track if content is added above it + const ref = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2) + const refTop = ref ? ref.getBoundingClientRect().top + scrollTop : scrollTop + + // observe the document for changes in height + const observer = new window.MutationObserver(() => { + // request animation frame to ensure the DOM is updated + window.requestAnimationFrame(() => { + // we can't proceed if we couldn't find a traceable reference element + if (!ref) { + cleanup() + return + } + + // get the new position of the reference element along with the new scroll position + const newRefTop = ref ? ref.getBoundingClientRect().top + window.scrollY : window.scrollY + // has the reference element moved? + const refMoved = newRefTop - refTop + + // if the reference element moved, we need to scroll to the new position + if (refMoved > 0) { + window.scrollTo({ + // some browsers don't respond well to fractional scroll position, so we round up the new position to the nearest integer + top: scrollTop + Math.ceil(refMoved), + behavior: 'instant' + }) + } + + cleanup() + }) + }) + + const timeout = setTimeout(() => cleanup(), 1000) // fallback + + function cleanup () { + clearTimeout(timeout) + observer.disconnect() + } + + observer.observe(document.body, { childList: true, subtree: true }) + + callback() +} diff --git a/components/show-new-comments.js b/components/show-new-comments.js deleted file mode 100644 index cc28686e..00000000 --- a/components/show-new-comments.js +++ /dev/null @@ -1,204 +0,0 @@ -import { useCallback, useRef } from 'react' -import { useApolloClient } from '@apollo/client' -import styles from './comment.module.css' -import { COMMENT_DEPTH_LIMIT } from '../lib/constants' -import { commentsViewedAfterComment } from '../lib/new-comments' -import classNames from 'classnames' -import useVisibility from './use-visibility' -import { - itemUpdateQuery, - commentUpdateFragment, - getLatestCommentCreatedAt, - updateAncestorsCommentCount, - readCommentsFragment -} from '../lib/comments' - -// filters out new comments, by id, that already exist in the item's comments -// preventing duplicate comments from being injected -function dedupeNewComments (newComments, comments = []) { - const existingIds = new Set(comments.map(c => c.id)) - return newComments.filter(id => !existingIds.has(id)) -} - -// of an array of new comments, count each new comment + all their existing comments -function countNComments (newComments) { - let totalNComments = newComments.length - for (const comment of newComments) { - totalNComments += comment.ncomments || 0 - } - return totalNComments -} - -// prepares and creates a new comments fragment for injection into the cache -// returns a function that can be used to update an item's comments field -function prepareComments (data, client, newComments) { - const totalNComments = countNComments(newComments) - - const itemHierarchy = data.path.split('.') - const ancestors = itemHierarchy.slice(0, -1) - const rootId = itemHierarchy[0] - - // update all ancestors, but not the item itself - updateAncestorsCommentCount(client.cache, ancestors, totalNComments) - - // update commentsViewedAt with the most recent fresh new comment - // quirk: this is not the most recent comment, it's the most recent comment in the newComments array - // as such, the next visit will not outline other new comments that are older than this one. - const latestCommentCreatedAt = getLatestCommentCreatedAt(newComments, data.createdAt) - commentsViewedAfterComment(rootId, latestCommentCreatedAt, totalNComments) - - // an item can either have a comments.comments field, or not - const payload = data.comments - ? { - ...data, - ncomments: data.ncomments + totalNComments, - newComments: [], - comments: { - ...data.comments, - comments: newComments.concat(data.comments.comments) - } - } - // when the fragment doesn't have a comments field, we just update stats fields - : { - ...data, - ncomments: data.ncomments + totalNComments, - newComments: [] - } - - return payload -} - -// traverses all new comments and their children -// if it's a thread, or we're in a new comment subtree, we also consider their existing children -function traverseNewComments (client, item, onLevel, currentDepth, inSubtree) { - // if we're at the depth limit, stop traversing, we've reached the bottom of the visible thread - if (currentDepth >= COMMENT_DEPTH_LIMIT) return - - // if the current item shows less comments than its nDirectComments, it's paginated - // we don't want to count/inject new comments in paginated items, as they shouldn't be visible - if (item.comments?.comments?.length < item.nDirectComments) return - - if (item.newComments && item.newComments.length > 0) { - const dedupedNewComments = dedupeNewComments(item.newComments, item.comments?.comments) - - // being newComments an array of comment ids, we can get their latest version from the cache - // ensuring that we don't miss any new comments - const freshNewComments = dedupedNewComments.map(id => { - // mark all new comments as injected, so we can outline them - return { ...readCommentsFragment(client, id), injected: true } - }).filter(Boolean) - - // at each level, we can execute a callback passing the current item's new comments, depth and ID - onLevel(freshNewComments, currentDepth, item.id) - - // traverse the new comment's new comments and their children - for (const newComment of freshNewComments) { - traverseNewComments(client, newComment, onLevel, currentDepth + 1, true) - } - } - - // check for new comments in existing children - // only if we're in a new comment subtree, or it's a thread - if (inSubtree && item.comments?.comments) { - for (const child of item.comments.comments) { - traverseNewComments(client, child, onLevel, currentDepth + 1, true) - } - } -} - -// recursively processes and displays all new comments -// handles comment injection at each level, respecting depth limits -function injectNewComments (client, item, sort, currentDepth, thread) { - traverseNewComments(client, item, (newComments, depth, itemId) => { - if (newComments.length > 0) { - // traverseNewComments also passes the depth of the current item - // used to determine if in an array of new comments, we are injecting topLevels (depth 0) or not - if (depth === 0) { - itemUpdateQuery(client, itemId, sort, (data) => prepareComments(data, client, newComments)) - } else { - commentUpdateFragment(client, itemId, (data) => prepareComments(data, client, newComments)) - } - } - }, currentDepth, thread) -} - -// counts all new comments of an item -function countAllNewComments (client, item, currentDepth, thread) { - let newCommentsCount = 0 - let threadChildren = false - - // count by traversing the comment structure - traverseNewComments(client, item, (newComments, depth) => { - newCommentsCount += countNComments(newComments) - - // if we reached a depth greater than 1, the thread's children have new comments - if (depth > 1 && newComments.length > 0) { - threadChildren = true - } - }, currentDepth, thread) - - return { newCommentsCount, threadChildren } -} - -function FloatingComments ({ buttonRef, showNewComments, text }) { - // show the floating comments button only when we're past the main top level button - const isButtonVisible = useVisibility(buttonRef, { pastElement: true }) - - if (isButtonVisible) return null - - return ( - { - // show new comments as we scroll up - showNewComments() - buttonRef.current?.scrollIntoView({ behavior: 'smooth' }) - }} - > - {text} - - ) -} - -// ShowNewComments is a component that dedupes, refreshes and injects newComments into the comments field -export function ShowNewComments ({ topLevel, item, sort, depth = 0 }) { - const client = useApolloClient() - const ref = useRef(null) - - // a thread comment is a comment at depth 1 (parent) - const thread = depth === 1 - - // recurse through all new comments and their children - // if the item is a thread, we also consider all of their existing children - const { newCommentsCount, threadChildren } = countAllNewComments(client, item, depth, thread) - - // only if the item is a thread and its children have new comments, we show "show all new comments" - const threadComment = thread && threadChildren - - const showNewComments = useCallback(() => { - // a top level comment doesn't pass depth, we pass its default value of 0 to signify this - // child comments are injected from the depth they're at - injectNewComments(client, item, sort, depth, threadComment) - }, [client, sort, item, depth]) - - const text = !threadComment - ? `${newCommentsCount} new comment${newCommentsCount > 1 ? 's' : ''}` - : 'show all new comments' - - return ( - <> - 0 ? 'visible' : 'hidden' }} - > - {text} -
- - {topLevel && newCommentsCount > 0 && ( - - )} - - ) -} diff --git a/components/use-live-comments.js b/components/use-live-comments.js index 0f401312..8016279b 100644 --- a/components/use-live-comments.js +++ b/components/use-live-comments.js @@ -1,47 +1,84 @@ -import { useQuery, useApolloClient } from '@apollo/client' -import { SSR } from '../lib/constants' +import preserveScroll from './preserve-scroll' import { GET_NEW_COMMENTS } from '../fragments/comments' import { useEffect, useState } from 'react' -import { itemUpdateQuery, commentUpdateFragment, getLatestCommentCreatedAt } from '../lib/comments' +import { SSR, COMMENT_DEPTH_LIMIT } from '../lib/constants' +import { useQuery, useApolloClient } from '@apollo/client' +import { commentsViewedAfterComment } from '../lib/new-comments' +import { + updateItemQuery, + updateCommentFragment, + getLatestCommentCreatedAt, + updateAncestorsCommentCount, + calculateDepth +} from '../lib/comments' -const POLL_INTERVAL = 1000 * 10 // 10 seconds +const POLL_INTERVAL = 1000 * 5 // 5 seconds -// merge new comment into item's newComments -// and prevent duplicates by checking if the comment is already in item's newComments or existing comments -function mergeNewComment (item, newComment) { - const existingNewComments = item.newComments || [] +// prepares and creates a fragment for injection into the cache +// also handles side effects like updating comment counts and viewedAt timestamps +function prepareComments (item, cache, newComment) { const existingComments = item.comments?.comments || [] - // is the incoming new comment already in item's new comments or existing comments? - if (existingNewComments.includes(newComment.id) || existingComments.some(comment => comment.id === newComment.id)) { - return item - } + // is the incoming new comment already in item's existing comments? + // if so, we don't need to update the cache + if (existingComments.some(comment => comment.id === newComment.id)) return item - return { ...item, newComments: [...existingNewComments, newComment.id] } + // count the new comment (+1) and its children (+ncomments) + const totalNComments = newComment.ncomments + 1 + + const itemHierarchy = item.path.split('.') + // update all ancestors comment count, but not the item itself + const ancestors = itemHierarchy.slice(0, -1) + updateAncestorsCommentCount(cache, ancestors, totalNComments) + // update commentsViewedAt to now, and add the number of new comments + const rootId = itemHierarchy[0] + commentsViewedAfterComment(rootId, Date.now(), totalNComments) + + // add a flag to the new comment to indicate it was injected + const injectedComment = { ...newComment, injected: true } + + // an item can either have a comments.comments field, or not + const payload = item.comments + ? { + ...item, + ncomments: item.ncomments + totalNComments, + comments: { + ...item.comments, + comments: [injectedComment, ...item.comments.comments] + } + } + // when the fragment doesn't have a comments field, we just update stats fields + : { + ...item, + ncomments: item.ncomments + totalNComments + } + + return payload } -function cacheNewComments (client, rootId, newComments, sort) { +function cacheNewComments (cache, rootId, newComments, sort) { for (const newComment of newComments) { const { parentId } = newComment const topLevel = Number(parentId) === Number(rootId) - // if the comment is a top level comment, update the item + // if the comment is a top level comment, update the item, else update the parent comment if (topLevel) { - // merge the new comment into the item's newComments field, checking for duplicates - itemUpdateQuery(client, rootId, sort, (data) => mergeNewComment(data, newComment)) + updateItemQuery(cache, rootId, sort, (item) => prepareComments(item, cache, newComment)) } else { - // if the comment is a reply, update the parent comment - // merge the new comment into the parent comment's newComments field, checking for duplicates - commentUpdateFragment(client, parentId, (data) => mergeNewComment(data, newComment)) + // if the comment is too deep, we can skip it + const depth = calculateDepth(newComment.path, rootId, parentId) + if (depth > COMMENT_DEPTH_LIMIT) continue + // inject the new comment into the parent comment's comments field + updateCommentFragment(cache, parentId, (parent) => prepareComments(parent, cache, newComment)) } } } -// useLiveComments fetches new comments under an item (rootId), that arrives after the latest comment createdAt -// and inserts them into the newComment client field of their parent comment/post. +// useLiveComments fetches new comments under an item (rootId), +// that are newer than the latest comment createdAt (after), and injects them into the cache. export default function useLiveComments (rootId, after, sort) { const latestKey = `liveCommentsLatest:${rootId}` - const client = useApolloClient() + const { cache } = useApolloClient() const [latest, setLatest] = useState(after) const [initialized, setInitialized] = useState(false) @@ -72,8 +109,9 @@ export default function useLiveComments (rootId, after, sort) { useEffect(() => { if (!data?.newComments?.comments?.length) return - // merge and cache new comments in their parent comment/post - cacheNewComments(client, rootId, data.newComments.comments, sort) + // directly inject new comments into the cache, preserving scroll position + // quirk: scroll is preserved even if we are not injecting new comments due to dedupe + preserveScroll(() => cacheNewComments(cache, rootId, data.newComments.comments, sort)) // update latest timestamp to the latest comment created at // save it to session storage, to persist between client-side navigations @@ -82,5 +120,5 @@ export default function useLiveComments (rootId, after, sort) { if (typeof window !== 'undefined') { window.sessionStorage.setItem(latestKey, newLatest) } - }, [data, client, rootId, sort, latest]) + }, [data, cache, rootId, sort, latest]) } diff --git a/fragments/comments.js b/fragments/comments.js index 3cbc7cc9..f7e88b5a 100644 --- a/fragments/comments.js +++ b/fragments/comments.js @@ -47,7 +47,6 @@ export const COMMENT_FIELDS = gql` otsHash ncomments nDirectComments - newComments @client injected @client imgproxyUrls rel @@ -176,7 +175,6 @@ export const COMMENT_WITH_NEW_RECURSIVE = gql` ...CommentsRecursive } } - newComments @client } ` @@ -190,7 +188,6 @@ export const COMMENT_WITH_NEW_LIMITED = gql` ...CommentFields } } - newComments @client } ` @@ -199,7 +196,6 @@ export const COMMENT_WITH_NEW_MINIMAL = gql` fragment CommentWithNewMinimal on Item { ...CommentFields - newComments @client } ` diff --git a/fragments/items.js b/fragments/items.js index 95eed042..151587a2 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -59,7 +59,6 @@ export const ITEM_FIELDS = gql` bio ncomments nDirectComments - newComments @client commentSats commentCredits lastCommentAt diff --git a/lib/apollo.js b/lib/apollo.js index 14d06f5e..f1887015 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -4,6 +4,7 @@ import { decodeCursor, LIMIT } from './cursor' import { COMMENTS_LIMIT, SSR } from './constants' import { RetryLink } from '@apollo/client/link/retry' import { isMutationOperation, isQueryOperation } from '@apollo/client/utilities' + function isFirstPage (cursor, existingThings, limit = LIMIT) { if (cursor) { const decursor = decodeCursor(cursor) @@ -323,11 +324,6 @@ function getClient (uri) { } } }, - newComments: { - read (newComments) { - return newComments || [] - } - }, injected: { read (injected) { return injected || false diff --git a/lib/comments.js b/lib/comments.js index a892cc83..dbe3e9ec 100644 --- a/lib/comments.js +++ b/lib/comments.js @@ -17,11 +17,10 @@ export function updateAncestorsCommentCount (cache, ancestors, increment) { }) } -// live comments - cache manipulations // updates the item query in the cache -// this is used by live comments to update a top level item's newComments field -export function itemUpdateQuery (client, id, sort, fn) { - client.cache.updateQuery({ +// this is used by live comments to update a top level item's comments field +export function updateItemQuery (cache, id, sort, fn) { + cache.updateQuery({ query: ITEM_FULL, // updateQuery needs the correct variables to update the correct item // the Item query might have the router.query.sort in the variables, so we need to pass it in if it exists @@ -33,8 +32,8 @@ export function itemUpdateQuery (client, id, sort, fn) { } // updates a comment fragment in the cache, with fallbacks for comments lacking CommentsRecursive or Comments altogether -export function commentUpdateFragment (client, id, fn) { - let result = client.cache.updateFragment({ +export function updateCommentFragment (cache, id, fn) { + let result = cache.updateFragment({ id: `Item:${id}`, fragment: COMMENT_WITH_NEW_RECURSIVE, fragmentName: 'CommentWithNewRecursive' @@ -46,7 +45,7 @@ export function commentUpdateFragment (client, id, fn) { // sometimes comments can start to reach their depth limit, and lack adherence to the CommentsRecursive fragment // for this reason, we update the fragment with a limited version that only includes the CommentFields fragment if (!result) { - result = client.cache.updateFragment({ + result = cache.updateFragment({ id: `Item:${id}`, fragment: COMMENT_WITH_NEW_LIMITED, fragmentName: 'CommentWithNewLimited' @@ -58,7 +57,7 @@ export function commentUpdateFragment (client, id, fn) { // at the deepest level, the comment can't have any children, here we update only the newComments field. if (!result) { - result = client.cache.updateFragment({ + result = cache.updateFragment({ id: `Item:${id}`, fragment: COMMENT_WITH_NEW_MINIMAL, fragmentName: 'CommentWithNewMinimal' @@ -71,19 +70,16 @@ export function commentUpdateFragment (client, id, fn) { return result } -// reads a nested comments fragment from the cache -// this is used to read a comment and its children comments -// it has a fallback for comments nearing the depth limit, that lack the CommentsRecursive fragment -export function readCommentsFragment (client, id) { - return client.cache.readFragment({ - id: `Item:${id}`, - fragment: COMMENT_WITH_NEW_RECURSIVE, - fragmentName: 'CommentWithNewRecursive' - }) || client.cache.readFragment({ - id: `Item:${id}`, - fragment: COMMENT_WITH_NEW_LIMITED, - fragmentName: 'CommentWithNewLimited' - }) +export function calculateDepth (path, rootId, parentId) { + // calculate depth by counting path segments from root to parent + const pathSegments = path.split('.') + const rootIndex = pathSegments.indexOf(rootId.toString()) + const parentIndex = pathSegments.indexOf(parentId.toString()) + + // depth is the distance from root to parent in the path + const depth = parentIndex - rootIndex + + return depth } // finds the most recent createdAt timestamp from an array of comments