/
viktorphp
/
front-socket
Обзор
Документация
Войти
/
viktorphp
/
front-socket
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/features/chat/ui/ChatBubble.tsx
255 строк
8 KB
Иванов Виктор Евгеньевич
[feat-tree]: добавить страницу дерева предков, поправить основные баги
13 апр 2026, 21:32
13 апр 2026, 21:32
6a6f509
Код
Авторство
О чём код?
import { useEffect, useRef, useState } from 'react'; import { Paper, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { IMessage } from '@shared/types'; export interface IChatBubbleProps { message: IMessage; isOwn?: boolean; } export const ChatBubble = ({ message, isOwn = false }: IChatBubbleProps) => { return ( <Paper elevation={0} sx={{ p: 1, maxWidth: '70%', mb: 1, backgroundColor: isOwn ? 'primary.light' : 'grey.100', borderRadius: 3, textAlign: 'left', ...(isOwn ? { borderBottomRightRadius: 5 } : { borderBottomLeftRadius: 5 }), boxShadow: isOwn ? '-2px 2px 4px rgba(0, 0, 0, 0.2)' : '2px 2px 4px rgba(0, 0, 0, 0.2)', '&::before,&::after': { content: '""', position: 'absolute', bottom: 1, height: 15, }, ...(isOwn ? { '&:before': { right: 15, width: 20, backgroundColor: 'inherit', borderBottomLeftRadius: 16, boxShadow: '-2px 2px 4px rgba(0, 0, 0, 0.2)', }, '&:after': { right: -5, width: 26, backgroundColor: 'white', borderBottomLeftRadius: 10, }, } : { '&:before': { left: 15, width: 20, backgroundColor: 'inherit', borderBottomRightRadius: 16, boxShadow: '2px 2px 4px rgba(0, 0, 0, 0.2)', }, '&:after': { left: -5, width: 26, backgroundColor: 'white', borderBottomRightRadius: 10, }, }), }} > {message.sender && !isOwn && ( <Typography variant="caption" color="textSecondary" sx={{ mb: 0.5, fontWeight: 'bold' }} > {message.sender.username} </Typography> )} <Typography variant="body1" sx={{ wordBreak: 'break-word' }}> {message.content} </Typography> <Typography variant="caption" display="block" sx={{ textAlign: 'right', mt: 0.5 }} > {new Date(message?.createdAt ?? '').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', })} </Typography> </Paper> ); }; export interface IChatBubbleSVGProps { message: IMessage; isOwn?: boolean; minWidth?: number; maxWidth?: number; } export const ChatBubbleSVG = ({ message, isOwn = false, minWidth = 100, maxWidth = 800, }: IChatBubbleSVGProps) => { const theme = useTheme(); const [bubbleWidth, setBubbleWidth] = useState(minWidth); const [textLines, setTextLines] = useState<string[]>([]); // Параметры отрисовки пузыря const padding = 10; // внутренний отступ const lineHeight = 20; // высота одной строки текста const charWidth = 8; // примерная ширина символа (для оценки) // Функция для разбиения текста на строки с учетом ширины useEffect(() => { // Примерная оценка ширины текста const estimatedWidth = Math.min( Math.max(message.content.length * charWidth + padding * 2, minWidth), maxWidth ); // Устанавливаем начальную ширину пузыря setBubbleWidth(estimatedWidth); // Разбиваем текст на строки с учетом переносов const initialLines = message.content.split('\n'); // Разбиваем длинные строки на подстроки const wrappedLines: string[] = []; initialLines.forEach((line) => { // Максимальное количество символов в строке (с учетом отступов) const maxCharsPerLine = Math.floor( (estimatedWidth - padding * 2) / charWidth ); if (line.length <= maxCharsPerLine) { wrappedLines.push(line); } else { // Разбиваем длинную строку на части let remainingText = line; while (remainingText.length > 0) { const chunk = remainingText.substring(0, maxCharsPerLine); wrappedLines.push(chunk); remainingText = remainingText.substring(maxCharsPerLine); } } }); setTextLines(wrappedLines); }, [message.content, minWidth, maxWidth]); const textLineCount = textLines.length; // Если сообщение не своё, добавляем строку для имени отправителя const headerHeight = message.sender && !isOwn ? lineHeight : 0; // Строка с временем внизу – тоже занимает место const footerHeight = lineHeight; const bubbleHeight = padding * 2 + headerHeight + lineHeight * textLineCount + footerHeight; // Определяем координаты пузыря в SVG, // если сообщение своё, хвост рисуем слева, иначе – справа. // Для удобного позиционирования контейнер SVG делаем шире, чем bubbleWidth. const offsetX = isOwn ? 30 : 0; // Цвета const bubbleColor = isOwn ? '#d4f0bc' : '#f5f5f5'; const shadowColor = 'rgba(0, 0, 0, 0.2)'; const textColor = '#000'; return ( <svg width={bubbleWidth + 30} height={bubbleHeight + 20} style={{ overflow: 'visible' }} > <defs> <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%"> <feDropShadow dx={isOwn ? -2 : 2} dy="4" stdDeviation="3" floodColor={shadowColor} /> </filter> </defs> <g filter={'url(#shadow)'}> {/* Основной прямоугольник – пузырь */} <rect x={0} y={0} width={bubbleWidth} height={bubbleHeight} rx="12" ry="12" fill={bubbleColor} /> {/* Хвост пузыря */} {isOwn ? ( <polygon points={` ${10},${bubbleHeight - 10} ${10},${bubbleHeight + 10} ${offsetX},${bubbleHeight} `} fill={bubbleColor} /> ) : ( <polygon points={` ${bubbleWidth - 10 + offsetX},${bubbleHeight - 10} ${bubbleWidth - 10 + offsetX},${bubbleHeight + 10} ${bubbleWidth + offsetX - 30},${bubbleHeight} `} fill={bubbleColor} /> )} </g> {/* Текст внутри пузыря */} <g fontFamily="Arial, sans-serif" fontSize="14" fill={textColor}> {/* Заголовок с именем отправителя, если есть и сообщение не своё */} {message.sender && !isOwn && ( <text x={offsetX + padding} y={padding + 14} fontWeight="bold"> {message.sender.username} </text> )} {/* Текст сообщения */} {textLines.map((line, index) => { // Если заголовок отрисован, смещаем первую строку вниз const yOffset = padding + (message.sender && !isOwn ? lineHeight : 0) + (index + 1) * lineHeight; return ( <text key={index} x={padding} y={yOffset}> {line} </text> ); })} {/* Временная метка – выровнена справа */} <text x={bubbleWidth - padding} y={bubbleHeight - 8} textAnchor="end"> {new Date(message.createdAt ?? '').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', })} </text> </g> </svg> ); };