import React, { useRef, useState } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Paper from '@mui/material/Paper'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; import ReplyIcon from '@mui/icons-material/Reply'; import type { NextcloudMessage } from '../../types/nextcloud.types'; import FileMessageContent from './FileMessageContent'; import RichMessageText from './RichMessageText'; import PollMessageContent from './PollMessageContent'; import MessageReactions from './MessageReactions'; interface ChatMessageProps { message: NextcloudMessage; isOwnMessage: boolean; onReplyClick?: (message: NextcloudMessage) => void; onReactionToggled?: (messageId: number) => void; reactionsOverride?: { reactions: Record; reactionsSelf: string[] }; } function hasFileParams(params?: Record): boolean { if (!params) return false; return Object.keys(params).some((k) => k === 'file' || k.startsWith('file')); } function hasPollParam(params?: Record): boolean { return params?.object?.type === 'talk-poll'; } const ChatMessage: React.FC = ({ message, isOwnMessage, onReplyClick, onReactionToggled, reactionsOverride }) => { const [hovered, setHovered] = useState(false); const longPressTimer = useRef | null>(null); const hideTimer = useRef | null>(null); const handleTouchStart = () => { longPressTimer.current = setTimeout(() => { setHovered(true); // Auto-hide after 4 seconds if the user doesn't interact hideTimer.current = setTimeout(() => setHovered(false), 4000); }, 500); }; const handleTouchEnd = () => { if (longPressTimer.current) { clearTimeout(longPressTimer.current); longPressTimer.current = null; } }; const handleTouchMove = () => { if (longPressTimer.current) { clearTimeout(longPressTimer.current); longPressTimer.current = null; } if (hideTimer.current) { clearTimeout(hideTimer.current); hideTimer.current = null; } setHovered(false); }; const time = new Date(message.timestamp * 1000).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', }); if (message.systemMessage) { return ( - {time} ); } const isFileMessage = hasFileParams(message.messageParameters) && message.message.includes('{file}'); const isPollMessage = hasPollParam(message.messageParameters); const textBeforeFile = isFileMessage ? message.message.split('{file}')[0].trim() : null; const reactions = reactionsOverride?.reactions ?? message.reactions ?? {}; const reactionsSelf = reactionsOverride?.reactionsSelf ?? message.reactionsSelf ?? []; const hasReactions = Object.keys(reactions).length > 0; return ( setHovered(true)} onMouseLeave={() => setHovered(false)} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd} onTouchMove={handleTouchMove} > {/* Reply button for own messages (shown on left) */} {isOwnMessage && onReplyClick && ( onReplyClick(message)} sx={{ opacity: hovered ? 0.7 : 0, transition: 'opacity 0.15s', mb: 0.5 }} > )} theme.palette.mode === 'dark' ? 'grey.800' : 'grey.200', color: isOwnMessage ? 'primary.contrastText' : 'text.primary', borderRadius: 2, }} > {!isOwnMessage && ( {message.actorDisplayName} )} {/* Quoted parent message */} {message.parent && ( {message.parent.actorDisplayName} {message.parent.message} )} {isPollMessage ? ( ) : isFileMessage ? ( <> {textBeforeFile && ( )} ) : ( )} {time} {/* Reactions — shown on hover or when reactions exist */} {(hovered || hasReactions) && ( onReactionToggled?.(message.id)} /> )} {/* Reply button for other messages (shown on right) */} {!isOwnMessage && onReplyClick && ( onReplyClick(message)} sx={{ opacity: hovered ? 0.7 : 0, transition: 'opacity 0.15s', mb: 0.5 }} > )} ); }; export default ChatMessage;