269 lines
9.6 KiB
TypeScript
269 lines
9.6 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>
|
|
);
|
|
}
|
|
|