diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 14905f74..1f55c991 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -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" diff --git a/components/comment.js b/components/comment.js index 9267658b..0130591a 100644 --- a/components/comment.js +++ b/components/comment.js @@ -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 diff --git a/components/item-full.js b/components/item-full.js index aa6b47c6..b215bf55 100644 --- a/components/item-full.js +++ b/components/item-full.js @@ -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'}` diff --git a/components/reply.js b/components/reply.js index 329a9382..5d6c5837 100644 --- a/components/reply.js +++ b/components/reply.js @@ -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 }) => { diff --git a/components/use-comments-view.js b/components/use-comments-view.js new file mode 100644 index 00000000..dadc9010 --- /dev/null +++ b/components/use-comments-view.js @@ -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 } +} diff --git a/components/use-live-comments.js b/components/use-live-comments.js index 348aafe8..91249ecc 100644 --- a/components/use-live-comments.js +++ b/components/use-live-comments.js @@ -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'