mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
[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:
@@ -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])
|
||||
}
|
||||
9
services/web/public/favicon-compiled.svg
Normal file
9
services/web/public/favicon-compiled.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="8" fill="#fff"/>
|
||||
<path
|
||||
d="M13.0748 3.19694C11.6883 2.6541 6.67193 2.45789 6.66539 5.45331C5.20038 6.38857 4.23242 7.91244 4.23242 9.5475C4.23242 11.5226 5.83478 13.125 7.80992 13.125C9.78507 13.125 11.3874 11.5226 11.3874 9.5475C11.3874 8.02362 10.4326 6.71558 9.08527 6.20544C8.82366 6.10734 8.2612 5.93075 7.81646 5.96999C7.17552 6.37549 6.3907 7.21263 6.02444 8.04978C6.57382 7.38922 7.43059 7.10145 8.1958 7.22571C9.31418 7.40884 10.1709 8.37679 10.1709 9.55404C10.1709 10.8555 9.11797 11.9085 7.81646 11.9085C7.09704 11.9085 6.4561 11.588 6.02444 11.0844C5.37696 10.3323 5.21346 9.52133 5.34426 8.72997C5.79554 5.95691 9.08527 4.38072 11.5313 3.77248C10.7334 4.19759 9.29456 4.89085 8.28736 5.64298C11.2239 6.78098 11.7014 4.30223 13.0748 3.19694Z"
|
||||
fill="#046530"/>
|
||||
<circle cx="10.5" cy="11.5" r="3.5" fill="#fff"/>
|
||||
<path fill="#000"
|
||||
d="m9.615 12.032 2.75-2.921a.31.31 0 0 1 .26-.111q.146 0 .26.122a.395.395 0 0 1 0 .555l-3 3.2a.349.349 0 0 1-.531 0l-1.24-1.322a.395.395 0 0 1 0-.556.35.35 0 0 1 .261-.122q.146 0 .26.122z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
9
services/web/public/favicon-compiling.svg
Normal file
9
services/web/public/favicon-compiling.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="8" fill="#fff"/>
|
||||
<path
|
||||
d="M13.0748 3.19694C11.6883 2.6541 6.67193 2.45789 6.66539 5.45331C5.20038 6.38857 4.23242 7.91244 4.23242 9.5475C4.23242 11.5226 5.83478 13.125 7.80992 13.125C9.78507 13.125 11.3874 11.5226 11.3874 9.5475C11.3874 8.02362 10.4326 6.71558 9.08527 6.20544C8.82366 6.10734 8.2612 5.93075 7.81646 5.96999C7.17552 6.37549 6.3907 7.21263 6.02444 8.04978C6.57382 7.38922 7.43059 7.10145 8.1958 7.22571C9.31418 7.40884 10.1709 8.37679 10.1709 9.55404C10.1709 10.8555 9.11797 11.9085 7.81646 11.9085C7.09704 11.9085 6.4561 11.588 6.02444 11.0844C5.37696 10.3323 5.21346 9.52133 5.34426 8.72997C5.79554 5.95691 9.08527 4.38072 11.5313 3.77248C10.7334 4.19759 9.29456 4.89085 8.28736 5.64298C11.2239 6.78098 11.7014 4.30223 13.0748 3.19694Z"
|
||||
fill="#046530"/>
|
||||
<circle cx="10.5" cy="11.5" r="3.5" fill="#fff"/>
|
||||
<path fill="#000"
|
||||
d="M9.1 11.4q0 .4.158.742.167.341.442.583V12.5q0-.125.083-.208A.3.3 0 0 1 10 12.2a.27.27 0 0 1 .208.092.27.27 0 0 1 .092.208v1q0 .125-.092.217A.28.28 0 0 1 10 13.8H9a.3.3 0 0 1-.217-.083.3.3 0 0 1-.083-.217q0-.125.083-.208A.3.3 0 0 1 9 13.2h.317a2.6 2.6 0 0 1-.6-.792A2.4 2.4 0 0 1 8.5 11.4q0-.708.367-1.275.366-.575.966-.867a.22.22 0 0 1 .217 0 .28.28 0 0 1 .15.167.3.3 0 0 1-.008.233.3.3 0 0 1-.15.167 1.8 1.8 0 0 0-.684.65q-.258.408-.258.925m3.6 0a1.7 1.7 0 0 0-.167-.742 1.7 1.7 0 0 0-.433-.583v.225q0 .125-.092.217a.28.28 0 0 1-.208.083.3.3 0 0 1-.217-.083.3.3 0 0 1-.083-.217v-1q0-.125.083-.208A.3.3 0 0 1 11.8 9h1a.27.27 0 0 1 .208.092.27.27 0 0 1 .092.208q0 .125-.092.217a.28.28 0 0 1-.208.083h-.317q.376.333.592.8.225.459.225 1 0 .708-.367 1.283a2.36 2.36 0 0 1-.966.859.23.23 0 0 1-.217.008.3.3 0 0 1-.15-.175.35.35 0 0 1-.008-.225.3.3 0 0 1 .15-.167q.425-.225.691-.641a1.7 1.7 0 0 0 .267-.942"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
9
services/web/public/favicon-error.svg
Normal file
9
services/web/public/favicon-error.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="8" fill="#fff"/>
|
||||
<path
|
||||
d="M13.0748 3.19694C11.6883 2.6541 6.67193 2.45789 6.66539 5.45331C5.20038 6.38857 4.23242 7.91244 4.23242 9.5475C4.23242 11.5226 5.83478 13.125 7.80992 13.125C9.78507 13.125 11.3874 11.5226 11.3874 9.5475C11.3874 8.02362 10.4326 6.71558 9.08527 6.20544C8.82366 6.10734 8.2612 5.93075 7.81646 5.96999C7.17552 6.37549 6.3907 7.21263 6.02444 8.04978C6.57382 7.38922 7.43059 7.10145 8.1958 7.22571C9.31418 7.40884 10.1709 8.37679 10.1709 9.55404C10.1709 10.8555 9.11797 11.9085 7.81646 11.9085C7.09704 11.9085 6.4561 11.588 6.02444 11.0844C5.37696 10.3323 5.21346 9.52133 5.34426 8.72997C5.79554 5.95691 9.08527 4.38072 11.5313 3.77248C10.7334 4.19759 9.29456 4.89085 8.28736 5.64298C11.2239 6.78098 11.7014 4.30223 13.0748 3.19694Z"
|
||||
fill="#046530"/>
|
||||
<circle cx="10.5" cy="11.5" r="3.5" fill="#fff"/>
|
||||
<path fill="#000"
|
||||
d="m10.5 12.064-1.814 1.814a.38.38 0 0 1-.277.122.43.43 0 0 1-.276-.133.38.38 0 0 1-.122-.276q0-.166.122-.288L9.936 11.5 8.122 9.686A.38.38 0 0 1 8 9.409a.44.44 0 0 1 .133-.287A.38.38 0 0 1 8.409 9q.166 0 .288.122l1.803 1.814 1.814-1.814A.38.38 0 0 1 12.591 9q.165 0 .287.122a.4.4 0 0 1 .122.287q0 .156-.122.277L11.064 11.5l1.814 1.814a.38.38 0 0 1 .122.277q0 .155-.122.276a.4.4 0 0 1-.287.122.38.38 0 0 1-.277-.122z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,131 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import * as CompileContext from '@/shared/context/detach-compile-context'
|
||||
|
||||
import { useStatusFavicon } from '@/features/ide-react/hooks/use-status-favicon'
|
||||
|
||||
type Compilation = { uncompiled: boolean; compiling: boolean; error: boolean }
|
||||
|
||||
describe('useStatusFavicon', function () {
|
||||
let mockUseDetachCompileContext: sinon.SinonStub
|
||||
let clock: sinon.SinonFakeTimers
|
||||
let originalHidden: PropertyDescriptor | undefined
|
||||
|
||||
const setCompilation = (compileContext: Compilation) => {
|
||||
mockUseDetachCompileContext.returns(compileContext)
|
||||
}
|
||||
const setHidden = (hidden: boolean) => {
|
||||
Object.defineProperty(document, 'hidden', {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: hidden,
|
||||
})
|
||||
document.dispatchEvent(new Event('visibilitychange'))
|
||||
}
|
||||
|
||||
const getFaviconElements = () =>
|
||||
document.querySelectorAll('link[data-compile-status="true"]')
|
||||
|
||||
const getCurrentFaviconHref = () => {
|
||||
const favicon = document.querySelector(
|
||||
'link[data-compile-status="true"]'
|
||||
) as HTMLLinkElement
|
||||
return favicon?.href || null
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockUseDetachCompileContext = sinon.stub(
|
||||
CompileContext,
|
||||
'useDetachCompileContext'
|
||||
)
|
||||
|
||||
// Mock timers for timeout testing
|
||||
clock = sinon.useFakeTimers()
|
||||
|
||||
// Store original document.hidden descriptor
|
||||
originalHidden = Object.getOwnPropertyDescriptor(
|
||||
Document.prototype,
|
||||
'hidden'
|
||||
)
|
||||
|
||||
// Clean up any existing favicon elements
|
||||
document
|
||||
.querySelectorAll('link[data-compile-status="true"]')
|
||||
.forEach(el => el.remove())
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
clock.restore()
|
||||
|
||||
// Restore original document.hidden
|
||||
if (originalHidden) {
|
||||
Object.defineProperty(document, 'hidden', originalHidden)
|
||||
}
|
||||
|
||||
// Clean up favicon elements
|
||||
document
|
||||
.querySelectorAll('link[data-compile-status="true"]')
|
||||
.forEach(el => el.remove())
|
||||
})
|
||||
|
||||
it('updates favicon to reflect status: UNCOMPILED', function () {
|
||||
setCompilation({ uncompiled: true, compiling: false, error: false })
|
||||
renderHook(() => useStatusFavicon())
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon.svg')
|
||||
})
|
||||
|
||||
it('updates favicon to reflect status: COMPILING', function () {
|
||||
setCompilation({ uncompiled: false, compiling: true, error: false })
|
||||
renderHook(() => useStatusFavicon())
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon-compiling.svg')
|
||||
})
|
||||
|
||||
it('updates favicon to reflect status: COMPILED', function () {
|
||||
setCompilation({ uncompiled: false, compiling: false, error: false })
|
||||
renderHook(() => useStatusFavicon())
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg')
|
||||
})
|
||||
|
||||
it('updates favicon to reflect status: ERROR', function () {
|
||||
setCompilation({ uncompiled: false, compiling: false, error: true })
|
||||
renderHook(() => useStatusFavicon())
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon-error.svg')
|
||||
})
|
||||
|
||||
it('keeps the COMPILED favicon for 5 seconds when the window is active', function () {
|
||||
setCompilation({ uncompiled: false, compiling: false, error: false })
|
||||
const { rerender } = renderHook(() => useStatusFavicon())
|
||||
setHidden(false)
|
||||
rerender()
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg')
|
||||
clock.tick(4500)
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg')
|
||||
clock.tick(1000)
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon.svg')
|
||||
})
|
||||
|
||||
it('keeps the COMPILED favicon forever when the window is hidden', function () {
|
||||
setCompilation({ uncompiled: false, compiling: false, error: false })
|
||||
const { rerender } = renderHook(() => useStatusFavicon())
|
||||
setHidden(true)
|
||||
rerender()
|
||||
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg')
|
||||
clock.tick(90000)
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg')
|
||||
})
|
||||
|
||||
it('should only have one favicon element at a time', function () {
|
||||
setCompilation({ uncompiled: true, compiling: false, error: false })
|
||||
const { rerender } = renderHook(() => useStatusFavicon())
|
||||
expect(getFaviconElements()).to.have.length(1)
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon.svg')
|
||||
|
||||
setCompilation({ uncompiled: false, compiling: true, error: false })
|
||||
rerender()
|
||||
expect(getFaviconElements()).to.have.length(1)
|
||||
expect(getCurrentFaviconHref()).to.include('/favicon-compiling.svg')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user