overleaf-cep/services/web/frontend/js/features/ide-react/hooks/use-status-favicon.ts
Antoine Clausse 2d5a3efc12 [web] Add compilation indicator favicon (#25990)
* Import changes from Hackathon

https://github.com/overleaf/internal/pull/24501

* Update compile status: allow errors

* Update favicons. Use the ones from Figma

* Optimize and reuse path from favicon.svg

* Clear status favicon after 5s on active tab

* Rename hook from useCompileNotification to useStatusFavicon

* Add tests

* Revert changes to favicon.svg

* Query favicon on document.head

GitOrigin-RevId: 3972b1981abaf6c80273e0ed5b1bc05eb51bd689
2025-06-24 08:05:15 +00:00

75 lines
2.3 KiB
TypeScript

import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useEffect, useState } from 'react'
import usePreviousValue from '@/shared/hooks/use-previous-value'
const RESET_AFTER_MS = 5_000
const COMPILE_ICONS = {
ERROR: '/favicon-error.svg',
COMPILING: '/favicon-compiling.svg',
COMPILED: '/favicon-compiled.svg',
UNCOMPILED: '/favicon.svg',
}
type CompileStatus = keyof typeof COMPILE_ICONS
const useCompileStatus = (): CompileStatus => {
const compileContext = useCompileContext()
if (compileContext.uncompiled) return 'UNCOMPILED'
if (compileContext.compiling) return 'COMPILING'
if (compileContext.error) return 'ERROR'
return 'COMPILED'
}
const removeFavicon = () => {
const existingFavicons = document.head.querySelectorAll(
"link[rel='icon']"
) as NodeListOf<HTMLLinkElement>
existingFavicons.forEach(favicon => {
if (favicon.href.endsWith('.svg')) favicon.parentNode?.removeChild(favicon)
})
}
const updateFavicon = (status: CompileStatus = 'UNCOMPILED') => {
removeFavicon()
const linkElement = document.createElement('link')
linkElement.rel = 'icon'
linkElement.href = COMPILE_ICONS[status]
linkElement.type = 'image/svg+xml'
linkElement.setAttribute('data-compile-status', 'true')
document.head.appendChild(linkElement)
}
const isActive = () => !document.hidden
const useIsWindowActive = () => {
const [isWindowActive, setIsWindowActive] = useState(isActive())
useEffect(() => {
const handleVisibilityChange = () => setIsWindowActive(isActive())
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}, [])
return isWindowActive
}
export const useStatusFavicon = () => {
const compileStatus = useCompileStatus()
const previousCompileStatus = usePreviousValue(compileStatus)
const isWindowActive = useIsWindowActive()
useEffect(() => {
if (previousCompileStatus !== compileStatus) {
return updateFavicon(compileStatus)
}
if (
isWindowActive &&
(compileStatus === 'COMPILED' || compileStatus === 'ERROR')
) {
const timeout = setTimeout(updateFavicon, RESET_AFTER_MS)
return () => clearTimeout(timeout)
}
}, [compileStatus, isWindowActive, previousCompileStatus])
}