* Fix duplicate comment on pessimistic creation - comment creation checks for comment's ID existence in cache - invoice.confirmedAt included in useCanEdit deps for anons live comments * switch to some as sets are not worth it * only check for duplicates if a pessimistic payment method has been used * default to empty array * add comment about side-effects * record ownership of an item to avoid injecting it via live comments * trigger check only if the incoming comment is ours, cleanup * correct conditions, correct comments, light cleanup * fix: add defensive condition to ownership recorder, better name * refactor: unified comment injection logic with deduplication, useCommentsView hook; revert sessionStorage-based fix * adjust live comments naming around the codebase * listen for hmac presence for anon edits * always return the injected comment createdAt to bump live comments * refactor: improve live comments hook readability - latest comment createdAt persistence helper - preserveScroll returns the returning value of the callback - compact conditional logic - refresh code comments - refresh naming - group constants - reorder imports * flat comment injection, fetch flat comments instead of the entire subtree that would've been deduplicated anyway, cleanup * always align new comment fragment to the comments query structure * generic useCommentsView hook * update comment counts if live injecting into fragments without comments field * fix: pass parentId, if a comment has a top level parent it always has the comments field * fix: update CommentsViewAt only if we actually injected a comment into cache * correct injectComment result usage * pass markViewedAt to further centralize side effects, remove live from Item server typedefs * fix: don't update counts for ancestors that are already up to date, update commentsViewedAt per batch not per comment * port: fix coalesce, useCommentsView hook and outline changes * update hmac field in cache on paid invoice, hmac as useCanEdit effect dependency * comments and light cleanup, update useCommentsView * efficient hasComments logic for live comments, establish a gql fragment * fix: typo on topLevel evaluation * limit extra evaluations to live comments scenarios * update comments * support live comments ncomments increments for anon view tracking
115 lines
4.4 KiB
JavaScript
115 lines
4.4 KiB
JavaScript
import { useEffect, useState, useCallback } from 'react'
|
|
import { useQuery, useApolloClient } from '@apollo/client'
|
|
import { SSR } from '../lib/constants'
|
|
import preserveScroll from './preserve-scroll'
|
|
import { GET_NEW_COMMENTS } from '../fragments/comments'
|
|
import { injectComment } from '../lib/comments'
|
|
import useCommentsView from './use-comments-view'
|
|
|
|
// live comments polling interval
|
|
const POLL_INTERVAL = 1000 * 5
|
|
// live comments toggle keys
|
|
const STORAGE_DISABLE_KEY = 'disableLiveComments'
|
|
const TOGGLE_EVENT = 'liveComments:toggle'
|
|
|
|
const readStoredLatest = (key, latest) => {
|
|
const stored = window.sessionStorage.getItem(key)
|
|
return stored && stored > latest ? stored : latest
|
|
}
|
|
|
|
// cache new comments and return the most recent timestamp between current latest and new comment
|
|
// regardless of whether the comments were injected or not
|
|
function cacheNewComments (cache, latest, itemId, newComments, markCommentViewedAt) {
|
|
let injected = 0
|
|
|
|
const injectedLatest = newComments.reduce((latestTimestamp, newComment) => {
|
|
const result = injectComment(cache, newComment, { live: true, rootId: itemId })
|
|
// if any comment was injected, increment injected
|
|
injected = result ? injected + 1 : injected
|
|
return new Date(newComment.createdAt) > new Date(latestTimestamp)
|
|
? newComment.createdAt
|
|
: latestTimestamp
|
|
}, latest)
|
|
|
|
if (injected > 0) {
|
|
markCommentViewedAt(injectedLatest, { ncomments: injected })
|
|
}
|
|
|
|
return injectedLatest
|
|
}
|
|
|
|
// fetches comments for an item that are newer than the latest comment createdAt (after),
|
|
// injects them into cache, and keeps scroll position stable.
|
|
export default function useLiveComments (itemId, after) {
|
|
const latestKey = `liveCommentsLatest:${itemId}`
|
|
const { cache } = useApolloClient()
|
|
const { markCommentViewedAt } = useCommentsView(itemId)
|
|
const [disableLiveComments] = useLiveCommentsToggle()
|
|
|
|
const [latest, setLatest] = useState(after)
|
|
const [initialized, setInitialized] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setLatest(readStoredLatest(latestKey, after))
|
|
// Apollo might update the cache before the page has fully rendered, causing reads of stale cached data
|
|
// this prevents GET_NEW_COMMENTS from producing results before the page has fully rendered
|
|
setInitialized(true)
|
|
}, [itemId, after])
|
|
|
|
const { data } = useQuery(GET_NEW_COMMENTS, {
|
|
pollInterval: POLL_INTERVAL,
|
|
// only get comments newer than the passed latest timestamp
|
|
variables: { itemId, after: latest },
|
|
nextFetchPolicy: 'cache-and-network',
|
|
skip: SSR || !initialized || disableLiveComments
|
|
})
|
|
|
|
useEffect(() => {
|
|
const newComments = data?.newComments?.comments
|
|
if (!newComments?.length) return
|
|
|
|
// directly inject new comments into the cache, preserving scroll position
|
|
// quirk: scroll is preserved even if we are not injecting new comments due to dedupe
|
|
const injectedLatest = preserveScroll(() => cacheNewComments(cache, latest, itemId, newComments, markCommentViewedAt))
|
|
|
|
// if we didn't process any newer comments, bail
|
|
if (new Date(injectedLatest).getTime() <= new Date(latest).getTime()) return
|
|
|
|
// update latest timestamp to the latest comment created at
|
|
// save it to session storage, to persist between client-side navigations
|
|
setLatest(injectedLatest)
|
|
window.sessionStorage.setItem(latestKey, injectedLatest)
|
|
}, [data, cache, itemId, latest, markCommentViewedAt])
|
|
}
|
|
|
|
export function useLiveCommentsToggle () {
|
|
const [disableLiveComments, setDisableLiveComments] = useState(false)
|
|
|
|
useEffect(() => {
|
|
// preference: local storage
|
|
const read = () => setDisableLiveComments(window.localStorage.getItem(STORAGE_DISABLE_KEY) === 'true')
|
|
read()
|
|
|
|
// update across tabs
|
|
const onStorage = e => { if (e.key === STORAGE_DISABLE_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_DISABLE_KEY) === 'true'
|
|
window.localStorage.setItem(STORAGE_DISABLE_KEY, !current)
|
|
// trigger local event to update this tab
|
|
window.dispatchEvent(new Event(TOGGLE_EVENT))
|
|
}, [])
|
|
|
|
return [disableLiveComments, toggle]
|
|
}
|