live comments: toggle (#2421)

* enhance: toggle live comments on posts, default status set by user settings

* wip: toggle via mutation, footer placement

* chat icon on footer, consistent naming, perf tweaks

* update all tabs on toggle by dispatching events, correct icon, cleanup

cleanup:
- remove useless window checks
- use skip instead of conditional options
- correct naming

* update localstorage on user setting change

* revert disableLiveComments user setting

* avoid redundant setState and usage of maybe stale state

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
soxa 2025-08-26 16:39:09 +02:00 committed by GitHub
parent e46f4f01b2
commit 8517e7277c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 19 deletions

View File

@ -12,10 +12,13 @@ import No from '@/svgs/no.svg'
import Bolt from '@/svgs/bolt.svg'
import Amboss from '@/svgs/amboss.svg'
import Mempool from '@/svgs/bimi.svg'
import Live from '@/svgs/chat-unread-fill.svg'
import NoLive from '@/svgs/chat-off-fill.svg'
import Rewards from './footer-rewards'
import useDarkMode from './dark-mode'
import ActionTooltip from './action-tooltip'
import { useAnimationEnabled } from '@/components/animation'
import { useLiveCommentsToggle } from './use-live-comments'
const RssPopover = (
<Popover>
@ -147,8 +150,11 @@ export default function Footer ({ links = true }) {
const [animationEnabled, toggleAnimation] = useAnimationEnabled()
const [disableLiveComments, toggleLiveComments] = useLiveCommentsToggle()
const DarkModeIcon = darkMode ? Sun : Moon
const LnIcon = animationEnabled ? No : Bolt
const LiveIcon = disableLiveComments ? Live : NoLive
const version = process.env.NEXT_PUBLIC_COMMIT_HASH
@ -164,6 +170,9 @@ export default function Footer ({ links = true }) {
<ActionTooltip notForm overlayText={`${animationEnabled ? 'disable' : 'enable'} lightning animations`}>
<LnIcon onClick={toggleAnimation} width={20} height={20} className='ms-2 fill-grey theme' suppressHydrationWarning />
</ActionTooltip>
<ActionTooltip notForm overlayText={`${disableLiveComments ? 'enable' : 'disable'} live comments`}>
<LiveIcon onClick={toggleLiveComments} width={20} height={20} className='ms-2 fill-grey theme' suppressHydrationWarning />
</ActionTooltip>
</div>
<div className='mb-0' style={{ fontWeight: 500 }}>
<Rewards />

View File

@ -1,6 +1,6 @@
import preserveScroll from './preserve-scroll'
import { GET_NEW_COMMENTS } from '../fragments/comments'
import { useEffect, useState } from 'react'
import { useEffect, useState, useCallback } from 'react'
import { SSR, COMMENT_DEPTH_LIMIT } from '../lib/constants'
import { useQuery, useApolloClient } from '@apollo/client'
import { commentsViewedAfterComment } from '../lib/new-comments'
@ -81,17 +81,16 @@ function cacheNewComments (cache, rootId, newComments, sort) {
export default function useLiveComments (rootId, after, sort) {
const latestKey = `liveCommentsLatest:${rootId}`
const { cache } = useApolloClient()
const [disableLiveComments] = useLiveCommentsToggle()
const [latest, setLatest] = useState(after)
const [initialized, setInitialized] = useState(false)
useEffect(() => {
if (typeof window !== 'undefined') {
const storedLatest = window.sessionStorage.getItem(latestKey)
if (storedLatest && storedLatest > after) {
setLatest(storedLatest)
} else {
setLatest(after)
}
const storedLatest = window.sessionStorage.getItem(latestKey)
if (storedLatest && storedLatest > after) {
setLatest(storedLatest)
} else {
setLatest(after)
}
// Apollo might update the cache before the page has fully rendered, causing reads of stale cached data
@ -99,14 +98,13 @@ export default function useLiveComments (rootId, after, sort) {
setInitialized(true)
}, [after])
const { data } = useQuery(GET_NEW_COMMENTS, SSR || !initialized
? {}
: {
pollInterval: POLL_INTERVAL,
// only get comments newer than the passed latest timestamp
variables: { rootId, after: latest },
nextFetchPolicy: 'cache-and-network'
})
const { data } = useQuery(GET_NEW_COMMENTS, {
pollInterval: POLL_INTERVAL,
// only get comments newer than the passed latest timestamp
variables: { rootId, after: latest },
nextFetchPolicy: 'cache-and-network',
skip: SSR || !initialized || disableLiveComments
})
useEffect(() => {
if (!data?.newComments?.comments?.length) return
@ -119,8 +117,41 @@ export default function useLiveComments (rootId, after, sort) {
// save it to session storage, to persist between client-side navigations
const newLatest = getLatestCommentCreatedAt(data.newComments.comments, latest)
setLatest(newLatest)
if (typeof window !== 'undefined') {
window.sessionStorage.setItem(latestKey, newLatest)
}
window.sessionStorage.setItem(latestKey, newLatest)
}, [data, cache, rootId, sort, latest])
}
const STORAGE_KEY = 'disableLiveComments'
const TOGGLE_EVENT = 'liveComments:toggle'
export function useLiveCommentsToggle () {
const [disableLiveComments, setDisableLiveComments] = useState(false)
useEffect(() => {
// preference: local storage
const read = () => setDisableLiveComments(window.localStorage.getItem(STORAGE_KEY) === 'true')
read()
// update across tabs
const onStorage = e => { if (e.key === STORAGE_KEY) read() }
// update this tab
const onToggle = () => read()
window.addEventListener('storage', onStorage)
window.addEventListener(TOGGLE_EVENT, onToggle)
return () => {
window.removeEventListener('storage', onStorage)
window.removeEventListener(TOGGLE_EVENT, onToggle)
}
}, [])
const toggle = useCallback(() => {
const current = window.localStorage.getItem(STORAGE_KEY) === 'true'
window.localStorage.setItem(STORAGE_KEY, !current)
// trigger local event to update this tab
window.dispatchEvent(new Event(TOGGLE_EVENT))
}, [disableLiveComments])
return [disableLiveComments, toggle]
}

1
svgs/chat-off-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2.80777 1.39337L22.6068 21.1924L21.1925 22.6066L17.5846 18.9994L6.45516 19L2.00016 22.5V3.99997C2.00016 3.83067 2.04223 3.6712 2.11649 3.53143L1.39355 2.80759L2.80777 1.39337ZM21.0002 2.99997C21.5524 2.99997 22.0002 3.44769 22.0002 3.99997V17.785L7.21416 2.99997H21.0002Z"></path></svg>

After

Width:  |  Height:  |  Size: 376 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21 7C22.6569 7 24 5.65685 24 4C24 2.34315 22.6569 1 21 1C19.3431 1 18 2.34315 18 4C18 5.65685 19.3431 7 21 7ZM21 9C21.3425 9 21.6769 8.96557 22 8.89998V18C22 18.5523 21.5523 19 21 19H6.45455L2 22.5V4C2 3.44772 2.44772 3 3 3H16.1C16.0344 3.32311 16 3.65753 16 4C16 6.76142 18.2386 9 21 9Z"></path></svg>

After

Width:  |  Height:  |  Size: 392 B