From a8ec33956514c57ebe9df3a30336f5b625e73ae7 Mon Sep 17 00:00:00 2001 From: lizerk Date: Wed, 1 Apr 2026 15:31:19 +0300 Subject: [PATCH] Refactor chat settings and tighten frontend types --- frontend/src/app/messages/[chatId]/page.tsx | 16 +- .../app/messages/[chatId]/settings/page.tsx | 529 ++---------------- .../src/components/NotificationCenter.tsx | 53 +- .../messages/AddChatMemberModal.tsx | 110 ++++ .../messages/ChatParticipantsSection.tsx | 219 ++++++++ .../src/components/messages/EditChatModal.tsx | 121 ++++ .../src/contexts/NotificationsContext.tsx | 20 +- frontend/src/hooks/useWebSocket.ts | 7 + .../src/lib/messages/chatSettingsUtils.ts | 81 +++ 9 files changed, 646 insertions(+), 510 deletions(-) create mode 100644 frontend/src/components/messages/AddChatMemberModal.tsx create mode 100644 frontend/src/components/messages/ChatParticipantsSection.tsx create mode 100644 frontend/src/components/messages/EditChatModal.tsx create mode 100644 frontend/src/lib/messages/chatSettingsUtils.ts diff --git a/frontend/src/app/messages/[chatId]/page.tsx b/frontend/src/app/messages/[chatId]/page.tsx index 0b478ad9..3b07589e 100644 --- a/frontend/src/app/messages/[chatId]/page.tsx +++ b/frontend/src/app/messages/[chatId]/page.tsx @@ -252,6 +252,10 @@ export default function MessageDialogPage() { handlers.current.onMessage = (data) => { if (data.type === 'new_message') { // Новое сообщение + if (!data.message) { + return; + } + const newMsg = data.message; const isMyMessage = newMsg.author_id === user?.id; const wasNearBottom = isNearBottom(); @@ -313,6 +317,10 @@ export default function MessageDialogPage() { } else if (data.type === 'message_edited') { // Редактирование сообщения + if (!data.message) { + return; + } + const editedMsg = data.message; setMessages(prev => prev.map(m => m.id === editedMsg.id ? { ...m, ...editedMsg } : m @@ -347,22 +355,24 @@ export default function MessageDialogPage() { } else if (data.type === 'reaction_added') { // Реакция добавлена - if (data.reactions_summary && data.message_id) { + if (data.reactions_summary && typeof data.message_id === 'number') { updateMessageReactionsSummary(data.message_id, data.reactions_summary); } } else if (data.type === 'reaction_removed') { // Реакция удалена - if (data.reactions_summary && data.message_id) { + if (data.reactions_summary && typeof data.message_id === 'number') { updateMessageReactionsSummary(data.message_id, data.reactions_summary); } } else if (data.type === 'marked_read' && data.chat_id === chatId && typeof data.last_read_message_id === 'number') { + const lastReadMessageId = data.last_read_message_id; + setChat((prev) => prev ? { ...prev, - last_read_message_id: Math.max(prev.last_read_message_id ?? 0, data.last_read_message_id), + last_read_message_id: Math.max(prev.last_read_message_id ?? 0, lastReadMessageId), } : prev ); diff --git a/frontend/src/app/messages/[chatId]/settings/page.tsx b/frontend/src/app/messages/[chatId]/settings/page.tsx index ac0571ee..10542b71 100644 --- a/frontend/src/app/messages/[chatId]/settings/page.tsx +++ b/frontend/src/app/messages/[chatId]/settings/page.tsx @@ -10,46 +10,20 @@ import { BellOff, Pin, Trash2, - UserPlus, UserMinus, - Users, - Pencil, - ChevronDown, - Shield, - ShieldCheck + Pencil } from "lucide-react"; import { AppShell } from "../../../../components/AppShell"; import { apiClient } from "@/lib/api"; -import { getChatAvatar, getChatInitials, getChatTitle, getUserFullName } from "@/lib/messages/chatUtils"; -import type { Chat, User } from "@/types/api"; +import { getChatAvatar, getChatInitials, getChatTitle } from "@/lib/messages/chatUtils"; +import ChatParticipantsSection from "@/components/messages/ChatParticipantsSection"; +import EditChatModal from "@/components/messages/EditChatModal"; +import AddChatMemberModal from "@/components/messages/AddChatMemberModal"; +import { getChatTypeLabel, type MemberSearchResult } from "@/lib/messages/chatSettingsUtils"; +import type { Chat } from "@/types/api"; import { useUser } from "@/contexts/UserContext"; import { resolveMediaUrl } from "@/lib/url"; -type MemberSearchResult = Pick & { - name?: string; -}; - -function getChatTypeLabel(chat: Chat): string { - const type = chat.chat_type || chat.type; - switch (type) { - case "global": - return "Глобальный чат"; - case "channel": - return "Канал"; - case "private": - case "direct": - return "Личный чат"; - case "group": - return "Групповой чат"; - case "announcement": - return "Канал объявлений"; - case "comments": - return "Комментарии"; - default: - return "Диалог"; - } -} - export default function ChatSettingsPage() { const params = useParams<{ chatId: string }>(); const router = useRouter(); @@ -264,7 +238,7 @@ export default function ChatSettingsPage() { } // Перезагружаем данные чата - const refreshedChat = await apiClient.getChat(chatId); + const refreshedChat: Chat = await apiClient.getChat(chatId); setChat(refreshedChat); handleCloseEditModal(); @@ -323,7 +297,7 @@ export default function ChatSettingsPage() { await apiClient.addChatMember(chatId, userId); // Перезагружаем данные чата - const refreshedChat = await apiClient.getChat(chatId); + const refreshedChat: Chat = await apiClient.getChat(chatId); setChat(refreshedChat); // Обновляем результаты поиска с учетом новых данных @@ -390,7 +364,7 @@ export default function ChatSettingsPage() { }); // Перезагружаем данные чата с сервера для синхронизации - const refreshedChat = await apiClient.getChat(chatId); + const refreshedChat: Chat = await apiClient.getChat(chatId); console.log('[handleChangeRole] Refreshed chat from server:', refreshedChat); console.log('[handleChangeRole] Refreshed memberships:', refreshedChat.memberships); setChat(refreshedChat); @@ -458,37 +432,6 @@ export default function ChatSettingsPage() { ) ?? false; const canEdit = isOwner || isAdmin; - // Получить роль участника - const getMemberRole = (userId: number): 'admin' | 'moderator' | 'member' | 'guest' | null => { - if (chat.created_by === userId) return null; // Владелец не имеет роли в membership - const membership = chat.memberships?.find(m => m.user === userId); - return membership?.role || 'member'; - }; - - // Получить отображаемое имя роли - const getRoleLabel = (role: 'admin' | 'moderator' | 'member' | 'guest' | null): string => { - if (role === null) return 'Владелец'; - switch (role) { - case 'admin': return 'Админ'; - case 'moderator': return 'Модератор'; - case 'member': return 'Участник'; - case 'guest': return 'Гость'; - default: return 'Участник'; - } - }; - - // Цвет бейджа роли - const getRoleBadgeColor = (role: 'admin' | 'moderator' | 'member' | 'guest' | null): string => { - if (role === null) return 'bg-purple-100 text-purple-700 ring-purple-200'; // Владелец - switch (role) { - case 'admin': return 'bg-red-100 text-red-700 ring-red-200'; - case 'moderator': return 'bg-blue-100 text-blue-700 ring-blue-200'; - case 'member': return 'bg-gray-100 text-gray-700 ring-gray-200'; - case 'guest': return 'bg-amber-100 text-amber-700 ring-amber-200'; - default: return 'bg-gray-100 text-gray-700 ring-gray-200'; - } - }; - return (
@@ -606,219 +549,21 @@ export default function ChatSettingsPage() { {/* Участники (для групповых чатов, каналов и объявлений) */} {hasMembers && ( -
-
-
-

Участники

- {isOwner && ( -

- Вы можете изменять роли участников -

- )} -
- {canEdit && ( - - )} -
- -
- {(chat.participant_details || []) - .slice(0, showAllParticipants ? undefined : 5) - .map((participant) => { - const memberRole = getMemberRole(participant.id); - const roleLabel = getRoleLabel(memberRole); - const roleBadgeColor = getRoleBadgeColor(memberRole); - const isCurrentUserOwner = chat.created_by === participant.id; - - return ( -
-
- {participant.avatar ? ( - {participant.name - ) : ( - - {(participant.name || "?") - .split(" ") - .filter(Boolean) - .slice(0, 2) - .map((p) => p[0]) - .join("") - .toUpperCase()} - - )} -
- -
-

- {participant.name || "Без имени"} - {participant.id === currentUserId && ( - (вы) - )} -

-
- - {isCurrentUserOwner && } - {!isCurrentUserOwner && memberRole === 'admin' && } - {roleLabel} - -
-
- - {/* Управление ролью (только для владельца) */} - {isOwner && !isCurrentUserOwner && participant.id !== currentUserId && ( -
- - - {roleDropdownOpen === participant.id && ( - <> - - - - -
- - )} -
- )} - - {/* Кнопка удаления */} - {canEdit && !isCurrentUserOwner && participant.id !== currentUserId && ( - - )} -
- ); - })} -
- - {/* Кнопка показать всех участников */} - {(chat.participant_details || []).length > 5 && ( - - )} - - {/* Легенда ролей */} - {isOwner && ( -
-

Роли участников:

-
-
- - - Владелец - - — полный контроль над чатом -
-
- - - Админ - - — управление участниками, редактирование -
-
- - - Модератор - - — закрепление и удаление сообщений -
-
- - Участник - - — отправка сообщений -
-
- - Гость - - — только чтение -
-
-
- )} - + setShowAllParticipants((prev) => !prev)} + onToggleRoleDropdown={(participantId) => setRoleDropdownOpen((prev) => prev === participantId ? null : participantId)} + onCloseRoleDropdown={() => setRoleDropdownOpen(null)} + onChangeRole={handleChangeRole} + onRemoveMember={handleRemoveMember} + /> )} {/* Опасная зона */} @@ -869,207 +614,31 @@ export default function ChatSettingsPage() { )} - {/* Модал редактирования чата */} - {isEditModalOpen && chat && ( -
-
-

Редактировать чат

- -
- {/* Аватар */} -
- -
-
- {editAvatarPreview ? ( - Preview - ) : getChatAvatar(chat, currentUserId) ? ( - Avatar - ) : ( - getChatInitials(chat, currentUserId) - )} -
- -
-

- Максимальный размер: 5MB. Форматы: JPG, PNG, GIF -

-
+ - {/* Название */} -
- - setEditName(e.target.value)} - placeholder="Введите название чата" - className="w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500/20" - maxLength={100} - /> -
- - {/* Описание */} -
- -