'use client' import React, { useState, useEffect, useCallback, useRef } from 'react' import { useTheme } from '@/lib/ThemeContext' import { MOTION, SHADOWS, SHADOWS_DARK, LAYERS } from '@/lib/spatial-ui/depth-system' import { usePerformance } from '@/lib/spatial-ui/PerformanceContext' /** * FloatingMessage - Cinematic message notification overlay * * Features: * - Slides in from right with spring animation * - Typewriter effect for message text * - Glassmorphism with depth * - Auto-dismiss with progress indicator * - Quick reply without leaving context * - Queue system for multiple messages */ export interface FloatingMessageData { id: string senderName: string senderInitials: string senderAvatar?: string content: string timestamp: Date conversationId: string isGroup?: boolean priority?: 'normal' | 'high' | 'urgent' } interface FloatingMessageProps { /** Auto-dismiss after X ms (0 = manual only) */ autoDismissMs?: number /** Max messages in queue */ maxQueue?: number /** Position on screen */ position?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left' /** Offset from edge */ offset?: { x: number; y: number } /** Callback when message is opened */ onOpen?: (message: FloatingMessageData) => void /** Callback when reply is sent */ onReply?: (message: FloatingMessageData, replyText: string) => void /** Callback when dismissed */ onDismiss?: (message: FloatingMessageData) => void } // Demo messages for testing const DEMO_MESSAGES: Omit[] = [ { senderName: 'Familie Mueller', senderInitials: 'FM', content: 'Guten Tag! Lisa hatte heute leider Fieber und konnte nicht zur Schule kommen. Koennten Sie uns bitte die Hausaufgaben mitteilen?', conversationId: 'conv1', isGroup: false, priority: 'normal', }, { senderName: 'Kollegium 7a', senderInitials: 'K7', content: 'Erinnerung: Morgen findet die Klassenkonferenz um 14:00 Uhr statt. Bitte alle Notenlisten vorbereiten.', conversationId: 'conv2', isGroup: true, priority: 'high', }, { senderName: 'Schulleitung', senderInitials: 'SL', content: 'Wichtig: Die Abiturklausuren muessen bis Freitag korrigiert sein. Bei Fragen wenden Sie sich an das Sekretariat.', conversationId: 'conv3', isGroup: false, priority: 'urgent', }, ] export function FloatingMessage({ autoDismissMs = 8000, maxQueue = 5, position = 'top-right', offset = { x: 24, y: 80 }, onOpen, onReply, onDismiss, }: FloatingMessageProps) { const { isDark } = useTheme() const { settings, reportAnimationStart, reportAnimationEnd } = usePerformance() const [messageQueue, setMessageQueue] = useState([]) const [currentMessage, setCurrentMessage] = useState(null) const [isVisible, setIsVisible] = useState(false) const [isExiting, setIsExiting] = useState(false) const [displayedText, setDisplayedText] = useState('') const [isTyping, setIsTyping] = useState(false) const [isReplying, setIsReplying] = useState(false) const [replyText, setReplyText] = useState('') const [dismissProgress, setDismissProgress] = useState(0) const typewriterRef = useRef(null) const dismissTimerRef = useRef(null) const progressRef = useRef(null) // Demo: Add messages periodically useEffect(() => { const addDemoMessage = (index: number) => { const demo = DEMO_MESSAGES[index % DEMO_MESSAGES.length] const message: FloatingMessageData = { ...demo, id: `msg-${Date.now()}-${index}`, timestamp: new Date(), } addToQueue(message) } // First message after 3 seconds const timer1 = setTimeout(() => addDemoMessage(0), 3000) // Second message after 15 seconds const timer2 = setTimeout(() => addDemoMessage(1), 15000) // Third message after 30 seconds const timer3 = setTimeout(() => addDemoMessage(2), 30000) return () => { clearTimeout(timer1) clearTimeout(timer2) clearTimeout(timer3) } }, []) // Add message to queue const addToQueue = useCallback( (message: FloatingMessageData) => { setMessageQueue((prev) => { if (prev.length >= maxQueue) { return [...prev.slice(1), message] } return [...prev, message] }) }, [maxQueue] ) // Process queue useEffect(() => { if (!currentMessage && messageQueue.length > 0 && !isExiting) { const next = messageQueue[0] setMessageQueue((prev) => prev.slice(1)) setCurrentMessage(next) setIsVisible(true) setDisplayedText('') setIsTyping(true) setDismissProgress(0) reportAnimationStart() } }, [currentMessage, messageQueue, isExiting, reportAnimationStart]) // Typewriter effect useEffect(() => { if (!currentMessage || !isTyping) return const fullText = currentMessage.content let charIndex = 0 if (settings.enableTypewriter) { const speed = Math.round(25 * settings.animationSpeed) typewriterRef.current = setInterval(() => { charIndex++ setDisplayedText(fullText.slice(0, charIndex)) if (charIndex >= fullText.length) { if (typewriterRef.current) clearInterval(typewriterRef.current) setIsTyping(false) } }, speed) } else { setDisplayedText(fullText) setIsTyping(false) } return () => { if (typewriterRef.current) clearInterval(typewriterRef.current) } }, [currentMessage, isTyping, settings.enableTypewriter, settings.animationSpeed]) // Auto-dismiss timer with progress useEffect(() => { if (!currentMessage || autoDismissMs <= 0 || isTyping || isReplying) return const startTime = Date.now() const updateProgress = () => { const elapsed = Date.now() - startTime const progress = Math.min(100, (elapsed / autoDismissMs) * 100) setDismissProgress(progress) if (progress >= 100) { handleDismiss() } } progressRef.current = setInterval(updateProgress, 50) dismissTimerRef.current = setTimeout(handleDismiss, autoDismissMs) return () => { if (progressRef.current) clearInterval(progressRef.current) if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current) } }, [currentMessage, autoDismissMs, isTyping, isReplying]) // Dismiss handler const handleDismiss = useCallback(() => { if (progressRef.current) clearInterval(progressRef.current) if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current) setIsExiting(true) const exitDuration = Math.round(MOTION.accelerate.duration * settings.animationSpeed) setTimeout(() => { if (currentMessage) { onDismiss?.(currentMessage) } setCurrentMessage(null) setIsVisible(false) setIsExiting(false) setDisplayedText('') setReplyText('') setIsReplying(false) setDismissProgress(0) reportAnimationEnd() }, exitDuration) }, [currentMessage, onDismiss, reportAnimationEnd, settings.animationSpeed]) // Open conversation const handleOpen = useCallback(() => { if (currentMessage) { onOpen?.(currentMessage) handleDismiss() } }, [currentMessage, onOpen, handleDismiss]) // Start reply const handleReplyClick = useCallback(() => { setIsReplying(true) // Cancel auto-dismiss while replying if (progressRef.current) clearInterval(progressRef.current) if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current) }, []) // Send reply const handleSendReply = useCallback(() => { if (!replyText.trim() || !currentMessage) return onReply?.(currentMessage, replyText) handleDismiss() }, [replyText, currentMessage, onReply, handleDismiss]) // Cancel reply const handleCancelReply = useCallback(() => { setIsReplying(false) setReplyText('') }, []) // Keyboard handler const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSendReply() } if (e.key === 'Escape') { if (isReplying) { handleCancelReply() } else { handleDismiss() } } }, [handleSendReply, isReplying, handleCancelReply, handleDismiss] ) if (!isVisible) return null // Position styles const positionStyles: React.CSSProperties = { position: 'fixed', zIndex: LAYERS.overlay.zIndex, ...(position.includes('top') ? { top: offset.y } : { bottom: offset.y }), ...(position.includes('right') ? { right: offset.x } : { left: offset.x }), } // Animation styles const animationDuration = Math.round(MOTION.decelerate.duration * settings.animationSpeed) const exitDuration = Math.round(MOTION.accelerate.duration * settings.animationSpeed) const transformOrigin = position.includes('right') ? 'right' : 'left' const slideDirection = position.includes('right') ? 'translateX(120%)' : 'translateX(-120%)' // Priority color const priorityColor = currentMessage?.priority === 'urgent' ? 'from-red-500 to-orange-500' : currentMessage?.priority === 'high' ? 'from-amber-500 to-yellow-500' : 'from-purple-500 to-pink-500' // Material styles - Ultra transparent for floating effect (4%) const cardBg = isDark ? 'rgba(255, 255, 255, 0.04)' : 'rgba(255, 255, 255, 0.08)' const shadow = '0 8px 32px rgba(0, 0, 0, 0.3)' const blur = settings.enableBlur ? `blur(${Math.round(24 * settings.blurIntensity)}px) saturate(180%)` : 'none' return (
{/* Progress bar */} {autoDismissMs > 0 && !isReplying && (
)}
{/* Header */}
{/* Avatar */}
{currentMessage?.senderInitials}

{currentMessage?.senderName}

{currentMessage?.isGroup ? 'Gruppenchat' : 'Direktnachricht'} • Jetzt

{/* Close button */}
{/* Message content */}

{displayedText} {isTyping && ( )}

{/* Reply input */} {isReplying && (