Skip to content

Live updates to comment threads #2115

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

Merged
merged 58 commits into from
Jul 21, 2025
Merged

Conversation

Soxasora
Copy link
Member

@Soxasora Soxasora commented Apr 18, 2025

Description

Closes #791
It features a useLiveComments hook that incrementally fetches comments newer than the last commentAt timestamp, performing cache updates to the newComments field of the parent Item/Comment.

New comments can be showed with a "X new comments" button that will retrieve fresh copies of the newComments from the cache and inject them in the parent's comment field.

Media

Desktop

Screen.Recording.2025-07-15.at.13.23.33.mp4

Mobile

Untitled.mp4

Additional Context

Live comments features:

  • 10 seconds polling
  • Orphan comments queuing
  • Top-level and nested replies dedicated flows
  • Duplicate prevention

Known bugs:

  • comments without item.comments are not subscribed to newComments

Checklist

Are your changes backwards compatible? Please answer below:
Yes

On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:
6, strong QA but it might benefit from human comment testing

For frontend changes: Tested on mobile, light and dark mode? Please answer below:
Yes

Did you introduce any new environment variables? If so, call them out explicitly here:
n/a

Soxasora and others added 21 commits April 18, 2025 05:34
…fix leak on useEffect because of missing sort

atomic apollo cache manipulations; manage top sort not being present in item query cache
queue nested comments without a parent, retry on the next poll
fix commit messages
cursor[bot]

This comment was marked as outdated.

Copy link
Member

@huumn huumn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't fault it. Nice work!

I guess all it needs is removing the layout shift.

Oh, and did you get it to handle limited comment threads?

@Soxasora
Copy link
Member Author

I can't fault it. Nice work!

Thanks! Iterative QA made the development slower than it needed to be, but I think that the result is much more sophisticated than what it would've been.

I guess all it needs is removing the layout shift.

This, and deepest comments support:

Oh, and did you get it to handle limited comment threads?

Kind of. The fallback mechanism on commentUpdateFragment/readCommentFragment (wow ok maybe the naming is all over the place) actually fixes this, by providing a fallback fragment, but not for the deepest comment, which lacks the comments field altogether.

Not a problem if you're loading the deepest levels yourself, since they're injected with the comment structure we need, but it is a problem if they're already there.

I didn't mess with item_comments_zaprank_with_me/_limited because it would inevitably impact the already established query performance and behavior, so I think I'll rather use another fallback fragment for this, albeit kinda ugly.

fixes:
- client-side navigation re-fetched all new comments because 'after' was cached, now the latest new comment time persists in sessionStorage

enhancements:
- use CommentWithNewMinimal fragment fallback for comments at the deepest level
- tweak ReplyOnAnotherPage to show also how many direct new comments are there

cleanup:
- queue management is not needed anymore, therefore it has been removed
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Comment on lines +67 to +73
// update latest timestamp to the latest comment created at
// save it to session storage, to persist between client-side navigations
const newLatest = getLatestCommentCreatedAt(data.newComments.comments, latest)
setLatest(newLatest)
if (typeof window !== 'undefined') {
window.sessionStorage.setItem(latestKey, newLatest)
}
Copy link
Member Author

@Soxasora Soxasora Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client-side navigations would make the hook refetch (from cache) from the first timestamp it received.
Other than being a performance waste, it tried to inject new comments on comments that already have them, triggering deduplication every time.

Riding this bug, deduplication doesn't even work on comments at the deepest level as they lack the comments field, so it would perpetually inject new comments, even if they're there.

Now the latest new comment timestamp persists via sessionStorage, win win

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option might be to use the logic we already have in lib/new-comments.js which handles the new comment outlining, and could allow you to use this value to solve outlining issues. I don't know though.

Copy link
Member Author

@Soxasora Soxasora Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is, outlining works somewhat correctly if you visit the post with client-side navigation, but if you visit it by its url, it fails because there isn't commentsViewedAt in the query params. I think I can work around this and make it work without the local injected field!

Here I update commentsViewedAt so that on the next visit, you don't get new comments you may have injected as unread:

    const latestCommentCreatedAt = getLatestCommentCreatedAt(freshNewComments, data.createdAt)
    const rootId = data.path.split('.')[0]
    commentsViewedAfterComment(rootId, latestCommentCreatedAt)

But, wait, I just discovered a bug: I'm calling commentsViewedAfterComment after injecting a batch of comments, ignoring that the function was made only for single replies, since it updates commentsViewedNum by 1

