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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user