stacker.news/components/preserve-scroll.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

53 lines
1.7 KiB
JavaScript

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) {
return callback()
}
// 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 })
return callback()
}