import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ImagePreview from './ImagePreview'; import { extractImagePaths, removeImagePathsFromText } from '../utils/imageUtils'; import MarkdownContent from './MarkdownContent'; import { getTextContent } from '../types/message'; import { Message } from '../api'; import MessageCopyLink from './MessageCopyLink'; import { formatMessageTimestamp } from '../utils/timeUtils'; import Edit from './icons/Edit'; import { Button } from './ui/button'; interface UserMessageProps { message: Message; onMessageUpdate?: (messageId: string, newContent: string) => void; } export default function UserMessage({ message, onMessageUpdate }: UserMessageProps) { const contentRef = useRef(null); const textareaRef = useRef(null); const [isEditing, setIsEditing] = useState(false); const [editContent, setEditContent] = useState(''); const [hasBeenEdited, setHasBeenEdited] = useState(false); const [error, setError] = useState(null); // Extract text content from the message const textContent = getTextContent(message); // Extract image paths from the message const imagePaths = extractImagePaths(textContent); // Remove image paths from text for display - memoized for performance const displayText = useMemo( () => removeImagePathsFromText(textContent, imagePaths), [textContent, imagePaths] ); // Memoize the timestamp const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]); // Effect to handle message content changes and ensure persistence useEffect(() => { // If we're not editing, update the edit content to match the current message if (!isEditing) { setEditContent(displayText); } }, [message.content, displayText, message.id, isEditing]); // Initialize edit mode with current message content const initializeEditMode = useCallback(() => { setEditContent(displayText); setError(null); window.electron.logInfo(`Entering edit mode with content: ${displayText}`); }, [displayText]); // Handle edit button click const handleEditClick = useCallback(() => { const newEditingState = !isEditing; setIsEditing(newEditingState); // Initialize edit content when entering edit mode if (newEditingState) { initializeEditMode(); window.electron.logInfo(`Edit interface shown for message: ${message.id}`); // Focus the textarea after a brief delay to ensure it's rendered setTimeout(() => { if (textareaRef.current) { textareaRef.current.focus(); textareaRef.current.setSelectionRange( textareaRef.current.value.length, textareaRef.current.value.length ); } }, 50); } window.electron.logInfo(`Edit state toggled: ${newEditingState} for message: ${message.id}`); }, [isEditing, initializeEditMode, message.id]); // Handle content changes in edit mode const handleContentChange = useCallback((e: React.ChangeEvent) => { const newContent = e.target.value; setEditContent(newContent); setError(null); // Clear any previous errors window.electron.logInfo(`Content changed: ${newContent}`); }, []); // Handle save action const handleSave = useCallback(() => { // Exit edit mode immediately setIsEditing(false); // Check if content has actually changed if (editContent !== displayText) { // Validate content if (editContent.trim().length === 0) { setError('Message cannot be empty'); return; } // Update the message content through the callback if (onMessageUpdate && message.id) { onMessageUpdate(message.id, editContent); setHasBeenEdited(true); } } }, [editContent, displayText, onMessageUpdate, message.id]); // Handle cancel action const handleCancel = useCallback(() => { window.electron.logInfo('Cancel clicked - reverting to original content'); setIsEditing(false); setEditContent(displayText); // Reset to original content setError(null); }, [displayText]); // Handle keyboard events for accessibility const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { window.electron.logInfo( `Key pressed: ${e.key}, metaKey: ${e.metaKey}, ctrlKey: ${e.ctrlKey}` ); if (e.key === 'Escape') { e.preventDefault(); handleCancel(); } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); window.electron.logInfo('Cmd+Enter detected, calling handleSave'); handleSave(); } }, [handleCancel, handleSave] ); // Auto-resize textarea based on content useEffect(() => { if (textareaRef.current && isEditing) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; } }, [editContent, isEditing]); return (
{isEditing ? ( // Truly wide, centered, in-place edit box replacing the bubble