Skip to content

Commit 25e8e12

Browse files
committed
enhance: highlight new comments when shown; nit-fixes and cleanups
fixes: - sync local commentsViewedAt on comment injection, to avoid double outline on item re-visit - avoid double highlighting when client-side visiting an item and injecting a new comment cleanups: - move ShowNewComments functions to dedicated lib/comments.js - bust auto-show enhancement due to bad useEffect usage todos: - two recursive counts might be too much
1 parent 535afb8 commit 25e8e12

File tree

5 files changed

+102
-79
lines changed

5 files changed

+102
-79
lines changed

components/comment.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,16 @@ export default function Comment ({
143143
useEffect(() => {
144144
if (router.query.commentsViewedAt &&
145145
me?.id !== item.user?.id &&
146+
!item.newComments &&
146147
new Date(item.createdAt).getTime() > router.query.commentsViewedAt) {
147148
ref.current.classList.add('outline-new-comment')
148149
}
150+
151+
// an injected new comment has the newComments field, a different class is needed
152+
// to reliably outline every new comment
153+
if (item.newComments && me?.id !== item.user?.id) {
154+
ref.current.classList.add('outline-new-injected-comment')
155+
}
149156
}, [item.id])
150157

151158
const bottomedOut = depth === COMMENT_DEPTH_LIMIT || (item.comments?.comments.length === 0 && item.nDirectComments > 0)

components/show-new-comments.js

Lines changed: 2 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,11 @@
11
import { useCallback, useMemo } from 'react'
22
import { useApolloClient } from '@apollo/client'
3-
import { COMMENT_WITH_NEW_RECURSIVE } from '../fragments/comments'
43
import styles from './comment.module.css'
54
import { itemUpdateQuery, commentUpdateFragment } from './use-live-comments'
6-
import { updateAncestorsCommentCount } from '@/lib/comments'
7-
import { COMMENT_DEPTH_LIMIT } from '@/lib/constants'
5+
import { prepareComments, dedupeNewComments, collectAllNewComments, showAllNewCommentsRecursively } from '@/lib/comments'
86

9-
function prepareComments (client, newComments) {
10-
return (data) => {
11-
// newComments is an array of comment ids that allows us
12-
// to read the latest newComments from the cache, guaranteeing that we're not reading stale data
13-
const freshNewComments = newComments.map(id => {
14-
const fragment = client.cache.readFragment({
15-
id: `Item:${id}`,
16-
fragment: COMMENT_WITH_NEW_RECURSIVE,
17-
fragmentName: 'CommentWithNewRecursive'
18-
})
19-
20-
if (!fragment) {
21-
return null
22-
}
23-
24-
return fragment
25-
}).filter(Boolean)
26-
27-
// count the total number of new comments including its nested new comments
28-
let totalNComments = freshNewComments.length
29-
for (const comment of freshNewComments) {
30-
totalNComments += (comment.ncomments || 0)
31-
}
32-
33-
// update all ancestors, but not the item itself
34-
const ancestors = data.path.split('.').slice(0, -1)
35-
updateAncestorsCommentCount(client.cache, ancestors, totalNComments)
36-
37-
return {
38-
...data,
39-
comments: { ...data.comments, comments: [...freshNewComments, ...data.comments.comments] },
40-
ncomments: data.ncomments + totalNComments,
41-
newComments: []
42-
}
43-
}
44-
}
45-
46-
function showAllNewCommentsRecursively (client, item, currentDepth = 1) {
47-
// handle new comments at this item level
48-
if (item.newComments && item.newComments.length > 0) {
49-
const dedupedNewComments = dedupeNewComments(item.newComments, item.comments?.comments)
50-
51-
if (dedupedNewComments.length > 0) {
52-
const payload = prepareComments(client, dedupedNewComments)
53-
commentUpdateFragment(client, item.id, payload)
54-
}
55-
}
56-
57-
// recursively handle new comments in child comments
58-
if (item.comments?.comments && currentDepth < (COMMENT_DEPTH_LIMIT - 1)) {
59-
for (const childComment of item.comments.comments) {
60-
showAllNewCommentsRecursively(client, childComment, currentDepth + 1)
61-
}
62-
}
63-
}
64-
65-
function dedupeNewComments (newComments, comments) {
66-
const existingIds = new Set(comments.map(c => c.id))
67-
return newComments.filter(id => !existingIds.has(id))
68-
}
69-
70-
function collectAllNewComments (item, currentDepth = 1) {
71-
const allNewComments = [...(item.newComments || [])]
72-
if (item.comments?.comments && currentDepth < (COMMENT_DEPTH_LIMIT - 1)) {
73-
for (const comment of item.comments.comments) {
74-
console.log('comment', comment)
75-
console.log('currentDepth', currentDepth)
76-
allNewComments.push(...collectAllNewComments(comment, currentDepth + 1))
77-
}
78-
}
79-
return allNewComments
80-
}
81-
82-
export function ShowNewComments ({ topLevel, sort, comments, itemId, item, setHasNewComments, newComments = [], depth = 1 }) {
7+
export const ShowNewComments = ({ topLevel, sort, comments, itemId, item, setHasNewComments, newComments = [], depth = 1 }) => {
838
const client = useApolloClient()
84-
859
// if item is provided, we're showing all new comments for a thread,
8610
// otherwise we're showing new comments for a comment
8711
const isThread = !topLevel && item?.path.split('.').length === 2

components/use-live-comments.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ function mergeNewComment (item, newComment) {
127127
return { ...item, newComments: [...existingNewComments, newComment.id] }
128128
}
129129

130-
function getLatestCommentCreatedAt (comments, latest) {
130+
export function getLatestCommentCreatedAt (comments, latest) {
131131
return comments.reduce(
132132
(max, { createdAt }) => (createdAt > max ? createdAt : max),
133133
latest

lib/comments.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { COMMENT_DEPTH_LIMIT } from './constants'
2+
import { commentUpdateFragment, getLatestCommentCreatedAt } from '../components/use-live-comments'
3+
import { commentsViewedAfterComment } from './new-comments'
4+
import { COMMENT_WITH_NEW_RECURSIVE } from '../fragments/comments'
5+
16
export function updateAncestorsCommentCount (cache, ancestors, increment) {
27
// update all ancestors
38
ancestors.forEach(id => {
@@ -12,3 +17,82 @@ export function updateAncestorsCommentCount (cache, ancestors, increment) {
1217
})
1318
})
1419
}
20+
21+
// live comments - cache manipulations
22+
export function dedupeNewComments (newComments, comments) {
23+
const existingIds = new Set(comments.map(c => c.id))
24+
return newComments.filter(id => !existingIds.has(id))
25+
}
26+
27+
export function collectAllNewComments (item, currentDepth = 1) {
28+
const allNewComments = [...(item.newComments || [])]
29+
if (item.comments?.comments && currentDepth < (COMMENT_DEPTH_LIMIT - 1)) {
30+
for (const comment of item.comments.comments) {
31+
allNewComments.push(...collectAllNewComments(comment, currentDepth + 1))
32+
}
33+
}
34+
return allNewComments
35+
}
36+
37+
export 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 {
73+
...data,
74+
comments: { ...data.comments, comments: [...freshNewComments, ...data.comments.comments] },
75+
ncomments: data.ncomments + totalNComments,
76+
newComments: []
77+
}
78+
}
79+
}
80+
81+
export function showAllNewCommentsRecursively (client, item, currentDepth = 1) {
82+
// handle new comments at this item level
83+
if (item.newComments && item.newComments.length > 0) {
84+
const dedupedNewComments = dedupeNewComments(item.newComments, item.comments?.comments)
85+
86+
if (dedupedNewComments.length > 0) {
87+
const payload = prepareComments(client, dedupedNewComments)
88+
commentUpdateFragment(client, item.id, payload)
89+
}
90+
}
91+
92+
// recursively handle new comments in child comments
93+
if (item.comments?.comments && currentDepth < (COMMENT_DEPTH_LIMIT - 1)) {
94+
for (const childComment of item.comments.comments) {
95+
showAllNewCommentsRecursively(client, childComment, currentDepth + 1)
96+
}
97+
}
98+
}

styles/globals.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,10 +908,18 @@ div[contenteditable]:focus,
908908
box-shadow: inset 0 0 1px 1px rgba(0, 123, 190, 0.25);
909909
}
910910

911+
.outline-new-injected-comment {
912+
box-shadow: inset 0 0 1px 1px rgba(0, 123, 190, 0.25);
913+
}
914+
911915
.outline-new-comment.outline-new-comment-unset {
912916
box-shadow: none;
913917
}
914918

919+
.outline-new-injected-comment.outline-new-comment-unset {
920+
box-shadow: none;
921+
}
922+
915923
.outline-new-comment .outline-new-comment {
916924
box-shadow: none;
917925
}

0 commit comments

Comments
 (0)