Skip to content

Commit a29f9e3

Browse files
committed
cleanup: detailed comments and better ShowNewComment text
1 parent 6a93d2a commit a29f9e3

File tree

2 files changed

+29
-17
lines changed

2 files changed

+29
-17
lines changed

components/comments.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,8 @@ export default function Comments ({
6868
commentSats, comments, commentsCursor, fetchMoreComments, ncomments, newComments, lastCommentAt, ...props
6969
}) {
7070
const router = useRouter()
71-
// TODO-LIVE: ok now item updates thanks to sort awareness, but please CLEAN THIS UP
72-
const sort = router.query.sort || defaultCommentSort(pinned, bio, parentCreatedAt)
73-
// update item.newComments in cache
74-
useLiveComments(parentId, lastCommentAt || parentCreatedAt, sort)
71+
// fetch new comments that arrived after the lastCommentAt, and update the item.newComments field in cache
72+
useLiveComments(parentId, lastCommentAt || parentCreatedAt, router.query.sort)
7573

7674
const pins = useMemo(() => comments?.filter(({ position }) => !!position).sort((a, b) => a.position - b.position), [comments])
7775

@@ -94,8 +92,7 @@ export default function Comments ({
9492
/>
9593
: null}
9694
{newComments?.length > 0 && (
97-
// TODO-LIVE: ok now item updates thanks to sort awareness, but please CLEAN THIS UP
98-
<ShowNewComments topLevel newComments={newComments} itemId={parentId} sort={sort} />
95+
<ShowNewComments topLevel newComments={newComments} itemId={parentId} sort={router.query.sort} />
9996
)}
10097
{pins.map(item => (
10198
<Fragment key={item.id}>

components/use-live-comments.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import styles from './comment.module.css'
77

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

10+
// useLiveComments fetches new comments under an item (rootId), that arrives after the latest comment createdAt
11+
// and inserts them into the newComment client field of their parent comment/post.
1012
export default function useLiveComments (rootId, after, sort) {
1113
const client = useApolloClient()
1214
const [latest, setLatest] = useState(after)
@@ -16,17 +18,18 @@ export default function useLiveComments (rootId, after, sort) {
1618
? {}
1719
: {
1820
pollInterval: POLL_INTERVAL,
21+
// only get comments newer than the passed latest timestamp
1922
variables: { rootId, after: latest },
2023
nextFetchPolicy: 'cache-and-network'
2124
})
2225

2326
useEffect(() => {
2427
if (!data?.newComments) return
2528

26-
// live comments can be orphans if the parent comment is not in the cache
27-
// queue them up and retry later, when the parent decides they want the children.
28-
const allComments = [...queue.current, ...data.newComments.comments]
29-
const { queuedComments } = cacheNewComments(client, rootId, allComments, sort)
29+
// sometimes new comments can arrive as orphans because their parent might not be in the cache yet
30+
// queue them up, retry until the parent shows up.
31+
const newComments = [...data.newComments.comments, ...queue.current]
32+
const { queuedComments } = cacheNewComments(client, rootId, newComments, sort)
3033

3134
// keep the queued comments for the next poll
3235
queue.current = queuedComments
@@ -40,13 +43,16 @@ export default function useLiveComments (rootId, after, sort) {
4043
function itemUpdateQuery (client, id, sort, fn) {
4144
client.cache.updateQuery({
4245
query: ITEM_FULL,
43-
variables: sort === 'top' ? { id } : { id, sort }
46+
// updateQuery needs the correct variables to update the correct item
47+
// the Item query might have the router.query.sort in the variables, so we need to pass it in if it exists
48+
variables: sort ? { id, sort } : { id }
4449
}, (data) => {
4550
if (!data) return data
4651
return { item: fn(data.item) }
4752
})
4853
}
4954

55+
// update the newComments field of a nested comment fragment
5056
function commentUpdateFragment (client, id, fn) {
5157
client.cache.updateFragment({
5258
id: `Item:${id}`,
@@ -67,17 +73,19 @@ function cacheNewComments (client, rootId, newComments, sort) {
6773

6874
// if the comment is a top level comment, update the item
6975
if (topLevel) {
76+
// merge the new comment into the item's newComments field, checking for duplicates
7077
itemUpdateQuery(client, rootId, sort, (data) => mergeNewComment(data, newComment))
7178
} else {
72-
// check if parent exists in cache before attempting update
79+
// if the comment is a reply, update the parent comment
80+
// but first check if parent exists in cache before attempting update
7381
const parentExists = client.cache.readFragment({
7482
id: `Item:${parentId}`,
7583
fragment: COMMENT_WITH_NEW,
7684
fragmentName: 'CommentWithNew'
7785
})
7886

7987
if (parentExists) {
80-
// if the comment is a reply, update the parent comment
88+
// merge the new comment into the parent comment's newComments field, checking for duplicates
8189
commentUpdateFragment(client, parentId, (data) => mergeNewComment(data, newComment))
8290
} else {
8391
// parent not in cache, queue for retry
@@ -90,7 +98,7 @@ function cacheNewComments (client, rootId, newComments, sort) {
9098
}
9199

92100
// merge new comment into item's newComments
93-
// if the new comment is already in item's newComments or existing comments, do nothing
101+
// and prevent duplicates by checking if the comment is already in item's newComments or existing comments
94102
function mergeNewComment (item, newComment) {
95103
const existingNewComments = item.newComments || []
96104
const existingComments = item.comments?.comments || []
@@ -103,7 +111,9 @@ function mergeNewComment (item, newComment) {
103111
return { ...item, newComments: [...existingNewComments, newComment] }
104112
}
105113

106-
// dedupe comments by id
114+
// even though we already deduplicated comments during the newComments merge
115+
// refetches, client-side navigation, etc. can cause duplicates to appear
116+
// we'll make sure to deduplicate them here, by id
107117
function dedupeComments (existing = [], incoming = []) {
108118
const existingIds = new Set(existing.map(c => c.id))
109119
return [...incoming.filter(c => !existingIds.has(c.id)), ...existing]
@@ -121,18 +131,23 @@ function getLatestCommentCreatedAt (comments, latest) {
121131
return new Date(maxTimestamp).toISOString()
122132
}
123133

134+
// ShowNewComments is a component that dedupes, refreshes and injects newComments into the comments field
124135
export function ShowNewComments ({ newComments = [], itemId, topLevel = false, sort }) {
125136
const client = useApolloClient()
126137

127138
const showNewComments = useCallback(() => {
128139
const payload = (data) => {
129-
// fresh newComments
140+
// TODO: it might be sane to pass the cache ref to the ShowNewComments component
141+
// TODO: and use it to read the latest newComments from the cache
142+
// newComments can have themselves new comments between the time the button is clicked and the query is executed
143+
// so we need to read the latest newComments from the cache
130144
const freshNewComments = newComments.map(c => {
131145
const fragment = client.cache.readFragment({
132146
id: `Item:${c.id}`,
133147
fragment: COMMENT_WITH_NEW,
134148
fragmentName: 'CommentWithNew'
135149
})
150+
// if the comment is not in the cache, return the original comment
136151
return fragment || c
137152
})
138153

@@ -155,7 +170,7 @@ export function ShowNewComments ({ newComments = [], itemId, topLevel = false, s
155170
onClick={showNewComments}
156171
className={`${topLevel && `d-block fw-bold ${styles.comment} pb-2`} d-flex align-items-center gap-2 px-3 pointer`}
157172
>
158-
show ({newComments.length}) new comments
173+
{newComments.length > 0 ? `${newComments.length} new comments` : 'new comment'}
159174
<div className={styles.newCommentDot} />
160175
</div>
161176
)

0 commit comments

Comments
 (0)