Skip to content

Commit ded6f5b

Browse files
committed
cleanup: better separation of concerns
1 parent e065f17 commit ded6f5b

File tree

3 files changed

+134
-135
lines changed

3 files changed

+134
-135
lines changed

components/show-new-comments.js

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,105 @@
11
import { useCallback, useMemo } from 'react'
22
import { useApolloClient } from '@apollo/client'
33
import styles from './comment.module.css'
4+
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
5+
import { COMMENT_WITH_NEW_RECURSIVE } from '../fragments/comments'
6+
import { commentsViewedAfterComment } from '../lib/new-comments'
47
import {
58
itemUpdateQuery,
69
commentUpdateFragment,
7-
prepareComments,
8-
dedupeNewComments,
9-
collectAllNewComments,
10-
showAllNewCommentsRecursively
10+
getLatestCommentCreatedAt,
11+
updateAncestorsCommentCount
1112
} from '../lib/comments'
1213

14+
// filters out new comments, by id, that already exist in the item's comments
15+
// preventing duplicate comments from being injected
16+
function dedupeNewComments (newComments, comments) {
17+
console.log('dedupeNewComments', newComments, comments)
18+
const existingIds = new Set(comments.map(c => c.id))
19+
return newComments.filter(id => !existingIds.has(id))
20+
}
21+
22+
// recursively collects all new comments from an item and its children
23+
// by respecting the depth limit, we avoid collecting new comments to inject in places
24+
// that are too deep in the tree
25+
function collectAllNewComments (item, currentDepth = 1) {
26+
const allNewComments = [...(item.newComments || [])]
27+
if (item.comments?.comments && currentDepth < (COMMENT_DEPTH_LIMIT - 1)) {
28+
for (const comment of item.comments.comments) {
29+
allNewComments.push(...collectAllNewComments(comment, currentDepth + 1))
30+
}
31+
}
32+
return allNewComments
33+
}
34+
35+
// prepares and creates a new comments fragment for injection into the cache
36+
// returns a function that can be used to update an item's comments field
37+
function prepareComments (client, newComments) {
38+
return (data) => {
39+
// newComments is an array of comment ids that allows us
40+
// to read the latest newComments from the cache, guaranteeing that we're not reading stale data
41+
const freshNewComments = newComments.map(id => {
42+
const fragment = client.cache.readFragment({
43+
id: `Item:${id}`,
44+
fragment: COMMENT_WITH_NEW_RECURSIVE,
45+
fragmentName: 'CommentWithNewRecursive'
46+
})
47+
48+
if (!fragment) {
49+
return null
50+
}
51+
52+
return fragment
53+
}).filter(Boolean)
54+
55+
// count the total number of new comments including its nested new comments
56+
let totalNComments = freshNewComments.length
57+
for (const comment of freshNewComments) {
58+
totalNComments += (comment.ncomments || 0)
59+
}
60+
61+
// update all ancestors, but not the item itself
62+
const ancestors = data.path.split('.').slice(0, -1)
63+
updateAncestorsCommentCount(client.cache, ancestors, totalNComments)
64+
65+
// update commentsViewedAt with the most recent fresh new comment
66+
// quirk: this is not the most recent comment, it's the most recent comment in the freshNewComments array
67+
// as such, the next visit will not outline other new comments that have not been injected yet
68+
const latestCommentCreatedAt = getLatestCommentCreatedAt(freshNewComments, data.createdAt)
69+
const rootId = data.path.split('.')[0]
70+
commentsViewedAfterComment(rootId, latestCommentCreatedAt)
71+
72+
// return the updated item with the new comments injected
73+
return {
74+
...data,
75+
comments: { ...data.comments, comments: [...freshNewComments, ...data.comments.comments] },
76+
ncomments: data.ncomments + totalNComments,
77+
newComments: []
78+
}
79+
}
80+
}
81+
82+
// recursively processes and displays all new comments for a thread
83+
// handles comment injection at each level, respecting depth limits
84+
function showAllNewCommentsRecursively (client, item, currentDepth = 1) {
85+
// handle new comments at this item level
86+
if (item.newComments && item.newComments.length > 0) {
87+
const dedupedNewComments = dedupeNewComments(item.newComments, item.comments?.comments)
88+
89+
if (dedupedNewComments.length > 0) {
90+
const payload = prepareComments(client, dedupedNewComments)
91+
commentUpdateFragment(client, item.id, payload)
92+
}
93+
}
94+
95+
// recursively handle new comments in child comments
96+
if (item.comments?.comments && currentDepth < (COMMENT_DEPTH_LIMIT - 1)) {
97+
for (const childComment of item.comments.comments) {
98+
showAllNewCommentsRecursively(client, childComment, currentDepth + 1)
99+
}
100+
}
101+
}
102+
13103
export const ShowNewComments = ({ topLevel, sort, comments, itemId, item, setHasNewComments, newComments = [], depth = 1 }) => {
14104
const client = useApolloClient()
15105
// if item is provided, we're showing all new comments for a thread,

components/use-live-comments.js

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,46 @@ import { itemUpdateQuery, commentUpdateFragment, getLatestCommentCreatedAt } fro
66

77
const POLL_INTERVAL = 1000 * 10 // 10 seconds
88

9+
// merge new comment into item's newComments
10+
// and prevent duplicates by checking if the comment is already in item's newComments or existing comments
11+
function mergeNewComment (item, newComment) {
12+
const existingNewComments = item.newComments || []
13+
const existingComments = item.comments?.comments || []
14+
15+
// is the incoming new comment already in item's new comments or existing comments?
16+
if (existingNewComments.includes(newComment.id) || existingComments.some(c => c.id === newComment.id)) {
17+
return item
18+
}
19+
20+
return { ...item, newComments: [...existingNewComments, newComment.id] }
21+
}
22+
23+
function cacheNewComments (client, rootId, newComments, sort) {
24+
const queuedComments = []
25+
26+
for (const newComment of newComments) {
27+
const { parentId } = newComment
28+
const topLevel = Number(parentId) === Number(rootId)
29+
30+
// if the comment is a top level comment, update the item
31+
if (topLevel) {
32+
// merge the new comment into the item's newComments field, checking for duplicates
33+
itemUpdateQuery(client, rootId, sort, (data) => mergeNewComment(data, newComment))
34+
} else {
35+
// if the comment is a reply, update the parent comment
36+
// merge the new comment into the parent comment's newComments field, checking for duplicates
37+
const result = commentUpdateFragment(client, parentId, (data) => mergeNewComment(data, newComment))
38+
39+
if (!result) {
40+
// parent not in cache, queue for retry
41+
queuedComments.push(newComment)
42+
}
43+
}
44+
}
45+
46+
return { queuedComments }
47+
}
48+
949
// useLiveComments fetches new comments under an item (rootId), that arrives after the latest comment createdAt
1050
// and inserts them into the newComment client field of their parent comment/post.
1151
export default function useLiveComments (rootId, after, sort, setHasNewComments) {
@@ -46,43 +86,3 @@ export default function useLiveComments (rootId, after, sort, setHasNewComments)
4686
}
4787
}, [])
4888
}
49-
50-
function cacheNewComments (client, rootId, newComments, sort) {
51-
const queuedComments = []
52-
53-
for (const newComment of newComments) {
54-
const { parentId } = newComment
55-
const topLevel = Number(parentId) === Number(rootId)
56-
57-
// if the comment is a top level comment, update the item
58-
if (topLevel) {
59-
// merge the new comment into the item's newComments field, checking for duplicates
60-
itemUpdateQuery(client, rootId, sort, (data) => mergeNewComment(data, newComment))
61-
} else {
62-
// if the comment is a reply, update the parent comment
63-
// merge the new comment into the parent comment's newComments field, checking for duplicates
64-
const result = commentUpdateFragment(client, parentId, (data) => mergeNewComment(data, newComment))
65-
66-
if (!result) {
67-
// parent not in cache, queue for retry
68-
queuedComments.push(newComment)
69-
}
70-
}
71-
}
72-
73-
return { queuedComments }
74-
}
75-
76-
// merge new comment into item's newComments
77-
// and prevent duplicates by checking if the comment is already in item's newComments or existing comments
78-
function mergeNewComment (item, newComment) {
79-
const existingNewComments = item.newComments || []
80-
const existingComments = item.comments?.comments || []
81-
82-
// is the incoming new comment already in item's new comments or existing comments?
83-
if (existingNewComments.includes(newComment.id) || existingComments.some(c => c.id === newComment.id)) {
84-
return item
85-
}
86-
87-
return { ...item, newComments: [...existingNewComments, newComment.id] }
88-
}

