[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
This commit is contained in:
Antoine Clausse
2025-06-24 08:05:15 +00:00
committed by Copybot
parent 46555d27b0
commit 2d5a3efc12
6 changed files with 235 additions and 0 deletions
@@ -16,6 +16,7 @@ import {
} from '@/features/ide-redesign/utils/new-editor-utils'
import EditorSurvey from '../editor-survey'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useStatusFavicon } from '@/features/ide-react/hooks/use-status-favicon'
const MainLayoutNew = lazy(
() => import('@/features/ide-redesign/components/main-layout')
@@ -30,6 +31,7 @@ export default function IdePage() {
useEditingSessionHeartbeat() // send a batched event when user is active
useRegisterUserActivity() // record activity and ensure connection when user is active
useHasLintingError() // pass editor:lint hasLintingError to the compiler
useStatusFavicon() // update the favicon based on the compile status
const newEditor = useIsNewEditorEnabled()
const canAccessNewEditor = canUseNewEditor()
@@ -0,0 +1,75 @@
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])
}