@@ -2,101 +2,97 @@ import { useQuery, useApolloClient } from '@apollo/client'
2
2
import { SSR } from '../lib/constants'
3
3
import { GET_NEW_COMMENTS , COMMENT_WITH_NEW } from '../fragments/comments'
4
4
import { ITEM_FULL } from '../fragments/items'
5
- import { useEffect , useState } from 'react'
5
+ import { useCallback , useEffect , useState , useRef } from 'react'
6
6
import styles from './comment.module.css'
7
7
8
8
const POLL_INTERVAL = 1000 * 10 // 10 seconds
9
- const ACTIVITY_TIMEOUT = 1000 * 60 * 30 // 30 minutes
10
- const ACTIVITY_CHECK_INTERVAL = 1000 * 60 // 1 minute
9
+
10
+ function itemUpdateQuery ( client , id , sort , fn ) {
11
+ client . cache . updateQuery ( {
12
+ query : ITEM_FULL ,
13
+ variables : sort === 'top' ? { id } : { id, sort }
14
+ } , ( data ) => fn ( data ) )
15
+ }
16
+
17
+ function commentUpdateFragment ( client , id , fn ) {
18
+ client . cache . updateFragment ( {
19
+ id : `Item:${ id } ` ,
20
+ fragment : COMMENT_WITH_NEW ,
21
+ fragmentName : 'CommentWithNew'
22
+ } , ( data ) => fn ( data ) )
23
+ }
11
24
12
25
export default function useLiveComments ( rootId , after , sort ) {
13
26
const client = useApolloClient ( )
14
27
const [ latest , setLatest ] = useState ( after )
15
- const [ polling , setPolling ] = useState ( true )
16
- const [ engagedAt , setEngagedAt ] = useState ( new Date ( ) )
17
-
18
- // reset engagedAt when polling is toggled
19
- useEffect ( ( ) => {
20
- if ( polling ) {
21
- setEngagedAt ( new Date ( ) )
22
- }
23
- } , [ polling ] )
24
-
25
- useEffect ( ( ) => {
26
- const checkActivity = ( ) => {
27
- const now = new Date ( )
28
- const timeSinceEngaged = now . getTime ( ) - engagedAt . getTime ( )
29
- const isActive = document . visibilityState === 'visible'
30
-
31
- // poll only if the user is active and has been active in the last 30 minutes
32
- if ( timeSinceEngaged < ACTIVITY_TIMEOUT ) {
33
- setPolling ( isActive )
34
- } else {
35
- setPolling ( false )
36
- }
37
- }
38
-
39
- // check activity every minute
40
- const interval = setInterval ( checkActivity , ACTIVITY_CHECK_INTERVAL )
41
- // check activity also on visibility change
42
- document . addEventListener ( 'visibilitychange' , checkActivity )
43
-
44
- return ( ) => {
45
- // cleanup
46
- document . removeEventListener ( 'visibilitychange' , checkActivity )
47
- clearInterval ( interval )
48
- }
49
- } , [ engagedAt ] )
28
+ const queuedCommentsRef = useRef ( [ ] )
50
29
51
30
const { data } = useQuery ( GET_NEW_COMMENTS , SSR
52
31
? { }
53
32
: {
54
- pollInterval : polling ? POLL_INTERVAL : null ,
33
+ pollInterval : POLL_INTERVAL ,
55
34
variables : { rootId, after : latest }
56
35
} )
57
36
58
37
useEffect ( ( ) => {
59
38
if ( ! data ?. newComments ) return
60
39
61
- cacheNewComments ( client , rootId , data . newComments . comments , sort )
62
- // check new comments created after the latest new comment
63
- setLatest ( prevLatest => getLatestCommentCreatedAt ( data . newComments . comments , prevLatest ) )
64
- } , [ data , client , rootId ] )
40
+ // live comments can be orphans if the parent comment is not in the cache
41
+ // queue them up and retry later, when the parent decides they want the children.
42
+ const allComments = [ ... queuedCommentsRef . current , ... data . newComments . comments ]
43
+ const { queuedComments } = cacheNewComments ( client , rootId , allComments , sort )
65
44
66
- return { polling, setPolling }
45
+ // keep the queued comments in the ref for the next poll
46
+ queuedCommentsRef . current = queuedComments
47
+
48
+ // update latest timestamp to the latest comment created at
49
+ setLatest ( prevLatest => getLatestCommentCreatedAt ( data . newComments . comments , prevLatest ) )
50
+ } , [ data , client , rootId , sort ] )
67
51
}
68
52
69
53
function cacheNewComments ( client , rootId , newComments , sort ) {
54
+ const queuedComments = [ ]
55
+
70
56
for ( const newComment of newComments ) {
57
+ console . log ( 'newComment' , newComment )
71
58
const { parentId } = newComment
72
59
const topLevel = Number ( parentId ) === Number ( rootId )
73
60
74
61
// if the comment is a top level comment, update the item
75
62
if ( topLevel ) {
76
- client . cache . updateQuery ( {
77
- query : ITEM_FULL ,
78
- variables : { id : rootId , sort } // TODO-LIVE: ok now item updates thanks to sort awareness, but please CLEAN THIS UP
79
- } , ( data ) => {
63
+ console . log ( 'topLevel' , topLevel )
64
+ itemUpdateQuery ( client , rootId , sort , ( data ) => {
80
65
if ( ! data ) return data
81
- // we return the entire item, not just the newComments
82
- return { item : mergeNewComments ( data ?. item , newComment ) }
66
+ return { item : mergeNewComment ( data ?. item , newComment ) }
83
67
} )
84
68
} else {
85
- // if the comment is a reply, update the parent comment
86
- client . cache . updateFragment ( {
69
+ // check if parent exists in cache before attempting update
70
+ const parentExists = client . cache . readFragment ( {
87
71
id : `Item:${ parentId } ` ,
88
72
fragment : COMMENT_WITH_NEW ,
89
73
fragmentName : 'CommentWithNew'
90
- } , ( data ) => {
91
- if ( ! data ) return data
92
- // here we return the parent comment with the new comment added
93
- return mergeNewComments ( data , newComment )
94
74
} )
75
+
76
+ if ( parentExists ) {
77
+ // if the comment is a reply, update the parent comment
78
+ console . log ( 'reply' , parentId )
79
+ commentUpdateFragment ( client , parentId , ( data ) => {
80
+ if ( ! data ) return data
81
+ return mergeNewComment ( data , newComment )
82
+ } )
83
+ } else {
84
+ // parent not in cache, queue for retry
85
+ queuedComments . push ( newComment )
86
+ }
95
87
}
96
88
}
89
+
90
+ return { queuedComments }
97
91
}
98
92
99
- function mergeNewComments ( item , newComment ) {
93
+ // merge new comment into item's newComments
94
+ // if the new comment is already in item's newComments or existing comments, do nothing
95
+ function mergeNewComment ( item , newComment ) {
100
96
const existingNewComments = item . newComments || [ ]
101
97
const existingComments = item . comments ?. comments || [ ]
102
98
@@ -122,42 +118,41 @@ function getLatestCommentCreatedAt (comments, latest) {
122
118
export function ShowNewComments ( { newComments = [ ] , itemId, topLevel = false , sort } ) {
123
119
const client = useApolloClient ( )
124
120
125
- const showNewComments = ( ) => {
121
+ const showNewComments = useCallback ( ( ) => {
126
122
if ( topLevel ) {
127
- client . cache . updateQuery ( {
128
- query : ITEM_FULL ,
129
- variables : { id : itemId , sort } // TODO-LIVE: ok now item updates thanks to sort awareness, but please CLEAN THIS UP
130
- } , ( data ) => {
123
+ console . log ( 'topLevel' , topLevel )
124
+ itemUpdateQuery ( client , itemId , sort , ( data ) => {
125
+ console . log ( 'data' , data )
131
126
if ( ! data ) return data
132
127
const { item } = data
133
128
134
129
return {
135
130
item : {
136
131
...item ,
137
- comments : dedupeComments ( item . comments , newComments ) ,
132
+ comments : injectComments ( item . comments , newComments ) ,
138
133
newComments : [ ]
139
134
}
140
135
}
141
136
} )
142
137
} else {
143
- client . cache . updateFragment ( {
144
- id : `Item:${ itemId } ` ,
145
- fragment : COMMENT_WITH_NEW ,
146
- fragmentName : 'CommentWithNew'
147
- } , ( data ) => {
138
+ console . log ( 'reply' , itemId )
139
+ commentUpdateFragment ( client , itemId , ( data ) => {
140
+ console . log ( 'data' , data )
148
141
if ( ! data ) return data
149
142
150
143
return {
151
144
...data ,
152
- comments : dedupeComments ( data . comments , newComments ) ,
145
+ comments : injectComments ( data . comments , newComments ) ,
153
146
newComments : [ ]
154
147
}
155
148
} )
156
149
}
157
- }
150
+ } , [ client , itemId , newComments , topLevel , sort ] )
158
151
159
- const dedupeComments = ( existingComments = [ ] , newComments = [ ] ) => {
160
- const existingIds = new Set ( existingComments . comments ?. map ( c => c . id ) )
152
+ // inject new comments into existing comments
153
+ // if the new comment is already in existing comments, do nothing
154
+ const injectComments = ( existingComments = [ ] , newComments = [ ] ) => {
155
+ const existingIds = new Set ( existingComments . comments . map ( c => c . id ) )
161
156
const filteredNew = newComments . filter ( c => ! existingIds . has ( c . id ) )
162
157
return {
163
158
...existingComments ,
0 commit comments