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",
|
"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)
|
to_jsonb("Sub".*) || jsonb_build_object('meMuteSub', "MuteSub"."userId" IS NOT NULL)
|
||||||
|| jsonb_build_object('meSubscription', "SubSubscription"."userId" IS NOT NULL) as sub,
|
|| 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 (
|
FROM (
|
||||||
${query}
|
${query}
|
||||||
) "Item"
|
) "Item"
|
||||||
|
@ -162,15 +162,15 @@ export default function Comment ({
|
|||||||
|
|
||||||
const itemCreatedAt = new Date(item.createdAt).getTime()
|
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()
|
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 isNewInjectedComment = item.injected && itemCreatedAt > (meViewedAt || rootLast)
|
||||||
const isNewComment = me?.id && rootViewedAt
|
|
||||||
? itemCreatedAt > rootViewedAt
|
if (!isNewComment && !isNewInjectedComment) return
|
||||||
// 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
|
|
||||||
|
|
||||||
if (item.injected) {
|
if (item.injected) {
|
||||||
// newly injected comments (item.injected) have to use a different class to outline every new comment
|
// 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 Button from 'react-bootstrap/Button'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import Poll from './poll'
|
import Poll from './poll'
|
||||||
import { commentsViewed } from '@/lib/new-comments'
|
|
||||||
import Related from './related'
|
import Related from './related'
|
||||||
import PastBounties from './past-bounties'
|
import PastBounties from './past-bounties'
|
||||||
import Check from '@/svgs/check-double-line.svg'
|
import Check from '@/svgs/check-double-line.svg'
|
||||||
@ -27,8 +26,7 @@ import classNames from 'classnames'
|
|||||||
import { CarouselProvider } from './carousel'
|
import { CarouselProvider } from './carousel'
|
||||||
import Embed from './embed'
|
import Embed from './embed'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useMutation } from '@apollo/client'
|
import useCommentsView from './use-comments-view'
|
||||||
import { UPDATE_ITEM_USER_VIEW } from '@/fragments/items'
|
|
||||||
|
|
||||||
function BioItem ({ item, handleClick }) {
|
function BioItem ({ item, handleClick }) {
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
@ -164,25 +162,12 @@ function ItemText ({ item }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props }) {
|
export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props }) {
|
||||||
const { me } = useMe()
|
|
||||||
// no cache update here because we need to preserve the initial value
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (item.parentId) return
|
markItemViewed(item)
|
||||||
// local comments viewed (anon fallback)
|
}, [item.id, markItemViewed])
|
||||||
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])
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const carouselKey = `${item.id}-${router.query?.sort || 'default'}`
|
const carouselKey = `${item.id}-${router.query?.sort || 'default'}`
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { Form, MarkdownInput } from '@/components/form'
|
import { Form, MarkdownInput } from '@/components/form'
|
||||||
import styles from './reply.module.css'
|
import styles from './reply.module.css'
|
||||||
import { COMMENTS } from '@/fragments/comments'
|
import { COMMENTS } from '@/fragments/comments'
|
||||||
import { UPDATE_ITEM_USER_VIEW } from '@/fragments/items'
|
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { forwardRef, useCallback, useEffect, useState, useRef, useMemo } from 'react'
|
import { forwardRef, useCallback, useEffect, useState, useRef, useMemo } from 'react'
|
||||||
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
|
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
|
||||||
import { commentsViewedAfterComment } from '@/lib/new-comments'
|
|
||||||
import { commentSchema } from '@/lib/validate'
|
import { commentSchema } from '@/lib/validate'
|
||||||
import { ItemButtonBar } from './post'
|
import { ItemButtonBar } from './post'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
@ -15,7 +13,7 @@ import { CREATE_COMMENT } from '@/fragments/paidAction'
|
|||||||
import useItemSubmit from './use-item-submit'
|
import useItemSubmit from './use-item-submit'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { updateAncestorsCommentCount } from '@/lib/comments'
|
import { updateAncestorsCommentCount } from '@/lib/comments'
|
||||||
import { useMutation } from '@apollo/client'
|
import useCommentsView from './use-comments-view'
|
||||||
|
|
||||||
export default forwardRef(function Reply ({
|
export default forwardRef(function Reply ({
|
||||||
item,
|
item,
|
||||||
@ -32,14 +30,7 @@ export default forwardRef(function Reply ({
|
|||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const sub = item?.sub || root?.sub
|
const sub = item?.sub || root?.sub
|
||||||
const [updateCommentsViewAt] = useMutation(UPDATE_ITEM_USER_VIEW, {
|
const { markCommentViewedAt } = useCommentsView(root.id)
|
||||||
update (cache, { data: { updateCommentsViewAt } }) {
|
|
||||||
cache.modify({
|
|
||||||
id: `Item:${root?.id}`,
|
|
||||||
fields: { meCommentsViewedAt: () => updateCommentsViewAt }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (replyOpen || quote || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text')) {
|
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
|
// 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
|
// but we also have record num comments, in case someone else commented when we did
|
||||||
const rootId = ancestors[0]
|
markCommentViewedAt(result.createdAt, { ncomments: 1 })
|
||||||
if (me?.id) {
|
|
||||||
// server-tracked view
|
|
||||||
updateCommentsViewAt({ variables: { id: rootId, meCommentsViewedAt: result.createdAt } })
|
|
||||||
} else {
|
|
||||||
// anon fallback
|
|
||||||
commentsViewedAfterComment(rootId, result.createdAt)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccessfulSubmit: (data, { resetForm }) => {
|
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 preserveScroll from './preserve-scroll'
|
||||||
import { GET_NEW_COMMENTS } from '../fragments/comments'
|
import { GET_NEW_COMMENTS } from '../fragments/comments'
|
||||||
import { UPDATE_ITEM_USER_VIEW } from '../fragments/items'
|
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { SSR, COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
import { SSR, COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
||||||
import { useQuery, useApolloClient, useMutation } from '@apollo/client'
|
import { useQuery, useApolloClient } from '@apollo/client'
|
||||||
import { commentsViewedAfterComment } from '../lib/new-comments'
|
import useCommentsView from './use-comments-view'
|
||||||
import {
|
import {
|
||||||
updateItemQuery,
|
updateItemQuery,
|
||||||
updateCommentFragment,
|
updateCommentFragment,
|
||||||
updateAncestorsCommentCount,
|
updateAncestorsCommentCount,
|
||||||
calculateDepth
|
calculateDepth
|
||||||
} from '../lib/comments'
|
} from '../lib/comments'
|
||||||
import { useMe } from './me'
|
|
||||||
import { useRoot } from './root'
|
|
||||||
|
|
||||||
const POLL_INTERVAL = 1000 * 5 // 5 seconds
|
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) {
|
export default function useLiveComments (topLevelId, after, sort) {
|
||||||
const latestKey = `liveCommentsLatest:${topLevelId}`
|
const latestKey = `liveCommentsLatest:${topLevelId}`
|
||||||
const { cache } = useApolloClient()
|
const { cache } = useApolloClient()
|
||||||
const { me } = useMe()
|
const { markCommentViewedAt } = useCommentsView(topLevelId)
|
||||||
const root = useRoot()
|
|
||||||
const [updateCommentsViewAt] = useMutation(UPDATE_ITEM_USER_VIEW, {
|
|
||||||
update (cache, { data: { updateCommentsViewAt } }) {
|
|
||||||
cache.modify({
|
|
||||||
id: `Item:${root.id}`,
|
|
||||||
fields: { meCommentsViewedAt: () => updateCommentsViewAt }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const [disableLiveComments] = useLiveCommentsToggle()
|
const [disableLiveComments] = useLiveCommentsToggle()
|
||||||
const [latest, setLatest] = useState(after)
|
const [latest, setLatest] = useState(after)
|
||||||
const [initialized, setInitialized] = useState(false)
|
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
|
// sync view time if we successfully injected new comments
|
||||||
if (new Date(injectedLatest).getTime() > new Date(latest).getTime()) {
|
if (new Date(injectedLatest).getTime() > new Date(latest).getTime()) {
|
||||||
if (me?.id) {
|
markCommentViewedAt(injectedLatest)
|
||||||
// server-tracked view
|
|
||||||
updateCommentsViewAt({ variables: { id: root.id, meCommentsViewedAt: injectedLatest } })
|
|
||||||
} else {
|
|
||||||
// anon fallback
|
|
||||||
commentsViewedAfterComment(root.id, injectedLatest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update latest timestamp to the latest comment created at
|
// update latest timestamp to the latest comment created at
|
||||||
// save it to session storage, to persist between client-side navigations
|
// 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)
|
window.sessionStorage.setItem(latestKey, injectedLatest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data, cache, topLevelId, root.id, sort, latest, me?.id])
|
}, [data, cache, topLevelId, sort, latest, markCommentViewedAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'disableLiveComments'
|
const STORAGE_KEY = 'disableLiveComments'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user