Fix comments view tracking (#2485)

* backport useCommentsView from comments refactor

* adapt live comments and creation to useCommentsView; better outline conditions

* better deps usage, remove unused props

* safer usage of root and item

* light cleanup

* cleanup: remove unused useRoot on live comments

* light cleanup and affirm purpose of each function

* fallback to createdAt if no lastCommentAt only if we actually visit the item, not by default

* fix: don't track comments, remove unused useRoot, fix signature
This commit is contained in:
soxa 2025-09-07 03:03:04 +02:00 committed by GitHub
parent c572fae8ae
commit fbeba23e27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 67 additions and 70 deletions

View File

@ -181,7 +181,7 @@ export async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ..
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward",
to_jsonb("Sub".*) || jsonb_build_object('meMuteSub', "MuteSub"."userId" IS NOT NULL)
|| jsonb_build_object('meSubscription', "SubSubscription"."userId" IS NOT NULL) as sub,
COALESCE("CommentsViewAt"."last_viewed_at", "Item"."lastCommentAt", "Item"."created_at") as "meCommentsViewedAt"
"CommentsViewAt"."last_viewed_at" as "meCommentsViewedAt"
FROM (
${query}
) "Item"

View File

@ -162,15 +162,15 @@ export default function Comment ({
const itemCreatedAt = new Date(item.createdAt).getTime()
const rootViewedAt = new Date(root.meCommentsViewedAt).getTime()
const meViewedAt = new Date(root.meCommentsViewedAt).getTime()
const viewedAt = me?.id ? meViewedAt : router.query.commentsViewedAt
const isNewComment = viewedAt && itemCreatedAt > viewedAt
// injected comments are new regardless of me or anon view time
const rootLast = new Date(root.lastCommentAt || root.createdAt).getTime()
// it's a new comment if it was created after the last comment was viewed
const isNewComment = me?.id && rootViewedAt
? itemCreatedAt > rootViewedAt
// anon fallback is based on the commentsViewedAt query param or the last comment createdAt
: ((router.query.commentsViewedAt && itemCreatedAt > router.query.commentsViewedAt) ||
(itemCreatedAt > rootLast))
if (!isNewComment) return
const isNewInjectedComment = item.injected && itemCreatedAt > (meViewedAt || rootLast)
if (!isNewComment && !isNewInjectedComment) return
if (item.injected) {
// newly injected comments (item.injected) have to use a different class to outline every new comment

View File

@ -11,7 +11,6 @@ import { useMe } from './me'
import Button from 'react-bootstrap/Button'
import { useEffect } from 'react'
import Poll from './poll'
import { commentsViewed } from '@/lib/new-comments'
import Related from './related'
import PastBounties from './past-bounties'
import Check from '@/svgs/check-double-line.svg'
@ -27,8 +26,7 @@ import classNames from 'classnames'
import { CarouselProvider } from './carousel'
import Embed from './embed'
import { useRouter } from 'next/router'
import { useMutation } from '@apollo/client'
import { UPDATE_ITEM_USER_VIEW } from '@/fragments/items'
import useCommentsView from './use-comments-view'
function BioItem ({ item, handleClick }) {
const { me } = useMe()
@ -164,25 +162,12 @@ function ItemText ({ item }) {
}
export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props }) {
const { me } = useMe()
// no cache update here because we need to preserve the initial value
const [updateCommentsViewAt] = useMutation(UPDATE_ITEM_USER_VIEW)
const { markItemViewed } = useCommentsView(item.id, { updateCache: false })
useEffect(() => {
if (item.parentId) return
// local comments viewed (anon fallback)
if (!me?.id) return commentsViewed(item)
const last = new Date(item.lastCommentAt || item.createdAt)
const viewedAt = new Date(item.meCommentsViewedAt)
if (viewedAt.getTime() >= last.getTime()) return
// me server comments viewed
updateCommentsViewAt({
variables: { id: item.id, meCommentsViewedAt: last }
})
}, [item.id, item.lastCommentAt, item.createdAt, item.meCommentsViewedAt, me?.id])
markItemViewed(item)
}, [item.id, markItemViewed])
const router = useRouter()
const carouselKey = `${item.id}-${router.query?.sort || 'default'}`

View File

@ -1,11 +1,9 @@
import { Form, MarkdownInput } from '@/components/form'
import styles from './reply.module.css'
import { COMMENTS } from '@/fragments/comments'
import { UPDATE_ITEM_USER_VIEW } from '@/fragments/items'
import { useMe } from './me'
import { forwardRef, useCallback, useEffect, useState, useRef, useMemo } from 'react'
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
import { commentsViewedAfterComment } from '@/lib/new-comments'
import { commentSchema } from '@/lib/validate'
import { ItemButtonBar } from './post'
import { useShowModal } from './modal'
@ -15,7 +13,7 @@ import { CREATE_COMMENT } from '@/fragments/paidAction'
import useItemSubmit from './use-item-submit'
import gql from 'graphql-tag'
import { updateAncestorsCommentCount } from '@/lib/comments'
import { useMutation } from '@apollo/client'
import useCommentsView from './use-comments-view'
export default forwardRef(function Reply ({
item,
@ -32,14 +30,7 @@ export default forwardRef(function Reply ({
const showModal = useShowModal()
const root = useRoot()
const sub = item?.sub || root?.sub
const [updateCommentsViewAt] = useMutation(UPDATE_ITEM_USER_VIEW, {
update (cache, { data: { updateCommentsViewAt } }) {
cache.modify({
id: `Item:${root?.id}`,
fields: { meCommentsViewedAt: () => updateCommentsViewAt }
})
}
})
const { markCommentViewedAt } = useCommentsView(root.id)
useEffect(() => {
if (replyOpen || quote || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text')) {
@ -97,14 +88,7 @@ export default forwardRef(function Reply ({
// so that we don't see indicator for our own comments, we record this comments as the latest time
// but we also have record num comments, in case someone else commented when we did
const rootId = ancestors[0]
if (me?.id) {
// server-tracked view
updateCommentsViewAt({ variables: { id: rootId, meCommentsViewedAt: result.createdAt } })
} else {
// anon fallback
commentsViewedAfterComment(rootId, result.createdAt)
}
markCommentViewedAt(result.createdAt, { ncomments: 1 })
}
},
onSuccessfulSubmit: (data, { resetForm }) => {

View File

@ -0,0 +1,46 @@
import { useMutation } from '@apollo/client'
import { useCallback } from 'react'
import { UPDATE_ITEM_USER_VIEW } from '@/fragments/items'
import { commentsViewedAfterComment, commentsViewed, newComments } from '@/lib/new-comments'
import { useMe } from './me'
export default function useCommentsView (itemId, { updateCache = true } = {}) {
const { me } = useMe()
const [updateCommentsViewAt] = useMutation(UPDATE_ITEM_USER_VIEW, {
update (cache, { data: { updateCommentsViewAt } }) {
if (!updateCache || !itemId) return
cache.modify({
id: `Item:${itemId}`,
fields: { meCommentsViewedAt: () => updateCommentsViewAt }
})
}
})
const updateViewedAt = useCallback((latest, anonFallbackFn) => {
if (me?.id) {
updateCommentsViewAt({ variables: { id: Number(itemId), meCommentsViewedAt: latest } })
} else {
anonFallbackFn()
}
}, [me?.id, itemId, updateCommentsViewAt])
// update meCommentsViewedAt on comment injection
const markCommentViewedAt = useCallback((latest, { ncomments } = {}) => {
if (!latest) return
updateViewedAt(latest, () => commentsViewedAfterComment(itemId, latest, ncomments))
}, [itemId, updateViewedAt])
// update meCommentsViewedAt on item view
const markItemViewed = useCallback((item, latest) => {
if (!item || item.parentId || (item?.meCommentsViewedAt && !newComments(item))) return
const lastAt = latest || item?.lastCommentAt || item?.createdAt
const newLatest = new Date(lastAt)
updateViewedAt(newLatest, () => commentsViewed(item))
}, [updateViewedAt])
return { markCommentViewedAt, markItemViewed }
}

View File

@ -1,18 +1,15 @@
import preserveScroll from './preserve-scroll'
import { GET_NEW_COMMENTS } from '../fragments/comments'
import { UPDATE_ITEM_USER_VIEW } from '../fragments/items'
import { useEffect, useState, useCallback } from 'react'
import { SSR, COMMENT_DEPTH_LIMIT } from '../lib/constants'
import { useQuery, useApolloClient, useMutation } from '@apollo/client'
import { commentsViewedAfterComment } from '../lib/new-comments'
import { useQuery, useApolloClient } from '@apollo/client'
import useCommentsView from './use-comments-view'
import {
updateItemQuery,
updateCommentFragment,
updateAncestorsCommentCount,
calculateDepth
} from '../lib/comments'
import { useMe } from './me'
import { useRoot } from './root'
const POLL_INTERVAL = 1000 * 5 // 5 seconds
@ -91,16 +88,7 @@ function cacheNewComments (cache, latest, topLevelId, newComments, sort) {
export default function useLiveComments (topLevelId, after, sort) {
const latestKey = `liveCommentsLatest:${topLevelId}`
const { cache } = useApolloClient()
const { me } = useMe()
const root = useRoot()
const [updateCommentsViewAt] = useMutation(UPDATE_ITEM_USER_VIEW, {
update (cache, { data: { updateCommentsViewAt } }) {
cache.modify({
id: `Item:${root.id}`,
fields: { meCommentsViewedAt: () => updateCommentsViewAt }
})
}
})
const { markCommentViewedAt } = useCommentsView(topLevelId)
const [disableLiveComments] = useLiveCommentsToggle()
const [latest, setLatest] = useState(after)
const [initialized, setInitialized] = useState(false)
@ -138,13 +126,7 @@ export default function useLiveComments (topLevelId, after, sort) {
// sync view time if we successfully injected new comments
if (new Date(injectedLatest).getTime() > new Date(latest).getTime()) {
if (me?.id) {
// server-tracked view
updateCommentsViewAt({ variables: { id: root.id, meCommentsViewedAt: injectedLatest } })
} else {
// anon fallback
commentsViewedAfterComment(root.id, injectedLatest)
}
markCommentViewedAt(injectedLatest)
// update latest timestamp to the latest comment created at
// save it to session storage, to persist between client-side navigations
@ -153,7 +135,7 @@ export default function useLiveComments (topLevelId, after, sort) {
window.sessionStorage.setItem(latestKey, injectedLatest)
}
}
}, [data, cache, topLevelId, root.id, sort, latest, me?.id])
}, [data, cache, topLevelId, sort, latest, markCommentViewedAt])
}
const STORAGE_KEY = 'disableLiveComments'