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:
parent
c572fae8ae
commit
fbeba23e27
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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'}`
|
||||
|
@ -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 }) => {
|
||||
|
46
components/use-comments-view.js
Normal file
46
components/use-comments-view.js
Normal 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 }
|
||||
}
|
@ -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'
|
||||
|
Loading…
x
Reference in New Issue
Block a user