Live updates to comment threads (#2115)

* check new comments every 10 seconds

* enhance: clear newComments on child comments when we show a topLevel new comment; cleanup: resolvers, logs

* handle comments of comments, new structure to clear newComments on childs

* use original recursive comments data structure

* correct comment structure after deduplication

* faster newComments query deduplication, don't need to know how many comments are there

* cleanup: comments on newComments fetches and dedupes

* cleanup, use correct function declarations

* stop polling after 30 minutes, pause polling if user is not on the page

* ActionTooltip indicating that the user is in a live comment section

* handleVisibilityChange to control polling by visibility

* paused polling styling, check activity on 1 minute intervals and visibility change, light cleanup

* user can resume polling without refreshing the page

* better naming, straightforward dedupeComment on newComment arrival

* cleanup: better naming, get latest comment creation, correct order of comment injection

* cleanup: refactor live comments related functions to use-live-comments.js

* refactor: clearer naming, optimized polling and date retrieval logic, use of constants, general cleanup

* ui: place ShowNewComments in the bottom-right corner of nested comments

* fix: make updateQuery sort-aware to correctly inject the comment in the correct Item query

* cleanup: better naming; fix: usecallback on live comments component; fix leak on useEffect because of missing sort
atomic apollo cache manipulations; manage top sort not being present in item query cache
queue nested comments without a parent, retry on the next poll
fix commit messages

* fix: don't show unpaid comments; cleanup: compact cache merge/dedupe, queue comments via state

* fix: read new comments fragments to inject fresh new comments, fixing dropped comments;

ui: show amount of new comments

refactor: correct function positioning;

cleanup: useless logs

* enhance: queuedComments Ref, cache-and-network fetch policy; freshNewComments readFragment fallback to received comment

* cleanup: detailed comments and better ShowNewComment text

* fix: while showing new comments, also update ncomments for UI and pagination

* refactor: ShowNewComments is its own component; cleanup: proven useless dedupe on ShowNewComments, count nested ncomments from fresh new comments

* enhance: direct latest comment createdAt calc with reduce

* cleanup queue on unmount

* feat: live comments indicator for bottomed-out replies, ncomments updates; fix: nested comment structures

- new comments indicator for bottomed-out replies
- ncomments sync for parent and its ancestors
- limited comments fragment for comments that don't have CommentsRecursive
- reduce cache complexity by removing useless roundtrips

ux: live comments indicator on bottomedOut replies

fix: dedupe newComments before displaying ShowNewComments to avoid false positives

enhance: store ids of new comments in the cache, instead of carrying full comments that would get discarded anyway

hotfix: newComments deduplication ID mismatch, filter null comments from freshNewComments

fix: ncomments not updating for all comment levels; refactor: share Reply update ancestors' ncomments function with ShowNewComments

cleanup: better naming to indicate the total number of comments including nested comments

fix: increment parent comment ncomments

cleanup: Items that will have comments will always have a structure where item.comments is true

cleanup: reduce code complexity checking the nested comment update result instead of preventively reading the fragment

cleanup: avoid double-updating ncomments on parent

fix: don't use CommentsRecursive for bottomed-out comments

cleanup: better fragment naming; add TODO for absolute bottom comments

* backport live comments logic enhancements

use-live-comments:
- remove useless dedupe against already present comments
- check newComments.comments length to tell if there are new comments
- code reordering

show-new-comments:
- show all new comments recursively for nested comments
- get always the newest comments to inject also their own child new comments
- update local storage commentsViewedAt on comment injection
- respect depth on comment injection

comments.js
- apollo cache manipulations now live here

* hotfix: handle undefined item.comments.comments on dedupe

* hotfix: limited fragment for recursive comment collection; protect from null fragments; add missing deps to memoization

* docs: clarify ncomments updates

* cleanup: remove unused export

* count and show only the direct new comments and recursively their children

enhance: dedupe against existing comments only in the component
enhance: recursive count/injection share the same logic

* fix regression on top level counting

* hotfix: introduce readNestedCommentsFragment in lib/comments.js

* fix: count also existing comments of a new comment; cleanup: use readCommentFragment also for prepareComments; reduce freshNewComments usage

* add support for comments at the deepest level

fixes:
- client-side navigation re-fetched all new comments because 'after' was cached, now the latest new comment time persists in sessionStorage

enhancements:
- use CommentWithNewMinimal fragment fallback for comments at the deepest level
- tweak ReplyOnAnotherPage to show also how many direct new comments are there

cleanup:
- queue management is not needed anymore, therefore it has been removed

* cleanup: remove logs

* revert counting on ReplyOnAnotherPage, TODO for enhancements PR

* move ShowNewComments to CommentsHeader for top level comments

* fix: update commentsViewedAfterComment to support ncomments

* fix typo, lint

* cleanup: remove old CSS

* enhance: inject topLevel and its children new comments, simplify injection logic

- top-level and nested comment handling share the same recursion logic
- ShowNewComments references the item object for every type of comments
— note: item from item-full.js is passed to comments.js
- depth now starts at 0 to support top level comments
- injection and counting now reach the deepest level, updating also the deepest comment

* cleanup: remove unused topLevel prop

* fix: deepest comments don't have CommentsRecursive structure, don't access it on injection

* move top level ShowNewComments above CommentsHeader; preserve space to avoid vertical layout shifting

* cleanup: remove unused item on CommentsHeader

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
soxa 2025-07-21 22:38:15 +02:00 committed by GitHub
parent 74ef0076fa
commit 6b440cfdf3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 426 additions and 16 deletions

View File

@ -739,6 +739,24 @@ export default {
homeMaxBoost: homeAgg._max.boost || 0, homeMaxBoost: homeAgg._max.boost || 0,
subMaxBoost: subAgg?._max.boost || 0 subMaxBoost: subAgg?._max.boost || 0
} }
},
newComments: async (parent, { rootId, after }, { models, me }) => {
const comments = await itemQueryWithMeta({
me,
models,
query: `
${SELECT}
FROM "Item"
-- comments can be nested, so we need to get all comments that are descendants of the root
${whereClause(
'"Item".path <@ (SELECT path FROM "Item" WHERE id = $1)',
activeOrMine(me),
'"Item"."created_at" > $2'
)}
ORDER BY "Item"."created_at" ASC`
}, Number(rootId), after)
return { comments }
} }
}, },

