uchill/front_material/app/(protected)/chat/page.tsx

271 lines
9.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { Box, Typography } from '@mui/material';
import { getConversations, getChatById } from '@/api/chat';
import type { Chat } from '@/api/chat';
import { ChatList } from '@/components/chat/ChatList';
import { ChatWindow } from '@/components/chat/ChatWindow';
import { usePresenceWebSocket } from '@/hooks/usePresenceWebSocket';
import { useAuth } from '@/contexts/AuthContext';
import { useNavBadgesRefresh } from '@/contexts/NavBadgesContext';
import { useIsMobile } from '@/hooks/useIsMobile';
export default function ChatPage() {
const { user } = useAuth();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const uuidFromUrl = searchParams.get('uuid');
const isMobile = useIsMobile();
const [loading, setLoading] = React.useState(true);
const [chats, setChats] = React.useState<Chat[]>([]);
const [selected, setSelected] = React.useState<Chat | null>(null);
const [error, setError] = React.useState<string | null>(null);
const [hasMore, setHasMore] = React.useState(false);
const [page, setPage] = React.useState(1);
const [loadingMore, setLoadingMore] = React.useState(false);
usePresenceWebSocket({ enabled: true });
const refreshNavBadges = useNavBadgesRefresh();
// На странице чата не должно быть общего скролла (скроллим только панели внутри)
React.useEffect(() => {
const prevHtml = document.documentElement.style.overflow;
const prevBody = document.body.style.overflow;
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
return () => {
document.documentElement.style.overflow = prevHtml;
document.body.style.overflow = prevBody;
};
}, []);
const normalizeChat = React.useCallback((c: any) => {
const otherName =
c?.other_participant?.full_name ||
[c?.other_participant?.first_name, c?.other_participant?.last_name].filter(Boolean).join(' ') ||
c?.participant_name ||
'Чат';
const avatarUrl = c?.other_participant?.avatar_url || c?.other_participant?.avatar || null;
const otherId = c?.other_participant?.id ?? null;
const otherOnline = !!c?.other_participant?.is_online;
const otherLast = c?.other_participant?.last_activity ?? null;
const lastText = c?.last_message?.content || c?.last_message?.text || c?.last_message || '';
const unread = c?.my_participant?.unread_count ?? c?.unread_count ?? 0;
return {
id: c.id,
uuid: c.uuid,
participant_name: otherName,
avatar_url: avatarUrl,
other_user_id: otherId,
other_is_online: otherOnline,
other_last_activity: otherLast,
last_message: lastText,
unread_count: unread,
created_at: c.created_at,
};
}, []);
React.useEffect(() => {
(async () => {
try {
setLoading(true);
setError(null);
const resp = await getConversations({ page: 1, page_size: 30 });
const normalized = (resp.results || []).map((c: any) => normalizeChat(c));
setChats(normalized as any);
setHasMore(!!(resp as any).next);
setPage(1);
} catch (e: any) {
console.error('[ChatPage] Ошибка загрузки чатов:', e);
const msg =
e?.response?.data?.detail ||
e?.response?.data?.error ||
e?.message ||
'Не удалось загрузить чаты';
setError(String(msg));
} finally {
setLoading(false);
}
})();
}, [normalizeChat]);
const restoredForUuidRef = React.useRef<string | null>(null);
// Восстановить выбранный чат из URL после загрузки списка (или по uuid)
React.useEffect(() => {
if (loading || error || !uuidFromUrl) return;
if (restoredForUuidRef.current === uuidFromUrl) return;
const found = chats.find((c) => (c as any).uuid === uuidFromUrl);
if (found) {
setSelected(found as Chat);
restoredForUuidRef.current = uuidFromUrl;
return;
}
(async () => {
try {
const c = await getChatById(uuidFromUrl);
const normalized = normalizeChat(c) as any;
setSelected(normalized as Chat);
restoredForUuidRef.current = uuidFromUrl;
} catch (e: any) {
console.warn('[ChatPage] Чат по uuid из URL не найден:', uuidFromUrl, e);
restoredForUuidRef.current = null;
router.replace(pathname ?? '/chat');
}
})();
}, [loading, error, uuidFromUrl, chats, normalizeChat, router, pathname]);
React.useEffect(() => {
if (!uuidFromUrl) restoredForUuidRef.current = null;
}, [uuidFromUrl]);
const handleSelectChat = React.useCallback(
(c: Chat) => {
setSelected(c);
const u = (c as any).uuid;
if (u) {
const base = pathname ?? '/chat';
router.replace(`${base}?uuid=${encodeURIComponent(u)}`);
}
},
[router, pathname]
);
const loadMore = React.useCallback(async () => {
if (loadingMore || !hasMore) return;
try {
setLoadingMore(true);
const next = page + 1;
const resp = await getConversations({ page: next, page_size: 30 });
const normalized = (resp.results || []).map((c: any) => normalizeChat(c));
setChats((prev) => [...prev, ...(normalized as any)]);
setHasMore(!!(resp as any).next);
setPage(next);
} catch (e: any) {
console.error('[ChatPage] Ошибка загрузки чатов:', e);
} finally {
setLoadingMore(false);
}
}, [page, hasMore, loadingMore, normalizeChat]);
const refreshChatListUnread = React.useCallback(async () => {
try {
const resp = await getConversations({ page: 1, page_size: 30 });
const fresh = (resp.results || []).map((c: any) => normalizeChat(c)) as Chat[];
const freshByUuid = new Map(fresh.map((c: any) => [(c as any).uuid, c]));
setChats((prev) =>
prev.map((c: any) => {
const updated = freshByUuid.get(c.uuid);
return updated ? (updated as Chat) : c;
})
);
await refreshNavBadges?.();
} catch {
// ignore
}
}, [normalizeChat, refreshNavBadges]);
const handleBackToList = React.useCallback(() => {
setSelected(null);
router.replace(pathname ?? '/chat');
}, [router, pathname]);
// Mobile: show only list or only chat
const mobileShowChat = isMobile && selected != null;
// Hide bottom navigation when a chat is open on mobile
React.useEffect(() => {
if (mobileShowChat) {
document.documentElement.classList.add('mobile-chat-open');
} else {
document.documentElement.classList.remove('mobile-chat-open');
}
return () => {
document.documentElement.classList.remove('mobile-chat-open');
};
}, [mobileShowChat]);
return (
<div className="ios26-dashboard ios26-chat-page" data-tour="chat-root" style={{ padding: isMobile ? '8px' : '16px' }}>
<Box
className="ios26-chat-layout"
sx={{
display: isMobile ? 'flex' : 'grid',
gridTemplateColumns: isMobile ? undefined : '320px 1fr',
flexDirection: isMobile ? 'column' : undefined,
gap: 'var(--ios26-spacing)',
alignItems: 'stretch',
height: isMobile ? '100%' : 'calc(90vh - 32px)',
maxHeight: isMobile ? undefined : 'calc(90vh - 32px)',
overflow: 'hidden',
}}
>
{/* Chat list: hidden on mobile when a chat is selected */}
{!mobileShowChat && (
<>
{loading ? (
<Box
className="ios-glass-panel"
sx={{
borderRadius: '20px',
p: 2,
display: 'flex',
flex: isMobile ? 1 : undefined,
alignItems: 'center',
justifyContent: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
background: 'var(--ios26-glass)',
border: '1px solid var(--ios26-glass-border)',
backdropFilter: 'var(--ios26-blur)',
}}
>
<Typography>Загрузка</Typography>
</Box>
) : error ? (
<Box
className="ios-glass-panel"
sx={{
borderRadius: '20px',
p: 2,
display: 'flex',
flex: isMobile ? 1 : undefined,
alignItems: 'center',
justifyContent: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
background: 'var(--ios26-glass)',
border: '1px solid var(--ios26-glass-border)',
backdropFilter: 'var(--ios26-blur)',
}}
>
<Typography>{error}</Typography>
</Box>
) : (
<ChatList
chats={chats}
selectedChatUuid={selected?.uuid ?? (selected as any)?.uuid ?? null}
onSelect={handleSelectChat}
hasMore={hasMore}
loadingMore={loadingMore}
onLoadMore={loadMore}
/>
)}
</>
)}
{/* Chat window: on mobile only visible when a chat is selected */}
{(!isMobile || mobileShowChat) && (
<ChatWindow
chat={selected}
currentUserId={user?.id ?? null}
onBack={isMobile ? handleBackToList : undefined}
onMessagesMarkedAsRead={refreshChatListUnread}
/>
)}
</Box>
</div>
);
}