live comments: comments navigation (#2377)
* live comments: stable navigator for new outlined comments * navigator keyboard shortcuts: arrow right/escape key * enhance: responsive fixed positioning; cleanup enhance: - two types of padding for desktop and mobile via CSS cleanup: - use appropriate <aside> for navigator - reorder CSS * Comments Navigator Context, new comments dot UI, refs autosorting, auto-untrack children - Navigator Context for item pages UI/UX - WIP: compact comments dot UI on navbars - long press to clear tracked refs - auto-untrack node's children on scroll Logic - auto-sort comment refs via createdAt - remove outline on untrack if called by scroll * stable navigator dot UI positioning * cleanup: better naming, clear structure * CSS visibility tweaks * scroll to start position of ref * fix undefined navigator on other comment calls * remove pulse animation
This commit is contained in:
		
							parent
							
								
									0394a5bdc2
								
							
						
					
					
						commit
						df2ccd9840
					
				@ -97,7 +97,8 @@ export function CommentFlat ({ item, rank, siblingComments, ...props }) {
 | 
			
		||||
 | 
			
		||||
export default function Comment ({
 | 
			
		||||
  item, children, replyOpen, includeParent, topLevel, rootLastCommentAt,
 | 
			
		||||
  rootText, noComments, noReply, truncate, depth, pin, setDisableRetry, disableRetry
 | 
			
		||||
  rootText, noComments, noReply, truncate, depth, pin, setDisableRetry, disableRetry,
 | 
			
		||||
  navigator
 | 
			
		||||
}) {
 | 
			
		||||
  const [edit, setEdit] = useState()
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
@ -122,6 +123,8 @@ export default function Comment ({
 | 
			
		||||
    // 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')
 | 
			
		||||
      // untrack the new comment
 | 
			
		||||
      navigator?.untrackNewComment(ref)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -173,6 +176,8 @@ export default function Comment ({
 | 
			
		||||
    } else {
 | 
			
		||||
      ref.current.classList.add('outline-new-comment')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    navigator?.trackNewComment(ref, itemCreatedAt)
 | 
			
		||||
  }, [item.id, rootLastCommentAt])
 | 
			
		||||
 | 
			
		||||
  const bottomedOut = depth === COMMENT_DEPTH_LIMIT || (item.comments?.comments.length === 0 && item.nDirectComments > 0)
 | 
			
		||||
@ -295,7 +300,7 @@ export default function Comment ({
 | 
			
		||||
                  ? (
 | 
			
		||||
                    <>
 | 
			
		||||
                      {item.comments.comments.map((item) => (
 | 
			
		||||
                        <Comment depth={depth + 1} key={item.id} item={item} rootLastCommentAt={rootLastCommentAt} />
 | 
			
		||||
                        <Comment depth={depth + 1} key={item.id} item={item} navigator={navigator} rootLastCommentAt={rootLastCommentAt} />
 | 
			
		||||
                      ))}
 | 
			
		||||
                      {item.comments.comments.length < item.nDirectComments && (
 | 
			
		||||
                        <div className={`d-block ${styles.comment} pb-2 ps-3`}>
 | 
			
		||||
 | 
			
		||||
@ -137,34 +137,30 @@
 | 
			
		||||
    padding-top: .5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.newCommentDot {
 | 
			
		||||
    width: 10px;
 | 
			
		||||
    height: 10px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    background-color: var(--bs-primary);
 | 
			
		||||
    animation: pulse 2s infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes pulse {
 | 
			
		||||
    0% {
 | 
			
		||||
        background-color: #80d3ff;
 | 
			
		||||
        opacity: 0.7;
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
        background-color: #007cbe;
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
        background-color: #80d3ff;
 | 
			
		||||
        opacity: 0.7;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.injectedComment {
 | 
			
		||||
    animation: fadeIn 0.5s ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes fadeIn {
 | 
			
		||||
  from { opacity: 0; }
 | 
			
		||||
  to { opacity: 1; }
 | 
			
		||||
    from { opacity: 0; }
 | 
			
		||||
    to { opacity: 1; }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
.commentNavigator {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    gap: 0.2rem;
 | 
			
		||||
    padding-bottom: 0.2rem;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    /* prevent double tap from zooming */
 | 
			
		||||
    touch-action: manipulation;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.newCommentDot {
 | 
			
		||||
    width: 12px;
 | 
			
		||||
    height: 12px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    background-color: #007cbe;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import { useRouter } from 'next/router'
 | 
			
		||||
import MoreFooter from './more-footer'
 | 
			
		||||
import { FULL_COMMENTS_THRESHOLD } from '@/lib/constants'
 | 
			
		||||
import useLiveComments from './use-live-comments'
 | 
			
		||||
import { useCommentsNavigatorContext } from './use-comments-navigator'
 | 
			
		||||
 | 
			
		||||
export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, commentSats }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
@ -72,6 +73,9 @@ export default function Comments ({
 | 
			
		||||
  // fetch new comments that arrived after the lastCommentAt, and update the item.comments field in cache
 | 
			
		||||
  useLiveComments(parentId, lastCommentAt || parentCreatedAt, router.query.sort)
 | 
			
		||||
 | 
			
		||||
  // new comments navigator, tracks new comments and provides navigation controls
 | 
			
		||||
  const { navigator } = useCommentsNavigatorContext()
 | 
			
		||||
 | 
			
		||||
  const pins = useMemo(() => comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position), [comments])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -94,11 +98,11 @@ export default function Comments ({
 | 
			
		||||
        : null}
 | 
			
		||||
      {pins.map(item => (
 | 
			
		||||
        <Fragment key={item.id}>
 | 
			
		||||
          <Comment depth={1} item={item} rootLastCommentAt={lastCommentAt || parentCreatedAt} {...props} pin />
 | 
			
		||||
          <Comment depth={1} item={item} navigator={navigator} rootLastCommentAt={lastCommentAt || parentCreatedAt} {...props} pin />
 | 
			
		||||
        </Fragment>
 | 
			
		||||
      ))}
 | 
			
		||||
      {comments.filter(({ position }) => !position).map(item => (
 | 
			
		||||
        <Comment depth={1} key={item.id} item={item} rootLastCommentAt={lastCommentAt || parentCreatedAt} {...props} />
 | 
			
		||||
        <Comment depth={1} key={item.id} item={item} navigator={navigator} rootLastCommentAt={lastCommentAt || parentCreatedAt} {...props} />
 | 
			
		||||
      ))}
 | 
			
		||||
      {ncomments > FULL_COMMENTS_THRESHOLD &&
 | 
			
		||||
        <MoreFooter
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,11 @@ import { Nav, Navbar } from 'react-bootstrap'
 | 
			
		||||
import styles from '../../header.module.css'
 | 
			
		||||
import { AnonCorner, Back, Brand, MeCorner, NavPrice, SearchItem } from '../common'
 | 
			
		||||
import { useMe } from '../../me'
 | 
			
		||||
import { useCommentsNavigatorContext, CommentsNavigator } from '@/components/use-comments-navigator'
 | 
			
		||||
 | 
			
		||||
export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
  const { navigator, commentCount } = useCommentsNavigatorContext()
 | 
			
		||||
  return (
 | 
			
		||||
    <Navbar>
 | 
			
		||||
      <Nav
 | 
			
		||||
@ -15,6 +17,7 @@ export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
 | 
			
		||||
        <Brand className='me-1' />
 | 
			
		||||
        <SearchItem prefix={prefix} className='me-0 ms-2 d-none d-md-flex' />
 | 
			
		||||
        <NavPrice className='ms-auto me-0 mx-md-auto d-none d-md-flex' />
 | 
			
		||||
        <CommentsNavigator navigator={navigator} commentCount={commentCount} />
 | 
			
		||||
        {me
 | 
			
		||||
          ? <MeCorner dropNavKey={dropNavKey} me={me} className='d-none d-md-flex' />
 | 
			
		||||
          : <AnonCorner path={path} className='d-none d-md-flex' />}
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,12 @@ import { Nav, Navbar } from 'react-bootstrap'
 | 
			
		||||
import styles from '../../header.module.css'
 | 
			
		||||
import { Back, NavPrice, NavSelect, NavWalletSummary, SignUpButton, hasNavSelect } from '../common'
 | 
			
		||||
import { useMe } from '@/components/me'
 | 
			
		||||
import { useCommentsNavigatorContext, CommentsNavigator } from '@/components/use-comments-navigator'
 | 
			
		||||
 | 
			
		||||
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
  const { navigator, commentCount } = useCommentsNavigatorContext()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Navbar>
 | 
			
		||||
      <Nav
 | 
			
		||||
@ -17,6 +20,7 @@ export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNa
 | 
			
		||||
          : (
 | 
			
		||||
            <>
 | 
			
		||||
              <NavPrice className='flex-shrink-1' />
 | 
			
		||||
              <CommentsNavigator navigator={navigator} commentCount={commentCount} className='px-2' />
 | 
			
		||||
              {me ? <NavWalletSummary /> : <SignUpButton width='fit-content' />}
 | 
			
		||||
            </>)}
 | 
			
		||||
      </Nav>
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,12 @@ import { Container, Nav, Navbar } from 'react-bootstrap'
 | 
			
		||||
import { NavPrice, MeCorner, AnonCorner, SearchItem, Back, NavWalletSummary, Brand, SignUpButton } from './common'
 | 
			
		||||
import { useMe } from '@/components/me'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import { CommentsNavigator, useCommentsNavigatorContext } from '../use-comments-navigator'
 | 
			
		||||
 | 
			
		||||
export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
 | 
			
		||||
  const ref = useRef()
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
  const { navigator, commentCount } = useCommentsNavigatorContext()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const stick = () => {
 | 
			
		||||
@ -37,6 +39,7 @@ export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey })
 | 
			
		||||
            <Brand className='me-1' />
 | 
			
		||||
            <SearchItem className='me-0 ms-2' />
 | 
			
		||||
            <NavPrice />
 | 
			
		||||
            <CommentsNavigator navigator={navigator} commentCount={commentCount} className='d-flex' />
 | 
			
		||||
            {me ? <MeCorner dropNavKey={dropNavKey} me={me} className='d-flex' /> : <AnonCorner path={path} className='d-flex' />}
 | 
			
		||||
          </Nav>
 | 
			
		||||
        </Navbar>
 | 
			
		||||
@ -44,11 +47,12 @@ export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey })
 | 
			
		||||
      <Container className='px-sm-0 d-block d-md-none'>
 | 
			
		||||
        <Navbar className='py-0'>
 | 
			
		||||
          <Nav
 | 
			
		||||
            className={classNames(styles.navbarNav, 'justify-content-between')}
 | 
			
		||||
            className={classNames(styles.navbarNav)}
 | 
			
		||||
            activeKey={topNavKey}
 | 
			
		||||
          >
 | 
			
		||||
            <Back />
 | 
			
		||||
            <NavPrice className='flex-shrink-1 flex-grow-0' />
 | 
			
		||||
            <NavPrice className='flex-shrink-1' />
 | 
			
		||||
            <CommentsNavigator navigator={navigator} commentCount={commentCount} className='d-flex' />
 | 
			
		||||
            {me ? <NavWalletSummary className='px-2' /> : <SignUpButton width='fit-content' />}
 | 
			
		||||
          </Nav>
 | 
			
		||||
        </Navbar>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										200
									
								
								components/use-comments-navigator.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								components/use-comments-navigator.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,200 @@
 | 
			
		||||
import { useCallback, useEffect, useRef, useState, startTransition, createContext, useContext } from 'react'
 | 
			
		||||
import styles from './comment.module.css'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import LongPressable from './long-pressable'
 | 
			
		||||
 | 
			
		||||
const CommentsNavigatorContext = createContext({
 | 
			
		||||
  navigator: {
 | 
			
		||||
    trackNewComment: () => {},
 | 
			
		||||
    untrackNewComment: () => {},
 | 
			
		||||
    scrollToComment: () => {},
 | 
			
		||||
    clearCommentRefs: () => {}
 | 
			
		||||
  },
 | 
			
		||||
  commentCount: 0
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export function CommentsNavigatorProvider ({ children }) {
 | 
			
		||||
  const value = useCommentsNavigator()
 | 
			
		||||
  return (
 | 
			
		||||
    <CommentsNavigatorContext.Provider value={value}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </CommentsNavigatorContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useCommentsNavigatorContext () {
 | 
			
		||||
  return useContext(CommentsNavigatorContext)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useCommentsNavigator () {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const [commentCount, setCommentCount] = useState(0)
 | 
			
		||||
  // refs in ref to not re-render on tracking
 | 
			
		||||
  const commentRefs = useRef([])
 | 
			
		||||
  // ref to track if the comment count is being updated
 | 
			
		||||
  const frameRef = useRef(null)
 | 
			
		||||
  const navigatorRef = useRef(null)
 | 
			
		||||
 | 
			
		||||
  // batch updates to the comment count
 | 
			
		||||
  const throttleCountUpdate = useCallback(() => {
 | 
			
		||||
    if (frameRef.current) return
 | 
			
		||||
    // prevent multiple updates in the same frame
 | 
			
		||||
    frameRef.current = true
 | 
			
		||||
    window.requestAnimationFrame(() => {
 | 
			
		||||
      const next = commentRefs.current.length
 | 
			
		||||
      // transition to the new comment count
 | 
			
		||||
      startTransition?.(() => setCommentCount(next))
 | 
			
		||||
      frameRef.current = false
 | 
			
		||||
    })
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  // clear the list of refs and reset the comment count
 | 
			
		||||
  const clearCommentRefs = useCallback(() => {
 | 
			
		||||
    commentRefs.current = []
 | 
			
		||||
    startTransition?.(() => setCommentCount(0))
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  // track a new comment
 | 
			
		||||
  const trackNewComment = useCallback((commentRef, createdAt) => {
 | 
			
		||||
    try {
 | 
			
		||||
      window.requestAnimationFrame(() => {
 | 
			
		||||
        if (!commentRef?.current || !commentRef.current.isConnected) return
 | 
			
		||||
 | 
			
		||||
        // don't track this new comment if it's visible in the viewport
 | 
			
		||||
        const rect = commentRef.current.getBoundingClientRect()
 | 
			
		||||
        if (rect.top >= 0 && rect.bottom <= window.innerHeight) return
 | 
			
		||||
 | 
			
		||||
        // dedupe
 | 
			
		||||
        const existing = commentRefs.current.some(item => item.ref.current === commentRef.current)
 | 
			
		||||
        if (existing) return
 | 
			
		||||
 | 
			
		||||
        // find the correct insertion position to maintain sort order
 | 
			
		||||
        const insertIndex = commentRefs.current.findIndex(item => item.createdAt > createdAt)
 | 
			
		||||
        const newItem = { ref: commentRef, createdAt }
 | 
			
		||||
 | 
			
		||||
        if (insertIndex === -1) {
 | 
			
		||||
          // append if no newer comments found
 | 
			
		||||
          commentRefs.current.push(newItem)
 | 
			
		||||
        } else {
 | 
			
		||||
          // insert at the correct position to maintain sort order
 | 
			
		||||
          commentRefs.current.splice(insertIndex, 0, newItem)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throttleCountUpdate()
 | 
			
		||||
      })
 | 
			
		||||
    } catch {
 | 
			
		||||
      // in the rare case of a ref being disconnected during RAF, ignore to avoid blocking UI
 | 
			
		||||
    }
 | 
			
		||||
  }, [throttleCountUpdate])
 | 
			
		||||
 | 
			
		||||
  // remove a comment ref from the list
 | 
			
		||||
  const untrackNewComment = useCallback((commentRef, options = {}) => {
 | 
			
		||||
    const { includeDescendants = false, clearOutline = false } = options
 | 
			
		||||
 | 
			
		||||
    const refNode = commentRef.current
 | 
			
		||||
    if (!refNode) return
 | 
			
		||||
 | 
			
		||||
    const toRemove = commentRefs.current.filter(item => {
 | 
			
		||||
      const node = item?.ref?.current
 | 
			
		||||
      return includeDescendants
 | 
			
		||||
        ? node && refNode.contains(node)
 | 
			
		||||
        : node === refNode
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (clearOutline) {
 | 
			
		||||
      for (const item of toRemove) {
 | 
			
		||||
        const node = item.ref.current
 | 
			
		||||
        if (!node) continue
 | 
			
		||||
        node.classList.remove(
 | 
			
		||||
          'outline-it',
 | 
			
		||||
          'outline-new-comment',
 | 
			
		||||
          'outline-new-injected-comment'
 | 
			
		||||
        )
 | 
			
		||||
        node.classList.add('outline-new-comment-unset')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (toRemove.length) {
 | 
			
		||||
      commentRefs.current = commentRefs.current.filter(item => !toRemove.includes(item))
 | 
			
		||||
      throttleCountUpdate()
 | 
			
		||||
    }
 | 
			
		||||
  }, [throttleCountUpdate])
 | 
			
		||||
 | 
			
		||||
  // scroll to the next new comment
 | 
			
		||||
  const scrollToComment = useCallback(() => {
 | 
			
		||||
    const list = commentRefs.current
 | 
			
		||||
    if (!list.length) return
 | 
			
		||||
 | 
			
		||||
    const ref = list[0]?.ref
 | 
			
		||||
    const node = ref?.current
 | 
			
		||||
    if (!node) return
 | 
			
		||||
 | 
			
		||||
    // smoothly scroll to the start of the comment
 | 
			
		||||
    node.scrollIntoView({ behavior: 'smooth', block: 'start' })
 | 
			
		||||
 | 
			
		||||
    // clear the outline class after the animation ends
 | 
			
		||||
    node.addEventListener('animationend', () => {
 | 
			
		||||
      node.classList.remove('outline-it')
 | 
			
		||||
    }, { once: true })
 | 
			
		||||
 | 
			
		||||
    // requestAnimationFrame to ensure untracking is processed before outlining
 | 
			
		||||
    window.requestAnimationFrame(() => {
 | 
			
		||||
      node.classList.add('outline-it')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // untrack the new comment and clear the outlines
 | 
			
		||||
    untrackNewComment(ref, { includeDescendants: true, clearOutline: true })
 | 
			
		||||
 | 
			
		||||
    // if we reached the end, reset the navigator
 | 
			
		||||
    if (list.length === 1) clearCommentRefs()
 | 
			
		||||
  }, [clearCommentRefs, untrackNewComment])
 | 
			
		||||
 | 
			
		||||
  // create the navigator object once
 | 
			
		||||
  if (!navigatorRef.current) {
 | 
			
		||||
    navigatorRef.current = { trackNewComment, untrackNewComment, scrollToComment, clearCommentRefs }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // clear the navigator on route changes
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    router.events.on('routeChangeStart', clearCommentRefs)
 | 
			
		||||
    return () => router.events.off('routeChangeStart', clearCommentRefs)
 | 
			
		||||
  }, [clearCommentRefs, router.events])
 | 
			
		||||
 | 
			
		||||
  return { navigator: navigatorRef.current, commentCount }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function CommentsNavigator ({ navigator, commentCount, className }) {
 | 
			
		||||
  const { scrollToComment, clearCommentRefs } = navigator
 | 
			
		||||
 | 
			
		||||
  const onNext = useCallback((e) => {
 | 
			
		||||
    // ignore if there are no new comments or if we're focused on a textarea or input
 | 
			
		||||
    if (!commentCount || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return
 | 
			
		||||
    // arrow right key scrolls to the next new comment
 | 
			
		||||
    if (e.key === 'ArrowRight' && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {
 | 
			
		||||
      e.preventDefault()
 | 
			
		||||
      scrollToComment()
 | 
			
		||||
    }
 | 
			
		||||
    // escape key clears the new comments navigator
 | 
			
		||||
    if (e.key === 'Escape') clearCommentRefs()
 | 
			
		||||
  }, [commentCount, scrollToComment, clearCommentRefs])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!commentCount) return
 | 
			
		||||
    document.addEventListener('keydown', onNext)
 | 
			
		||||
    return () => document.removeEventListener('keydown', onNext)
 | 
			
		||||
  }, [onNext])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <LongPressable onShortPress={scrollToComment} onLongPress={clearCommentRefs}>
 | 
			
		||||
      <aside
 | 
			
		||||
        className={`${styles.commentNavigator} fw-bold nav-link ${className}`}
 | 
			
		||||
        style={{ visibility: commentCount ? 'visible' : 'hidden' }}
 | 
			
		||||
      >
 | 
			
		||||
        <span aria-label='next comment' className={styles.navigatorButton}>
 | 
			
		||||
          <div className={styles.newCommentDot} />
 | 
			
		||||
        </span>
 | 
			
		||||
        <span className=''>{commentCount}</span>
 | 
			
		||||
      </aside>
 | 
			
		||||
    </LongPressable>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -1,32 +0,0 @@
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
// observe the passed element ref and return its visibility
 | 
			
		||||
export default function useVisibility (elementRef, options = {}) {
 | 
			
		||||
  // threshold is the percentage of the element that must be visible to be considered visible
 | 
			
		||||
  // with pastElement, we consider the element not visible only when we're past it
 | 
			
		||||
  const { threshold = 0, pastElement = false } = options
 | 
			
		||||
  const [isVisible, setIsVisible] = useState(true)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const element = elementRef.current
 | 
			
		||||
    if (!element || !window.IntersectionObserver || typeof window === 'undefined') return
 | 
			
		||||
 | 
			
		||||
    const observer = new window.IntersectionObserver(
 | 
			
		||||
      ([entry]) => {
 | 
			
		||||
        if (entry.isIntersecting) {
 | 
			
		||||
          setIsVisible(true)
 | 
			
		||||
        } else if (pastElement) {
 | 
			
		||||
          setIsVisible(entry.boundingClientRect.top > 0)
 | 
			
		||||
        } else {
 | 
			
		||||
          setIsVisible(false)
 | 
			
		||||
        }
 | 
			
		||||
      }, { threshold }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    // observe the passed element ref
 | 
			
		||||
    observer.observe(element)
 | 
			
		||||
    return () => observer.disconnect()
 | 
			
		||||
  }, [threshold, elementRef, pastElement])
 | 
			
		||||
 | 
			
		||||
  return isVisible
 | 
			
		||||
}
 | 
			
		||||
@ -5,6 +5,7 @@ import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
			
		||||
import { useQuery } from '@apollo/client'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import PageLoading from '@/components/page-loading'
 | 
			
		||||
import { CommentsNavigatorProvider } from '@/components/use-comments-navigator'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps({
 | 
			
		||||
  query: ITEM_FULL,
 | 
			
		||||
@ -25,8 +26,10 @@ export default function Item ({ ssrData }) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout sub={sub} item={item}>
 | 
			
		||||
      <ItemFull item={item} fetchMoreComments={fetchMoreComments} />
 | 
			
		||||
    </Layout>
 | 
			
		||||
    <CommentsNavigatorProvider>
 | 
			
		||||
      <Layout sub={sub} item={item}>
 | 
			
		||||
        <ItemFull item={item} fetchMoreComments={fetchMoreComments} />
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </CommentsNavigatorProvider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user