View File

@ -11,6 +11,7 @@ export default gql`
auctionPosition(sub: String, id: ID, boost: Int): Int! auctionPosition(sub: String, id: ID, boost: Int): Int!
boostPosition(sub: String, id: ID, boost: Int): BoostPositions! boostPosition(sub: String, id: ID, boost: Int): BoostPositions!
itemRepetition(parentId: ID): Int! itemRepetition(parentId: ID): Int!
newComments(rootId: ID, after: Date): Comments!
} }
type BoostPositions { type BoostPositions {
@ -148,6 +149,7 @@ export default gql`
ncomments: Int! ncomments: Int!
nDirectComments: Int! nDirectComments: Int!
comments(sort: String, cursor: String): Comments! comments(sort: String, cursor: String): Comments!
newComments(rootId: ID, after: Date): Comments!
path: String path: String
position: Int position: Int
prior: Int prior: Int

View File

@ -28,6 +28,7 @@ import LinkToContext from './link-to-context'
import Boost from './boost-button' import Boost from './boost-button'
import { gql, useApolloClient } from '@apollo/client' import { gql, useApolloClient } from '@apollo/client'
import classNames from 'classnames' import classNames from 'classnames'
import { ShowNewComments } from './show-new-comments'
function Parent ({ item, rootText }) { function Parent ({ item, rootText }) {
const root = useRoot() const root = useRoot()
@ -260,6 +261,11 @@ export default function Comment ({
: !noReply && : !noReply &&
<Reply depth={depth + 1} item={item} replyOpen={replyOpen} onCancelQuote={cancelQuote} onQuoteReply={quoteReply} quote={quote}> <Reply depth={depth + 1} item={item} replyOpen={replyOpen} onCancelQuote={cancelQuote} onQuoteReply={quoteReply} quote={quote}>
{root.bounty && !bountyPaid && <PayBounty item={item} />} {root.bounty && !bountyPaid && <PayBounty item={item} />}
{item.newComments?.length > 0 && (
<div className='ms-auto'>
<ShowNewComments item={item} depth={depth} />
</div>
)}
</Reply>} </Reply>}
{children} {children}
<div className={styles.comments}> <div className={styles.comments}>
@ -304,8 +310,9 @@ function ReplyOnAnotherPage ({ item }) {
} }
return ( return (
<Link href={`/items/${rootId}?commentId=${item.id}`} as={`/items/${rootId}`} className='d-block pb-2 fw-bold text-muted'> <Link href={`/items/${rootId}?commentId=${item.id}`} as={`/items/${rootId}`} className='pb-2 fw-bold d-flex align-items-center gap-2 text-muted'>
{text} {text}
{item.newComments?.length > 0 && <div className={styles.newCommentDot} />}
</Link> </Link>
) )
} }

