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 (