fix: harden splash screen timer/listener leaks and origin validation
- SplashScreen: track all scheduled timers, clear on unmount (prevents ghost callbacks) - SplashScreen: manage video pause listener lifecycle (add once, remove when done) - SplashScreen: clear videoTimeUpdateInterval on unmount - Chat.vue: validate postMessage origin before accepting ready signal - App.vue: remove shadowed variable re-declaration in onKeyDown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -142,8 +142,6 @@ function onKeyDown(e: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
// 's' key activates screensaver when authenticated (skip if typing in input)
|
// 's' key activates screensaver when authenticated (skip if typing in input)
|
||||||
if (e.key === 's' || e.key === 'S') {
|
if (e.key === 's' || e.key === 'S') {
|
||||||
const target = e.target as HTMLElement
|
|
||||||
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
|
|
||||||
if (!isInput && appStore.isAuthenticated && !screensaverStore.isActive) {
|
if (!isInput && appStore.isAuthenticated && !screensaverStore.isActive) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
screensaverStore.activate()
|
screensaverStore.activate()
|
||||||
|
|||||||
@@ -191,23 +191,35 @@ const isTypingLine3 = ref(false)
|
|||||||
const isTypingLine4 = ref(false)
|
const isTypingLine4 = ref(false)
|
||||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||||
let introTypingTimeout: ReturnType<typeof setTimeout> | null = null
|
let introTypingTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const pendingTimers: ReturnType<typeof setTimeout>[] = []
|
||||||
|
|
||||||
|
function scheduleTimer(fn: () => void, delay: number) {
|
||||||
|
const id = setTimeout(fn, delay)
|
||||||
|
pendingTimers.push(id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure video plays continuously from Welcome Noderunner through logo
|
// Ensure video plays continuously from Welcome Noderunner through logo
|
||||||
|
let videoPauseHandler: ((e: Event) => void) | null = null
|
||||||
watch([showWelcome, showLogo], ([welcome, logo]) => {
|
watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||||||
if ((welcome || logo) && videoElement.value) {
|
if ((welcome || logo) && videoElement.value) {
|
||||||
// Ensure video is playing and doesn't pause
|
|
||||||
if (videoElement.value.paused) {
|
if (videoElement.value.paused) {
|
||||||
videoElement.value.play().catch(err => {
|
videoElement.value.play().catch(err => {
|
||||||
console.warn('Video autoplay failed:', err)
|
console.warn('Video autoplay failed:', err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Keep video playing - prevent any pauses
|
// Add pause prevention handler once, remove when no longer needed
|
||||||
videoElement.value.addEventListener('pause', (e) => {
|
if (!videoPauseHandler) {
|
||||||
if (welcome || logo) {
|
videoPauseHandler = () => {
|
||||||
e.preventDefault()
|
if ((showWelcome.value || showLogo.value) && videoElement.value) {
|
||||||
videoElement.value?.play()
|
videoElement.value.play().catch(() => {})
|
||||||
}
|
}
|
||||||
}, { once: false })
|
}
|
||||||
|
videoElement.value.addEventListener('pause', videoPauseHandler)
|
||||||
|
}
|
||||||
|
} else if (videoPauseHandler && videoElement.value) {
|
||||||
|
videoElement.value.removeEventListener('pause', videoPauseHandler)
|
||||||
|
videoPauseHandler = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -303,36 +315,34 @@ function skipIntro() {
|
|||||||
isTypingLine4.value = false
|
isTypingLine4.value = false
|
||||||
|
|
||||||
// Start background fade in at 0.3 opacity when welcome appears
|
// Start background fade in at 0.3 opacity when welcome appears
|
||||||
setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
backgroundOpacity.value = 0.3
|
backgroundOpacity.value = 0.3
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
// Continue with welcome fade out after typing (2s) + cursor continues (1.5s) + 3 blinks (1.35s)
|
// Continue with welcome fade out after typing (2s) + cursor continues (1.5s) + 3 blinks (1.35s)
|
||||||
setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
fadeWelcome.value = true
|
fadeWelcome.value = true
|
||||||
typingWelcome.value = false
|
typingWelcome.value = false
|
||||||
}, 4850)
|
}, 4850)
|
||||||
|
|
||||||
// Show logo - no zoom, just fade
|
// Show logo - no zoom, just fade
|
||||||
setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
showLogo.value = true
|
showLogo.value = true
|
||||||
// Keep background at 0.3 opacity during logo display
|
|
||||||
}, 5500)
|
}, 5500)
|
||||||
|
|
||||||
// Hide welcome after logo starts appearing
|
// Hide welcome after logo starts appearing
|
||||||
setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
showWelcome.value = false
|
showWelcome.value = false
|
||||||
}, 6000)
|
}, 6000)
|
||||||
|
|
||||||
// Fade background to full opacity just before completing (for smooth transition to modal)
|
// Fade background to full opacity just before completing (for smooth transition to modal)
|
||||||
setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
backgroundOpacity.value = 1
|
backgroundOpacity.value = 1
|
||||||
}, 9000)
|
}, 9000)
|
||||||
|
|
||||||
// Complete splash with smooth transition - wait for zoom to complete
|
// Complete splash with smooth transition
|
||||||
setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
// Add a small delay to ensure smooth transition
|
scheduleTimer(() => {
|
||||||
setTimeout(() => {
|
|
||||||
showSplash.value = false
|
showSplash.value = false
|
||||||
document.body.classList.add('splash-complete')
|
document.body.classList.add('splash-complete')
|
||||||
localStorage.setItem('neode_intro_seen', '1')
|
localStorage.setItem('neode_intro_seen', '1')
|
||||||
@@ -409,24 +419,24 @@ function startAlienIntro() {
|
|||||||
}
|
}
|
||||||
backgroundOpacity.value = 0.3
|
backgroundOpacity.value = 0.3
|
||||||
|
|
||||||
introTypingTimeout = setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
fadeWelcome.value = true
|
fadeWelcome.value = true
|
||||||
typingWelcome.value = false
|
typingWelcome.value = false
|
||||||
}, 4850)
|
}, 4850)
|
||||||
|
|
||||||
introTypingTimeout = setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
showLogo.value = true
|
showLogo.value = true
|
||||||
}, 5500)
|
}, 5500)
|
||||||
|
|
||||||
introTypingTimeout = setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
showWelcome.value = false
|
showWelcome.value = false
|
||||||
}, 6000)
|
}, 6000)
|
||||||
|
|
||||||
introTypingTimeout = setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
backgroundOpacity.value = 1
|
backgroundOpacity.value = 1
|
||||||
}, 9000)
|
}, 9000)
|
||||||
|
|
||||||
introTypingTimeout = setTimeout(() => {
|
scheduleTimer(() => {
|
||||||
if (videoElement.value && !videoElement.value.paused) {
|
if (videoElement.value && !videoElement.value.paused) {
|
||||||
sessionStorage.setItem('video_intro_currentTime', videoElement.value.currentTime.toString())
|
sessionStorage.setItem('video_intro_currentTime', videoElement.value.currentTime.toString())
|
||||||
sessionStorage.setItem('video_intro_wasPlaying', 'true')
|
sessionStorage.setItem('video_intro_wasPlaying', 'true')
|
||||||
@@ -455,6 +465,14 @@ onBeforeUnmount(() => {
|
|||||||
clearTimeout(introTypingTimeout)
|
clearTimeout(introTypingTimeout)
|
||||||
introTypingTimeout = null
|
introTypingTimeout = null
|
||||||
}
|
}
|
||||||
|
// Clear all scheduled timers to prevent firing on unmounted component
|
||||||
|
for (const id of pendingTimers) clearTimeout(id)
|
||||||
|
pendingTimers.length = 0
|
||||||
|
// Clear video time update interval
|
||||||
|
if (videoTimeUpdateInterval) {
|
||||||
|
clearInterval(videoTimeUpdateInterval)
|
||||||
|
videoTimeUpdateInterval = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ function closeChat() {
|
|||||||
|
|
||||||
function onAiuiMessage(event: MessageEvent) {
|
function onAiuiMessage(event: MessageEvent) {
|
||||||
if (!aiuiUrl.value) return
|
if (!aiuiUrl.value) return
|
||||||
|
// Validate origin — only accept messages from AIUI
|
||||||
|
try {
|
||||||
|
const expected = new URL(aiuiUrl.value, window.location.origin).origin
|
||||||
|
if (event.origin !== expected) return
|
||||||
|
} catch { return }
|
||||||
const msg = event.data
|
const msg = event.data
|
||||||
if (msg && msg.type === 'ready') {
|
if (msg && msg.type === 'ready') {
|
||||||
aiuiConnected.value = true
|
aiuiConnected.value = true
|
||||||
|
|||||||
Reference in New Issue
Block a user