View File

@ -136,3 +136,26 @@
.comment:has(.comment) + .comment{ .comment:has(.comment) + .comment{
padding-top: .5rem; 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: #FADA5E;
opacity: 0.7;
}
50% {
background-color: #F6911D;
opacity: 1;
}
100% {
background-color: #FADA5E;
opacity: 0.7;
}
}

View File

@ -8,6 +8,8 @@ import { defaultCommentSort } from '@/lib/item'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import MoreFooter from './more-footer' import MoreFooter from './more-footer'
import { FULL_COMMENTS_THRESHOLD } from '@/lib/constants' import { FULL_COMMENTS_THRESHOLD } from '@/lib/constants'
import useLiveComments from './use-live-comments'
import { ShowNewComments } from './show-new-comments'
export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, commentSats }) { export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, commentSats }) {
const router = useRouter() const router = useRouter()
@ -64,14 +66,17 @@ export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, comm
export default function Comments ({ export default function Comments ({
parentId, pinned, bio, parentCreatedAt, parentId, pinned, bio, parentCreatedAt,
commentSats, comments, commentsCursor, fetchMoreComments, ncomments, ...props commentSats, comments, commentsCursor, fetchMoreComments, ncomments, newComments, lastCommentAt, item, ...props
}) { }) {
const router = useRouter() const router = useRouter()
// fetch new comments that arrived after the lastCommentAt, and update the item.newComments field in cache
useLiveComments(parentId, lastCommentAt || parentCreatedAt, router.query.sort)
const pins = useMemo(() => comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position), [comments]) const pins = useMemo(() => comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position), [comments])
return ( return (
<> <>
<ShowNewComments item={item} sort={router.query.sort} />
{comments?.length > 0 {comments?.length > 0
? <CommentsHeader ? <CommentsHeader
commentSats={commentSats} parentCreatedAt={parentCreatedAt} commentSats={commentSats} parentCreatedAt={parentCreatedAt}

View File

@ -191,6 +191,8 @@ export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props
comments={item.comments.comments} comments={item.comments.comments}
commentsCursor={item.comments.cursor} commentsCursor={item.comments.cursor}
fetchMoreComments={fetchMoreComments} fetchMoreComments={fetchMoreComments}
lastCommentAt={item.lastCommentAt}
item={item}
/> />
</div>} </div>}
</CarouselProvider> </CarouselProvider>

View File

@ -13,6 +13,7 @@ import { useRoot } from './root'
import { CREATE_COMMENT } from '@/fragments/paidAction' 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'
export default forwardRef(function Reply ({ export default forwardRef(function Reply ({
item, item,
@ -82,17 +83,7 @@ export default forwardRef(function Reply ({
const ancestors = item.path.split('.') const ancestors = item.path.split('.')
// update all ancestors // update all ancestors
ancestors.forEach(id => { updateAncestorsCommentCount(cache, ancestors, 1)
cache.modify({
id: `Item:${id}`,
fields: {
ncomments (existingNComments = 0) {
return existingNComments + 1
}
},
optimistic: true
})
})
// 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

View File

@ -0,0 +1,135 @@
import { useCallback } from 'react'
import { useApolloClient } from '@apollo/client'
import styles from './comment.module.css'
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
import { commentsViewedAfterComment } from '../lib/new-comments'
import {
itemUpdateQuery,
commentUpdateFragment,
getLatestCommentCreatedAt,
updateAncestorsCommentCount,
readCommentsFragment
} from '../lib/comments'
// filters out new comments, by id, that already exist in the item's comments
// preventing duplicate comments from being injected
function dedupeNewComments (newComments, comments = []) {
const existingIds = new Set(comments.map(c => c.id))
return newComments.filter(id => !existingIds.has(id))
}
// prepares and creates a new comments fragment for injection into the cache
// returns a function that can be used to update an item's comments field
function prepareComments ({ client, newComments }) {
return (data) => {
// count total comments being injected: each new comment + all their existing nested comments
let totalNComments = newComments.length
for (const comment of newComments) {
// add all nested comments (subtree) under this newly injected comment to the total
totalNComments += (comment.ncomments || 0)
}
// update all ancestors, but not the item itself
const ancestors = data.path.split('.').slice(0, -1)
updateAncestorsCommentCount(client.cache, ancestors, totalNComments)
// update commentsViewedAt with the most recent fresh new comment
// quirk: this is not the most recent comment, it's the most recent comment in the newComments array
// as such, the next visit will not outline other new comments that are older than this one.
const latestCommentCreatedAt = getLatestCommentCreatedAt(newComments, data.createdAt)
const rootId = data.path.split('.')[0]
commentsViewedAfterComment(rootId, latestCommentCreatedAt, totalNComments)
// return the updated item with the new comments injected
return {
...data,
comments: { ...data.comments, comments: [...newComments, ...(data.comments?.comments || [])] },
ncomments: data.ncomments + totalNComments,
newComments: []
}
}
}
// traverses all new comments and their children
// at each level, we can execute a callback giving the new comments and the item
function traverseNewComments (client, item, onLevel, currentDepth = 1) {
if (currentDepth > COMMENT_DEPTH_LIMIT) return
if (item.newComments && item.newComments.length > 0) {
const dedupedNewComments = dedupeNewComments(item.newComments, item.comments?.comments)
// being newComments an array of comment ids, we can get their latest version from the cache
// ensuring that we don't miss any new comments
const freshNewComments = dedupedNewComments.map(id => {
return readCommentsFragment(client, id)
}).filter(Boolean)
// passing currentDepth allows children of top level comments
// to be updated by the commentUpdateFragment
onLevel(freshNewComments, item, currentDepth)
for (const newComment of freshNewComments) {
traverseNewComments(client, newComment, onLevel, currentDepth + 1)
}
}
}
// recursively processes and displays all new comments and its children
// handles comment injection at each level, respecting depth limits
function injectNewComments (client, item, currentDepth, sort) {
traverseNewComments(client, item, (newComments, item, depth) => {
if (newComments.length > 0) {
const payload = prepareComments({ client, newComments })
// used to determine if by iterating through the new comments
// we are injecting topLevels (depth 0) or not
if (depth === 0) {
itemUpdateQuery(client, item.id, sort, payload)
} else {
commentUpdateFragment(client, item.id, payload)
}
}
}, currentDepth)
}
// counts all new comments for an item and its children
function countAllNewComments (client, item, currentDepth = 1) {
let totalNComments = 0
// count by traversing all new comments and their children
traverseNewComments(client, item, (newComments) => {
totalNComments += newComments.length
for (const newComment of newComments) {
totalNComments += newComment.ncomments || 0
}
}, currentDepth)
return totalNComments
}
// ShowNewComments is a component that dedupes, refreshes and injects newComments into the comments field
export function ShowNewComments ({ item, sort, depth = 0 }) {
const client = useApolloClient()
// recurse through all new comments and their children
const newCommentsCount = item.newComments?.length > 0 ? countAllNewComments(client, item, depth) : 0
const showNewComments = useCallback(() => {
// a top level comment doesn't have depth, we pass 0 to signify this
// other comments are injected from their depth
injectNewComments(client, item, depth, sort)
}, [client, sort, item, depth])
return (
<span
onClick={showNewComments}
className='fw-bold d-flex align-items-center gap-2 px-3 pointer'
style={{ visibility: newCommentsCount > 0 ? 'visible' : 'hidden' }}
>
{newCommentsCount > 1
? `${newCommentsCount} new comments`
: 'show new comment'}
<div className={styles.newCommentDot} />
</span>
)
}

View File

@ -0,0 +1,75 @@
import { useQuery, useApolloClient } from '@apollo/client'
import { SSR } from '../lib/constants'
import { GET_NEW_COMMENTS } from '../fragments/comments'
import { useEffect, useState } from 'react'
import { itemUpdateQuery, commentUpdateFragment, getLatestCommentCreatedAt } from '../lib/comments'
const POLL_INTERVAL = 1000 * 10 // 10 seconds
// merge new comment into item's newComments
// and prevent duplicates by checking if the comment is already in item's newComments
function mergeNewComment (item, newComment) {
const existingNewComments = item.newComments || []
// is the incoming new comment already in item's new comments?
if (existingNewComments.includes(newComment.id)) {
return item
}
return { ...item, newComments: [...existingNewComments, newComment.id] }
}
function cacheNewComments (client, rootId, newComments, sort) {
for (const newComment of newComments) {
const { parentId } = newComment
const topLevel = Number(parentId) === Number(rootId)
// if the comment is a top level comment, update the item
if (topLevel) {
// merge the new comment into the item's newComments field, checking for duplicates
itemUpdateQuery(client, rootId, sort, (data) => mergeNewComment(data, newComment))
} else {
// if the comment is a reply, update the parent comment
// merge the new comment into the parent comment's newComments field, checking for duplicates
commentUpdateFragment(client, parentId, (data) => mergeNewComment(data, newComment))
}
}
}
// useLiveComments fetches new comments under an item (rootId), that arrives after the latest comment createdAt
// and inserts them into the newComment client field of their parent comment/post.
export default function useLiveComments (rootId, after, sort) {
const latestKey = `liveCommentsLatest:${rootId}`
const client = useApolloClient()
const [latest, setLatest] = useState(() => {
// if we're on the client, get the latest timestamp from session storage, otherwise use the passed after timestamp
if (typeof window !== 'undefined') {
return window.sessionStorage.getItem(latestKey) || after
}
return after
})
const { data } = useQuery(GET_NEW_COMMENTS, SSR
? {}
: {
pollInterval: POLL_INTERVAL,
// only get comments newer than the passed latest timestamp
variables: { rootId, after: latest },
nextFetchPolicy: 'cache-and-network'
})
useEffect(() => {
if (!data?.newComments?.comments?.length) return
// merge and cache new comments in their parent comment/post
cacheNewComments(client, rootId, data.newComments.comments, sort)
// update latest timestamp to the latest comment created at
// save it to session storage, to persist between client-side navigations
const newLatest = getLatestCommentCreatedAt(data.newComments.comments, latest)
setLatest(newLatest)
if (typeof window !== 'undefined') {
window.sessionStorage.setItem(latestKey, newLatest)
}
}, [data, client, rootId, sort, latest])
}

View File

@ -47,6 +47,7 @@ export const COMMENT_FIELDS = gql`
otsHash otsHash
ncomments ncomments
nDirectComments nDirectComments
newComments @client
imgproxyUrls imgproxyUrls
rel rel
apiKey apiKey
@ -116,3 +117,53 @@ export const COMMENTS = gql`
} }
} }
}` }`
export const COMMENT_WITH_NEW_RECURSIVE = gql`
${COMMENT_FIELDS}
${COMMENTS}
fragment CommentWithNewRecursive on Item {
...CommentFields
comments {
comments {
...CommentsRecursive
}
}
newComments @client
}
`
export const COMMENT_WITH_NEW_LIMITED = gql`
${COMMENT_FIELDS}
fragment CommentWithNewLimited on Item {
...CommentFields
comments {
comments {
...CommentFields
}
}
newComments @client
}
`
export const COMMENT_WITH_NEW_MINIMAL = gql`
${COMMENT_FIELDS}
fragment CommentWithNewMinimal on Item {
...CommentFields
newComments @client
}
`
export const GET_NEW_COMMENTS = gql`
${COMMENTS}
query GetNewComments($rootId: ID, $after: Date) {
newComments(rootId: $rootId, after: $after) {
comments {
...CommentsRecursive
}
}
}
`

View File

@ -59,6 +59,7 @@ export const ITEM_FIELDS = gql`
bio bio
ncomments ncomments
nDirectComments nDirectComments
newComments @client
commentSats commentSats
commentCredits commentCredits
lastCommentAt lastCommentAt

View File

@ -323,6 +323,11 @@ function getClient (uri) {
} }
} }
}, },
newComments: {
read (newComments) {
return newComments || []
}
},
meAnonSats: { meAnonSats: {
read (existingAmount, { readField }) { read (existingAmount, { readField }) {
if (SSR) return null if (SSR) return null

95
lib/comments.js Normal file
View File

@ -0,0 +1,95 @@
import { COMMENT_WITH_NEW_RECURSIVE, COMMENT_WITH_NEW_LIMITED, COMMENT_WITH_NEW_MINIMAL } from '../fragments/comments'
import { ITEM_FULL } from '../fragments/items'
// updates the ncomments field of all ancestors of an item/comment in the cache
export function updateAncestorsCommentCount (cache, ancestors, increment) {
// update all ancestors
ancestors.forEach(id => {
cache.modify({
id: `Item:${id}`,
fields: {
ncomments (existingNComments = 0) {
return existingNComments + increment
}
},
optimistic: true
})
})
}
// live comments - cache manipulations
// updates the item query in the cache
// this is used by live comments to update a top level item's newComments field
export function itemUpdateQuery (client, id, sort, fn) {
client.cache.updateQuery({
query: ITEM_FULL,
// updateQuery needs the correct variables to update the correct item
// the Item query might have the router.query.sort in the variables, so we need to pass it in if it exists
variables: sort ? { id, sort } : { id }
}, (data) => {
if (!data) return data
return { item: fn(data.item) }
})
}
// updates a comment fragment in the cache, with fallbacks for comments lacking CommentsRecursive or Comments altogether
export function commentUpdateFragment (client, id, fn) {
let result = client.cache.updateFragment({
id: `Item:${id}`,
fragment: COMMENT_WITH_NEW_RECURSIVE,
fragmentName: 'CommentWithNewRecursive'
}, (data) => {
if (!data) return data
return fn(data)
})
// sometimes comments can start to reach their depth limit, and lack adherence to the CommentsRecursive fragment
// for this reason, we update the fragment with a limited version that only includes the CommentFields fragment
if (!result) {
result = client.cache.updateFragment({
id: `Item:${id}`,
fragment: COMMENT_WITH_NEW_LIMITED,
fragmentName: 'CommentWithNewLimited'
}, (data) => {
if (!data) return data
return fn(data)
})
}
// at the deepest level, the comment can't have any children, here we update only the newComments field.
if (!result) {
result = client.cache.updateFragment({
id: `Item:${id}`,
fragment: COMMENT_WITH_NEW_MINIMAL,
fragmentName: 'CommentWithNewMinimal'
}, (data) => {
if (!data) return data
return fn(data)
})
}
return result
}
// reads a nested comments fragment from the cache
// this is used to read a comment and its children comments
// it has a fallback for comments nearing the depth limit, that lack the CommentsRecursive fragment
export function readCommentsFragment (client, id) {
return client.cache.readFragment({
id: `Item:${id}`,
fragment: COMMENT_WITH_NEW_RECURSIVE,
fragmentName: 'CommentWithNewRecursive'
}) || client.cache.readFragment({
id: `Item:${id}`,
fragment: COMMENT_WITH_NEW_LIMITED,
fragmentName: 'CommentWithNewLimited'
})
}
// finds the most recent createdAt timestamp from an array of comments
export function getLatestCommentCreatedAt (comments, latest) {
return comments.reduce(
(max, { createdAt }) => (createdAt > max ? createdAt : max),
latest
)
}

View File

@ -16,9 +16,9 @@ export function commentsViewedNum (itemId) {
return Number(window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${itemId}`)) return Number(window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${itemId}`))
} }
export function commentsViewedAfterComment (rootId, createdAt) { export function commentsViewedAfterComment (rootId, createdAt, ncomments = 1) {
window.localStorage.setItem(`${COMMENTS_VIEW_PREFIX}:${rootId}`, new Date(createdAt).getTime()) window.localStorage.setItem(`${COMMENTS_VIEW_PREFIX}:${rootId}`, new Date(createdAt).getTime())
window.localStorage.setItem(`${COMMENTS_NUM_PREFIX}:${rootId}`, commentsViewedNum(rootId) + 1) window.localStorage.setItem(`${COMMENTS_NUM_PREFIX}:${rootId}`, commentsViewedNum(rootId) + ncomments)
} }
export function newComments (item) { export function newComments (item) {