subtle highlight of new comments

This commit is contained in:
keyan 2023-08-06 14:18:40 -05:00
parent ea1e31c6ee
commit 1efc17fcc2
5 changed files with 48 additions and 7 deletions

View File

@ -120,6 +120,14 @@ export default function Comment ({
} }
}, [item.id, router.query.commentId]) }, [item.id, router.query.commentId])
useEffect(() => {
if (router.query.commentsViewedAt &&
me?.id !== item.user?.id &&
new Date(item.createdAt).getTime() > router.query.commentsViewedAt) {
ref.current.classList.add('outline-new-comment')
}
}, [item.id])
const bottomedOut = depth === COMMENT_DEPTH_LIMIT const bottomedOut = depth === COMMENT_DEPTH_LIMIT
const op = root.user.name === item.user.name const op = root.user.name === item.user.name
const bountyPaid = root.bountyPaidTo?.includes(Number(item.id)) const bountyPaid = root.bountyPaidTo?.includes(Number(item.id))
@ -127,6 +135,7 @@ export default function Comment ({
return ( return (
<div <div
ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse === 'yep' ? styles.collapsed : ''}`} ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse === 'yep' ? styles.collapsed : ''}`}
onMouseEnter={() => ref.current.classList.remove('outline-new-comment')}
> >
<div className={`${itemStyles.item} ${styles.item}`}> <div className={`${itemStyles.item} ${styles.item}`}>
{item.meDontLike {item.meDontLike

View File

@ -5,7 +5,7 @@ import Badge from 'react-bootstrap/Badge'
import Dropdown from 'react-bootstrap/Dropdown' import Dropdown from 'react-bootstrap/Dropdown'
import Countdown from './countdown' import Countdown from './countdown'
import { abbrNum } from '../lib/format' import { abbrNum } from '../lib/format'
import { newComments } from '../lib/new-comments' import { newComments, commentsViewedAt } from '../lib/new-comments'
import { timeSince } from '../lib/time' import { timeSince } from '../lib/time'
import CowboyHat from './cowboy-hat' import CowboyHat from './cowboy-hat'
import { DeleteDropdownItem } from './delete' import { DeleteDropdownItem } from './delete'
@ -42,7 +42,17 @@ export default function ItemInfo ({ item, pendingSats, full, commentsText, class
<span>{abbrNum(item.boost)} boost</span> <span>{abbrNum(item.boost)} boost</span>
<span> \ </span> <span> \ </span>
</>} </>}
<Link href={`/items/${item.id}`} title={`${item.commentSats} sats`} className='text-reset position-relative'> <Link
href={`/items/${item.id}`} onClick={(e) => {
const viewedAt = commentsViewedAt(item)
if (viewedAt) {
e.preventDefault()
router.push(
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
`/items/${item.id}`)
}
}} title={`${item.commentSats} sats`} className='text-reset position-relative'
>
{item.ncomments} {commentsText || 'comments'} {item.ncomments} {commentsText || 'comments'}
{hasNewComments && {hasNewComments &&
<span className={styles.notification}> <span className={styles.notification}>

View File

@ -12,6 +12,8 @@ import Flag from '../svgs/flag-fill.svg'
import ImageIcon from '../svgs/image-fill.svg' import ImageIcon from '../svgs/image-fill.svg'
import { abbrNum } from '../lib/format' import { abbrNum } from '../lib/format'
import ItemInfo from './item-info' import ItemInfo from './item-info'
import { commentsViewedAt } from '../lib/new-comments'
import { useRouter } from 'next/router'
export function SearchTitle ({ title }) { export function SearchTitle ({ title }) {
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => { return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
@ -21,6 +23,7 @@ export function SearchTitle ({ title }) {
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments }) { export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments }) {
const titleRef = useRef() const titleRef = useRef()
const router = useRouter()
const [pendingSats, setPendingSats] = useState(0) const [pendingSats, setPendingSats] = useState(0)
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL) const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
@ -39,7 +42,18 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
: item.meDontLike ? <Flag width={24} height={24} className={styles.dontLike} /> : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />} : item.meDontLike ? <Flag width={24} height={24} className={styles.dontLike} /> : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />}
<div className={styles.hunk}> <div className={styles.hunk}>
<div className={`${styles.main} flex-wrap`}> <div className={`${styles.main} flex-wrap`}>
<Link href={`/items/${item.id}`} ref={titleRef} className={`${styles.title} text-reset me-2`}> <Link
href={`/items/${item.id}`}
onClick={(e) => {
const viewedAt = commentsViewedAt(item)
if (viewedAt) {
e.preventDefault()
router.push(
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
`/items/${item.id}`)
}
}} ref={titleRef} className={`${styles.title} text-reset me-2`}
>
{item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title} {item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
{item.pollCost && <span className={styles.icon}> <PollIcon className='fill-grey ms-1' height={14} width={14} /></span>} {item.pollCost && <span className={styles.icon}> <PollIcon className='fill-grey ms-1' height={14} width={14} /></span>}
{item.bounty > 0 && {item.bounty > 0 &&

View File

@ -14,13 +14,17 @@ export function commentsViewedAfterComment (rootId, createdAt) {
window.localStorage.setItem(`${COMMENTS_NUM_PREFIX}:${rootId}`, existingRootComments + 1) window.localStorage.setItem(`${COMMENTS_NUM_PREFIX}:${rootId}`, existingRootComments + 1)
} }
export function commentsViewedAt (item) {
return window.localStorage.getItem(`${COMMENTS_VIEW_PREFIX}:${item.id}`)
}
export function newComments (item) { export function newComments (item) {
if (!item.parentId) { if (!item.parentId) {
const commentsViewedAt = window.localStorage.getItem(`${COMMENTS_VIEW_PREFIX}:${item.id}`) const viewedAt = commentsViewedAt(item)
const commentsViewNum = window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${item.id}`) const viewNum = window.localStorage.getItem(`${COMMENTS_NUM_PREFIX}:${item.id}`)
if (commentsViewedAt && commentsViewNum) { if (viewedAt && viewNum) {
return commentsViewedAt < new Date(item.lastCommentAt).getTime() || commentsViewNum < item.ncomments return viewedAt < new Date(item.lastCommentAt).getTime() || viewNum < item.ncomments
} }
} }

View File

@ -688,6 +688,10 @@ div[contenteditable]:focus,
animation: outline 3s linear 1; animation: outline 3s linear 1;
} }
.outline-new-comment {
box-shadow: inset 0 0 1px 1px rgba(0, 123, 190, 0.35);
}
@keyframes spin { @keyframes spin {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);