@@ -7,6 +7,8 @@ import styles from './comment.module.css'
7
7
8
8
const POLL_INTERVAL = 1000 * 10 // 10 seconds
9
9
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.
10
12
export default function useLiveComments ( rootId , after , sort ) {
11
13
const client = useApolloClient ( )
12
14
const [ latest , setLatest ] = useState ( after )
@@ -16,17 +18,18 @@ export default function useLiveComments (rootId, after, sort) {
16
18
? { }
17
19
: {
18
20
pollInterval : POLL_INTERVAL ,
21
+ // only get comments newer than the passed latest timestamp
19
22
variables : { rootId, after : latest } ,
20
23
nextFetchPolicy : 'cache-and-network'
21
24
} )
22
25
23
26
useEffect ( ( ) => {
24
27
if ( ! data ?. newComments ) return
25
28
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 )
30
33
31
34
// keep the queued comments for the next poll
32
35
queue . current = queuedComments
@@ -40,13 +43,16 @@ export default function useLiveComments (rootId, after, sort) {
40
43
function itemUpdateQuery ( client , id , sort , fn ) {
41
44
client . cache . updateQuery ( {
42
45
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 }
44
49
} , ( data ) => {
45
50
if ( ! data ) return data
46
51
return { item : fn ( data . item ) }
47
52
} )
48
53
}
49
54
55
+ // update the newComments field of a nested comment fragment
50
56
function commentUpdateFragment ( client , id , fn ) {
51
57
client . cache . updateFragment ( {
52
58
id : `Item:${ id } ` ,
@@ -67,17 +73,19 @@ function cacheNewComments (client, rootId, newComments, sort) {
67
73
68
74
// if the comment is a top level comment, update the item
69
75
if ( topLevel ) {
76
+ // merge the new comment into the item's newComments field, checking for duplicates
70
77
itemUpdateQuery ( client , rootId , sort , ( data ) => mergeNewComment ( data , newComment ) )
71
78
} 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
73
81
const parentExists = client . cache . readFragment ( {
74
82
id : `Item:${ parentId } ` ,
75
83
fragment : COMMENT_WITH_NEW ,
76
84
fragmentName : 'CommentWithNew'
77
85
} )
78
86
79
87
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
81
89
commentUpdateFragment ( client , parentId , ( data ) => mergeNewComment ( data , newComment ) )
82
90
} else {
83
91
// parent not in cache, queue for retry
@@ -90,7 +98,7 @@ function cacheNewComments (client, rootId, newComments, sort) {
90
98
}
91
99
92
100
// 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
94
102
function mergeNewComment ( item , newComment ) {
95
103
const existingNewComments = item . newComments || [ ]
96
104
const existingComments = item . comments ?. comments || [ ]
@@ -103,7 +111,9 @@ function mergeNewComment (item, newComment) {
103
111
return { ...item , newComments : [ ...existingNewComments , newComment ] }
104
112
}
105
113
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
107
117
function dedupeComments ( existing = [ ] , incoming = [ ] ) {
108
118
const existingIds = new Set ( existing . map ( c => c . id ) )
109
119
return [ ...incoming . filter ( c => ! existingIds . has ( c . id ) ) , ...existing ]
@@ -121,18 +131,23 @@ function getLatestCommentCreatedAt (comments, latest) {
121
131
return new Date ( maxTimestamp ) . toISOString ( )
122
132
}
123
133
134
+ // ShowNewComments is a component that dedupes, refreshes and injects newComments into the comments field
124
135
export function ShowNewComments ( { newComments = [ ] , itemId, topLevel = false , sort } ) {
125
136
const client = useApolloClient ( )
126
137
127
138
const showNewComments = useCallback ( ( ) => {
128
139
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
130
144
const freshNewComments = newComments . map ( c => {
131
145
const fragment = client . cache . readFragment ( {
132
146
id : `Item:${ c . id } ` ,
133
147
fragment : COMMENT_WITH_NEW ,
134
148
fragmentName : 'CommentWithNew'
135
149
} )
150
+ // if the comment is not in the cache, return the original comment
136
151
return fragment || c
137
152
} )
138
153
@@ -155,7 +170,7 @@ export function ShowNewComments ({ newComments = [], itemId, topLevel = false, s
155
170
onClick = { showNewComments }
156
171
className = { `${ topLevel && `d-block fw-bold ${ styles . comment } pb-2` } d-flex align-items-center gap-2 px-3 pointer` }
157
172
>
158
- show ( { newComments . length } ) new comments
173
+ { newComments . length > 0 ? ` $ {newComments . length } new comments` : 'new comment' }
159
174
< div className = { styles . newCommentDot } />
160
175
</ div >
161
176
)
0 commit comments