Add chat settings modal & reply previews

Introduce a full ChatSettingsModal component for managing group chats (rename/icon upload, add/remove members, toggle moderators, leave/delete) and wire it into ChatRoomPage with a settings button. Add reply preview support end-to-end: new ReplyToSerializer and members_detail in backend serializers, updated frontend Message model (reply_to now contains preview object), and UI changes to Message to render reply excerpts. Improve socket handling to attach reply previews when available. Tweak backend Dockerfile to optionally install Windows/corporate CA bundle only if present and move pip install after copying source. Add Czech translations and small tooling/.claude config enhancements.
This commit is contained in:
2026-05-29 00:41:43 +02:00
parent 8269d044a2
commit 3d1965e5e6
8 changed files with 574 additions and 14 deletions

View File

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { FiMoreVertical } from "react-icons/fi";
import type { Message as MessageModel } from "@/api/generated/private/models/message";
import { getApiSocialChatsListQueryKey, useApiSocialChatsRetrieve } from "@/api/generated/private/chat/chat";
import { useAuth } from "@/hooks/useAuth";
@@ -11,6 +12,7 @@ import { useIntersectionLoader } from "@/hooks/useIntersectionLoader";
import { messagesQueryKey, type CursorPaginated } from "@/api/social/feed";
import Message from "@/components/social/chat/Message";
import MessageComposer from "@/components/social/chat/MessageComposer";
import ChatSettingsModal from "@/components/social/chat/ChatSettingsModal";
import Spinner from "@/components/ui/Spinner";
import EmptyState from "@/components/ui/EmptyState";
import Avatar from "@/components/ui/Avatar";
@@ -33,6 +35,7 @@ export default function ChatRoomPage() {
const [replyTo, setReplyTo] = useState<MessageModel | null>(null);
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const [settingsOpen, setSettingsOpen] = useState(false);
const bottomRef = useRef<HTMLDivElement | null>(null);
// Top sentinel triggers loading older messages (scroll-back).
@@ -85,11 +88,21 @@ export default function ChatRoomPage() {
const handleSocketEvent = useCallback(
(event: ChatSocketEvent) => {
if (event.type === "new_chat_message" || event.type === "new_reply_chat_message") {
let replyTo: MessageModel["reply_to"] = null;
if (event.type === "new_reply_chat_message" && event.reply_to_id != null) {
const cached = queryClient.getQueryData<{ pages: CursorPaginated<MessageModel>[] }>(
messagesQueryKey(chatId),
);
const found = cached?.pages.flatMap((p) => p.results).find((m) => m.id === event.reply_to_id);
replyTo = found
? { id: found.id, content: found.content, sender: found.sender }
: { id: event.reply_to_id, content: undefined, sender: { id: 0, username: "…", avatar: null } };
}
appendMessage({
id: event.message_id,
chat: chatId,
sender: { id: event.sender_id, username: event.sender, avatar: event.sender_avatar } as never,
reply_to: event.type === "new_reply_chat_message" ? event.reply_to_id : null,
reply_to: replyTo,
content: event.message,
is_edited: false,
edited_at: null,
@@ -160,8 +173,24 @@ export default function ChatRoomPage() {
</div>
)}
</div>
<button
type="button"
onClick={() => setSettingsOpen(true)}
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-brand-text/60 hover:bg-brand-lines/15 hover:text-brand-text transition-colors"
aria-label={t("chat.settings.title")}
>
<FiMoreVertical size={17} />
</button>
</header>
{chat && (
<ChatSettingsModal
chat={chat as Parameters<typeof ChatSettingsModal>[0]["chat"]}
open={settingsOpen}
onClose={() => setSettingsOpen(false)}
/>
)}
<div className="flex-1 overflow-y-auto py-2">
{hasNextPage && (
<div ref={topSentinelRef} className="flex justify-center py-2">