uchill/front_material/components/chat/ChatList.tsx

269 lines
9.4 KiB
TypeScript

'use client';
import React from 'react';
import { Box, TextField, List, ListItemButton, ListItemText, Badge, Avatar, Button, IconButton } from '@mui/material';
import NotificationsActiveRoundedIcon from '@mui/icons-material/NotificationsActiveRounded';
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
import AssignmentRoundedIcon from '@mui/icons-material/AssignmentRounded';
import AddCommentRoundedIcon from '@mui/icons-material/AddCommentRounded';
import type { Chat } from '@/api/chat';
import { NewChatModal } from './NewChatModal';
interface ChatListProps {
chats: Chat[];
selectedChatUuid: string | null;
onSelect: (chat: Chat) => void;
hasMore?: boolean;
loadingMore?: boolean;
onLoadMore?: () => void;
}
function parsePreview(text: string) {
const t = (text || '').trim();
if (!t) return { icons: [] as Array<'bell' | 'trash' | 'note'>, text: '—' };
const icons: Array<'bell' | 'trash' | 'note'> = [];
let rest = t;
// выкусываем частые эмодзи-пиктограммы из system сообщений
const take = (emoji: string, key: 'bell' | 'trash' | 'note') => {
if (rest.includes(emoji)) {
icons.push(key);
rest = rest.replace(emoji, '').trim();
}
};
take('🔔', 'bell');
take('🗑️', 'trash');
take('🗑', 'trash');
take('📝', 'note');
// убираем лишние эмодзи/разделители в начале
rest = rest.replace(/^[-–—•\s]+/, '').trim();
return { icons, text: rest || '—' };
}
export function ChatList({ chats, selectedChatUuid, onSelect, hasMore, loadingMore, onLoadMore }: ChatListProps) {
const [q, setQ] = React.useState('');
const [isNewChatModalOpen, setIsNewChatModalOpen] = React.useState(false);
const filtered = React.useMemo(() => {
const qq = q.trim().toLowerCase();
if (!qq) return chats;
return chats.filter((c) => {
const name = (c.participant_name || '').toLowerCase();
const last = (c.last_message || '').toLowerCase();
return name.includes(qq) || last.includes(qq);
});
}, [chats, q]);
return (
<Box
className="ios-glass-panel"
data-tour="chat-list"
sx={{
borderRadius: '20px',
p: 2,
height: '100%',
display: 'flex',
flexDirection: 'column',
gap: 1.5,
background: 'var(--ios26-glass)',
border: '1px solid var(--ios26-glass-border)',
backdropFilter: 'var(--ios26-blur)',
}}
>
<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Поиск"
size="small"
fullWidth
sx={{
'& .MuiInputBase-root': {
borderRadius: 3,
backgroundColor: 'rgba(255,255,255,0.7)',
},
}}
/>
<IconButton
onClick={() => setIsNewChatModalOpen(true)}
sx={{
bgcolor: 'var(--md-sys-color-primary)',
color: 'var(--md-sys-color-on-primary)',
'&:hover': { bgcolor: 'var(--md-sys-color-primary)', filter: 'brightness(0.9)' }
}}
>
<AddCommentRoundedIcon />
</IconButton>
</Box>
<Box
sx={{
flex: 1,
minHeight: 0,
overflowY: 'auto',
overflowX: 'hidden',
pr: 0.5,
}}
>
<List disablePadding>
{filtered.map((chat) => {
const selected = !!selectedChatUuid && chat.uuid === selectedChatUuid;
const preview = parsePreview(chat.last_message || '');
return (
<ListItemButton
key={chat.uuid || chat.id}
selected={selected}
onClick={() => onSelect(chat)}
sx={{
borderRadius: 2,
mb: 0.5,
minWidth: 0,
'&.Mui-selected': {
backgroundColor: 'rgba(116, 68, 253, 0.12)',
},
}}
>
<Box sx={{ position: 'relative', mr: 1.25, width: 36, height: 36, flex: '0 0 36px' }}>
<Avatar
src={(chat as any).avatar_url || undefined}
alt={chat.participant_name || 'Аватар'}
sx={{
width: 36,
height: 36,
bgcolor: 'rgba(116, 68, 253, 0.18)',
color: 'var(--md-sys-color-primary)',
fontWeight: 800,
fontSize: 13,
}}
>
{(chat.participant_name || 'Ч')
.trim()
.split(/\s+/)
.slice(0, 2)
.map((p) => p[0])
.join('')
.toUpperCase()}
</Avatar>
{!!(chat as any).other_is_online && (
<Box
sx={{
position: 'absolute',
right: -1,
bottom: -1,
width: 10,
height: 10,
borderRadius: '999px',
backgroundColor: '#22c55e',
border: '2px solid rgba(255,255,255,0.9)',
boxShadow: '0 0 0 1px rgba(0,0,0,0.06)',
}}
/>
)}
</Box>
<ListItemText
primary={
<Box
component="span"
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}
>
<Box
component="span"
sx={{ fontWeight: 700, fontSize: 14, color: 'var(--md-sys-color-on-surface)' }}
>
{chat.participant_name || 'Чат'}
</Box>
{!!chat.unread_count && chat.unread_count > 0 && (
<Badge
color="primary"
badgeContent={chat.unread_count}
sx={{
'& .MuiBadge-badge': {
backgroundColor: 'var(--md-sys-color-primary)',
color: 'var(--md-sys-color-on-primary)',
},
}}
/>
)}
</Box>
}
secondary={
<Box
component="span"
sx={{
fontSize: 12,
color: 'var(--md-sys-color-on-surface-variant)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
pr: 1,
display: 'flex',
alignItems: 'center',
gap: 0.75,
}}
>
{preview.icons.includes('bell') && (
<NotificationsActiveRoundedIcon sx={{ fontSize: 16, color: 'var(--md-sys-color-primary)' }} />
)}
{preview.icons.includes('trash') && (
<DeleteRoundedIcon sx={{ fontSize: 16, color: 'var(--md-sys-color-on-surface-variant)' }} />
)}
{preview.icons.includes('note') && (
<AssignmentRoundedIcon sx={{ fontSize: 16, color: 'var(--md-sys-color-primary)' }} />
)}
<Box
component="span"
sx={{
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{preview.text}
</Box>
</Box>
}
sx={{ minWidth: 0 }}
primaryTypographyProps={{ component: 'span' }}
secondaryTypographyProps={{ component: 'span' }}
/>
</ListItemButton>
);
})}
</List>
{hasMore && onLoadMore && (
<Box sx={{ py: 1.5, display: 'flex', justifyContent: 'center' }}>
<Button
variant="outlined"
size="small"
onClick={onLoadMore}
disabled={loadingMore}
sx={{
borderRadius: 2,
textTransform: 'none',
borderColor: 'var(--md-sys-color-outline-variant)',
color: 'var(--md-sys-color-on-surface-variant)',
}}
>
{loadingMore ? 'Загрузка…' : 'Загрузить ещё'}
</Button>
</Box>
)}
</Box>
<NewChatModal
isOpen={isNewChatModalOpen}
onClose={() => setIsNewChatModalOpen(false)}
onChatCreated={(chat) => {
onSelect(chat);
// Можно добавить уведомление
}}
/>
</Box>
);
}