update
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
import React from 'react';
|
||||
import React, { 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;
|
||||
}
|
||||
|
||||
function hasFileParams(params?: Record<string, any>): boolean {
|
||||
@@ -15,7 +23,12 @@ function hasFileParams(params?: Record<string, any>): boolean {
|
||||
return Object.keys(params).some((k) => k === 'file' || k.startsWith('file'));
|
||||
}
|
||||
|
||||
const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage }) => {
|
||||
function hasPollParam(params?: Record<string, any>): boolean {
|
||||
return params?.object?.type === 'talk-poll';
|
||||
}
|
||||
|
||||
const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, onReplyClick, onReactionToggled }) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const time = new Date(message.timestamp * 1000).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
@@ -25,7 +38,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage }) => {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', my: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontStyle: 'italic', color: 'text.secondary' }}>
|
||||
{message.message} - {time}
|
||||
<RichMessageText message={message.message} messageParameters={message.messageParameters} /> - {time}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@@ -33,10 +46,13 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage }) => {
|
||||
|
||||
const isFileMessage =
|
||||
hasFileParams(message.messageParameters) && message.message.includes('{file}');
|
||||
const isPollMessage = hasPollParam(message.messageParameters);
|
||||
|
||||
// For file messages, extract any surrounding text
|
||||
const textBeforeFile = isFileMessage ? message.message.split('{file}')[0].trim() : null;
|
||||
|
||||
const reactions = message.reactions ?? {};
|
||||
const reactionsSelf = message.reactionsSelf ?? [];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -44,53 +60,129 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage }) => {
|
||||
justifyContent: isOwnMessage ? 'flex-end' : 'flex-start',
|
||||
my: 0.5,
|
||||
px: 1,
|
||||
alignItems: 'flex-end',
|
||||
gap: 0.5,
|
||||
}}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
maxWidth: '80%',
|
||||
bgcolor: isOwnMessage ? 'primary.main' : (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.200',
|
||||
color: isOwnMessage ? 'primary.contrastText' : 'text.primary',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{!isOwnMessage && (
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block' }}>
|
||||
{message.actorDisplayName}
|
||||
</Typography>
|
||||
)}
|
||||
{isFileMessage ? (
|
||||
<>
|
||||
{textBeforeFile && (
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', mb: 0.5 }}>
|
||||
{textBeforeFile}
|
||||
</Typography>
|
||||
)}
|
||||
<FileMessageContent
|
||||
messageParameters={message.messageParameters!}
|
||||
isOwnMessage={isOwnMessage}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{message.message}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography
|
||||
variant="caption"
|
||||
{/* Reply button for own messages (shown on left) */}
|
||||
{isOwnMessage && onReplyClick && (
|
||||
<Tooltip title="Antworten">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onReplyClick(message)}
|
||||
sx={{ opacity: hovered ? 0.7 : 0, transition: 'opacity 0.15s', mb: 0.5 }}
|
||||
>
|
||||
<ReplyIcon sx={{ fontSize: '0.9rem', transform: 'scaleX(-1)' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Box sx={{ maxWidth: '80%' }}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
display: 'block',
|
||||
textAlign: 'right',
|
||||
mt: 0.25,
|
||||
opacity: 0.7,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
bgcolor: isOwnMessage ? 'primary.main' : (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.200',
|
||||
color: isOwnMessage ? 'primary.contrastText' : 'text.primary',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</Typography>
|
||||
</Paper>
|
||||
{!isOwnMessage && (
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block' }}>
|
||||
{message.actorDisplayName}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Quoted parent message */}
|
||||
{message.parent && (
|
||||
<Box sx={{
|
||||
borderLeft: '3px solid',
|
||||
borderColor: isOwnMessage ? 'rgba(255,255,255,0.4)' : 'divider',
|
||||
pl: 1,
|
||||
mb: 0.75,
|
||||
opacity: 0.75,
|
||||
borderRadius: '0 4px 4px 0',
|
||||
py: 0.25,
|
||||
bgcolor: isOwnMessage ? 'rgba(0,0,0,0.1)' : 'rgba(0,0,0,0.04)',
|
||||
}}>
|
||||
<Typography variant="caption" sx={{ display: 'block', fontWeight: 600, lineHeight: 1.4 }}>
|
||||
{message.parent.actorDisplayName}
|
||||
</Typography>
|
||||
<Typography variant="caption" noWrap sx={{ display: 'block', lineHeight: 1.4 }}>
|
||||
{message.parent.message}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isPollMessage ? (
|
||||
<PollMessageContent
|
||||
roomToken={message.token}
|
||||
pollId={message.messageParameters!.object.id}
|
||||
pollName={message.messageParameters!.object.name ?? ''}
|
||||
isOwnMessage={isOwnMessage}
|
||||
/>
|
||||
) : isFileMessage ? (
|
||||
<>
|
||||
{textBeforeFile && (
|
||||
<RichMessageText
|
||||
message={textBeforeFile}
|
||||
messageParameters={message.messageParameters}
|
||||
isOwnMessage={isOwnMessage}
|
||||
/>
|
||||
)}
|
||||
<FileMessageContent
|
||||
messageParameters={message.messageParameters!}
|
||||
isOwnMessage={isOwnMessage}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<RichMessageText
|
||||
message={message.message}
|
||||
messageParameters={message.messageParameters}
|
||||
isOwnMessage={isOwnMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
display: 'block',
|
||||
textAlign: 'right',
|
||||
mt: 0.25,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
{/* Reactions */}
|
||||
{!message.systemMessage && (
|
||||
<MessageReactions
|
||||
token={message.token}
|
||||
messageId={message.id}
|
||||
reactions={reactions}
|
||||
reactionsSelf={reactionsSelf}
|
||||
onReactionToggled={() => onReactionToggled?.(message.id)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Reply button for other messages (shown on right) */}
|
||||
{!isOwnMessage && onReplyClick && (
|
||||
<Tooltip title="Antworten">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onReplyClick(message)}
|
||||
sx={{ opacity: hovered ? 0.7 : 0, transition: 'opacity 0.15s', mb: 0.5 }}
|
||||
>
|
||||
<ReplyIcon sx={{ fontSize: '0.9rem' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user