Backend: initialize a Django session on token obtain (login) so AuthMiddlewareStack can authenticate WebSocket connections; validate the token serializer and map TokenError -> InvalidToken; call django_logout on logout to destroy the session. Frontend: add a CreateChatModal component (DM/group creation) with user search, selection, validation, API mutation and cache invalidation; wire modal into ChatSidebar and add Czech translations for the new UI strings.
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import { useState, useMemo } from "react";
|
|
import { NavLink } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FiSearch, FiPlus } from "react-icons/fi";
|
|
import { useApiSocialChatsList } from "@/api/generated/private/chat/chat";
|
|
import Avatar from "@/components/ui/Avatar";
|
|
import Spinner from "@/components/ui/Spinner";
|
|
import EmptyState from "@/components/ui/EmptyState";
|
|
import IconButton from "@/components/ui/IconButton";
|
|
import CreateChatModal from "./CreateChatModal";
|
|
|
|
export default function ChatSidebar() {
|
|
const { t } = useTranslation("social");
|
|
const [query, setQuery] = useState("");
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
|
|
const { data, isLoading } = useApiSocialChatsList(
|
|
query ? { search: query } : undefined,
|
|
);
|
|
|
|
const chats = useMemo(() => data?.results ?? [], [data]);
|
|
|
|
return (
|
|
<aside className="flex h-full flex-col border-r border-brand-lines/15">
|
|
<header className="flex items-center justify-between gap-2 border-b border-brand-lines/10 px-3 py-3">
|
|
<h2 className="text-sm font-semibold text-brand-text">
|
|
{t("chat.sidebar.title")}
|
|
</h2>
|
|
<IconButton
|
|
icon={<FiPlus size={16} />}
|
|
label={t("chat.sidebar.new")}
|
|
onClick={() => setCreateOpen(true)}
|
|
/>
|
|
</header>
|
|
|
|
<div className="relative px-3 py-2">
|
|
<FiSearch
|
|
className="absolute left-5 top-1/2 -translate-y-1/2 text-brand-text/50"
|
|
size={14}
|
|
/>
|
|
<input
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder={t("chat.sidebar.search")}
|
|
className="w-full rounded-xl bg-brand-bgLight/40 border border-brand-lines/20 py-2 pl-8 pr-2 text-sm text-brand-text placeholder:text-brand-text/40 focus:outline-none focus:border-brand-accent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto">
|
|
{isLoading && (
|
|
<div className="flex justify-center py-6">
|
|
<Spinner size={20} />
|
|
</div>
|
|
)}
|
|
|
|
{!isLoading && chats.length === 0 && (
|
|
<EmptyState message={t("chat.sidebar.empty")} />
|
|
)}
|
|
|
|
<ul>
|
|
{chats.map((chat) => (
|
|
<li key={chat.id}>
|
|
<NavLink
|
|
to={`/social/chats/${chat.id}`}
|
|
className={({ isActive }) =>
|
|
[
|
|
"flex items-center gap-3 px-3 py-2.5 hover:bg-brand-lines/10 transition-colors",
|
|
isActive ? "bg-brand-lines/10" : "",
|
|
].join(" ")
|
|
}
|
|
>
|
|
<Avatar
|
|
name={chat.name ?? `chat ${chat.id}`}
|
|
src={chat.icon ?? undefined}
|
|
size={36}
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-sm font-semibold text-brand-text">
|
|
{chat.name || `Chat #${chat.id}`}
|
|
</div>
|
|
<div className="truncate text-xs text-brand-text/60">
|
|
{chat.chat_type}
|
|
</div>
|
|
</div>
|
|
{(chat.unread_count ?? 0) > 0 && (
|
|
<span className="ml-auto shrink-0 rounded-full bg-brand-accent px-1.5 py-0.5 text-[10px] font-bold leading-none text-brand-bg">
|
|
{(chat.unread_count ?? 0) > 99 ? "99+" : chat.unread_count}
|
|
</span>
|
|
)}
|
|
</NavLink>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<CreateChatModal
|
|
open={createOpen}
|
|
onClose={() => setCreateOpen(false)}
|
|
/>
|
|
</aside>
|
|
);
|
|
}
|