lib/comments.js

Lines changed: 0 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { COMMENT_DEPTH_LIMIT } from './constants'
2-
import { commentsViewedAfterComment } from './new-comments'
31
import { COMMENT_WITH_NEW_RECURSIVE, COMMENT_WITH_NEW_LIMITED } from '../fragments/comments'
42
import { ITEM_FULL } from '../fragments/items'
53

@@ -61,95 +59,6 @@ export function commentUpdateFragment (client, id, fn) {
6159
return result
6260
}
6361

64-
// filters out new comments, by id, that already exist in the item's comments
65-
// preventing duplicate comments from being injected
66-
export function dedupeNewComments (newComments, comments) {
67-
console.log('dedupeNewComments', newComments, comments)
68-
const existingIds = new Set(comments.map(c => c.id))
69-
return newComments.filter(id => !existingIds.has(id))
70-
}
71-
72-
// recursively collects all new comments from an item and its children
73-
// by respecting the depth limit, we avoid collecting new comments to inject in places
74-
// that are too deep in the tree
75-
export function collectAllNewComments (item, currentDepth = 1) {
76-
const allNewComments = [...(item.newComments || [])]
77-
if (item.comments?.comments && currentDepth < (COMMENT_DEPTH_LIMIT - 1)) {
78-
for (const comment of item.comments.comments) {
79-
allNewComments.push(...collectAllNewComments(comment, currentDepth + 1))
80-
}
81-
}
82-
return allNewComments
83-
}
84-
85-
// prepares and creates a new comments fragment for injection into the cache
86-
// returns a function that can be used to update an item's comments field
87-
export function prepareComments (client, newComments) {
88-
return (data) => {
89-
// newComments is an array of comment ids that allows us
90-
// to read the latest newComments from the cache, guaranteeing that we're not reading stale data
91-
const freshNewComments = newComments.map(id => {
92-
const fragment = client.cache.readFragment({
93-
id: `Item:${id}`,
94-
fragment: COMMENT_WITH_NEW_RECURSIVE,
95-
fragmentName: 'CommentWithNewRecursive'
96-
})
97-
98-
if (!fragment) {
99-
return null
100-
}
101-
102-
return fragment
103-
}).filter(Boolean)
104-
105-
// count the total number of new comments including its nested new comments
106-
let totalNComments = freshNewComments.length
107-
for (const comment of freshNewComments) {
108-
totalNComments += (comment.ncomments || 0)
109-
}
110-
111-
// update all ancestors, but not the item itself
112-
const ancestors = data.path.split('.').slice(0, -1)
113-
updateAncestorsCommentCount(client.cache, ancestors, totalNComments)
114-
115-
// update commentsViewedAt with the most recent fresh new comment
116-
// quirk: this is not the most recent comment, it's the most recent comment in the freshNewComments array
117-
// as such, the next visit will not outline other new comments that have not been injected yet
118-
const latestCommentCreatedAt = getLatestCommentCreatedAt(freshNewComments, data.createdAt)
119-
const rootId = data.path.split('.')[0]
120-
commentsViewedAfterComment(rootId, latestCommentCreatedAt)
121-
122-
// return the updated item with the new comments injected
123-
return {
124-
...data,
125-
comments: { ...data.comments, comments: [...freshNewComments, ...data.comments.comments] },
126-
ncomments: data.ncomments + totalNComments,
127-
newComments: []
128-
}
129-
}
130-
}
131-
132-
// recursively processes and displays all new comments for a thread
133-
// handles comment injection at each level, respecting depth limits
134-
export function showAllNewCommentsRecursively (client, item, currentDepth = 1) {
135-
// handle new comments at this item level
136-
if (item.newComments && item.newComments.length > 0) {
137-
const dedupedNewComments = dedupeNewComments(item.newComments, item.comments?.comments)
138-
139-
if (dedupedNewComments.length > 0) {
140-
const payload = prepareComments(client, dedupedNewComments)
141-
commentUpdateFragment(client, item.id, payload)
142-
}
143-
}
144-
145-
// recursively handle new comments in child comments
146-
if (item.comments?.comments && currentDepth < (COMMENT_DEPTH_LIMIT - 1)) {
147-
for (const childComment of item.comments.comments) {
148-
showAllNewCommentsRecursively(client, childComment, currentDepth + 1)
149-
}
150-
}
151-
}
152-
15362
// finds the most recent createdAt timestamp from an array of comments
15463
export function getLatestCommentCreatedAt (comments, latest) {
15564
return comments.reduce(

0 commit comments

Comments
 (0)