export function commentsViewedAfterComment (rootId, createdAt) {
window.localStorage.setItem(`${COMMENTS_VIEW_PREFIX}:${rootId}`, new Date(createdAt).getTime())
const existingRootComments = window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${rootId}`) || 0
window.localStorage.setItem(`${COMMENTS_NUM_PREFIX}:${rootId}`, existingRootComments + 1)
}

Pushing a patch to also add totalNComments.


Sadly I can't use commentsViewedAt for live comments, as it gets updated only when new comments are showed (new comments that have not been shown will be outlined this way on next visit).

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@Soxasora
Copy link
Member Author

Soxasora commented Jul 18, 2025

This causes existingRootComments + ncomments to perform string concatenation (e.g., "5" + 1 = "51") instead of numeric addition, resulting in incorrect comment counts.

oh well, that was actually happening before too, maybe we never reached a point in which viewNum would be used.

    if (viewedAt && viewNum) {
      return viewedAt < new Date(item.lastCommentAt).getTime() || viewNum < item.ncomments
    }

edit: just realized that if you visit a post for the first time and comment in dev, the new comment dot appears even if it's yours. Hmm

edit2: Ah, it's based off item.lastCommentAt which may be null in dev.

useEffect(() => {
commentsViewed(item)
}, [item.lastCommentAt])

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: CSS Redundancy Causes Maintenance Issues

Duplicate CSS definitions for .newCommentDot and @keyframes pulse have been added to components/header.module.css. These styles are already defined and used in components/comment.module.css, making the additions to header.module.css redundant and unused. This duplication can lead to maintenance issues and potential style inconsistencies.

components/header.module.css#L114-L141

https://github.com/stackernews/stacker.news/blob/b1b49d737ecf9a080f146f8efa4cfa7f3eb10d9c/components/header.module.css#L114-L141

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

Soxasora added 3 commits July 18, 2025 21:39
…ction logic

- top-level and nested comment handling share the same recursion logic
- ShowNewComments references the item object for every type of comments
— note: item from item-full.js is passed to comments.js
- depth now starts at 0 to support top level comments
- injection and counting now reach the deepest level, updating also the deepest comment
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Comment Access Fails Without Null Check

The prepareComments function attempts to access data.comments.comments without null-checking data.comments. For comments at the deepest levels, which may not include a comments field in their data structure, this results in a runtime error. The access should use optional chaining with a fallback, e.g., data.comments?.comments || [].

components/show-new-comments.js#L45-L46

...data,
comments: { ...data.comments, comments: [...newComments, ...data.comments.comments] },

Fix in CursorFix in Web


Bug: Depth Logic and Ancestor Counting Bugs

The injectNewComments function has two issues:

  1. Incorrect Depth Logic: The if (depth === 0) condition within injectNewComments incorrectly determines if an item is top-level. The depth parameter passed to the traverseNewComments callback represents the current item's depth, not whether the injection itself is for a top-level item. Consequently, itemUpdateQuery is never called for top-level items, and commentUpdateFragment is incorrectly used for all comment injections.
  2. Double-Counting Ancestor Comments: Clicking multiple "show new comments" buttons at different nesting levels causes ancestor comment counts (ncomments) to be double-counted. This occurs because updateAncestorsCommentCount is repeatedly called for the same ancestors when the same new comments are processed multiple times.

components/show-new-comments.js#L77-L93

// recursively processes and displays all new comments and its children
// handles comment injection at each level, respecting depth limits
function injectNewComments (client, item, currentDepth, sort) {
traverseNewComments(client, item, (newComments, item, depth) => {
if (newComments.length > 0) {
const payload = prepareComments({ client, newComments })
// used to determine if by iterating through the new comments
// we are injecting topLevels (depth 0) or not
if (depth === 0) {
itemUpdateQuery(client, item.id, sort, payload)
} else {
commentUpdateFragment(client, item.id, payload)
}
}
}, currentDepth)
}

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

@huumn
Copy link
Member

huumn commented Jul 20, 2025

Top level comments are messed up on mobile.

Screenshot 2025-07-20 at 3 22 32 PM

I probably wasn't explicit enough, but this is why I thought we had no choice but to do fixed positioning.

Another solution to this kind of thing is to reserve empty space for the show new comments component, originally rendering it as .invisible.

@Soxasora
Copy link
Member Author

top level comments are messed up on mobile

Ah! Sorry about it, that wasn't supposed to happen.

We can make space for it in the CommentsHeader but I knew this positioning was anyway rushed, overall not so good. I think we can reserve empty space like you mentioned and avoid fixed positioning!

I'll look into this right now.

@Soxasora
Copy link
Member Author

Soxasora commented Jul 21, 2025

I tried some positions yesterday

A smaller padding, feels overwhelming, looks a bit ugly
IMG_1604

Under CommentsHeader, too much space, still a bit ugly

under_commentsheader.mp4

Above CommentsHeader, now it feels seamless, but still something feels off (bigger than what's underneath it)

above_commentsheader.mp4

I think the last one is the most comfortable to look at and interact with, but it highlights the need for a scroll button or, yes, a fixed button that floats and then brings you to the comments

cursor[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

@huumn huumn merged commit 6b440cf into stackernews:master Jul 21, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Live updates to comment threads
2 participants