feat(web): redesign chat ui in telegram-like style
All checks were successful
CI / test (push) Successful in 21s
All checks were successful
CI / test (push) Successful in 21s
- update overall layout for desktop/mobile chat navigation - restyle dialogs list, message bubbles and composer - add atmospheric background and unified panel styling
This commit is contained in:
@@ -3,22 +3,53 @@ import { NewChatPanel } from "./NewChatPanel";
|
|||||||
|
|
||||||
export function ChatList() {
|
export function ChatList() {
|
||||||
const chats = useChatStore((s) => s.chats);
|
const chats = useChatStore((s) => s.chats);
|
||||||
|
const messagesByChat = useChatStore((s) => s.messagesByChat);
|
||||||
const activeChatId = useChatStore((s) => s.activeChatId);
|
const activeChatId = useChatStore((s) => s.activeChatId);
|
||||||
const setActiveChatId = useChatStore((s) => s.setActiveChatId);
|
const setActiveChatId = useChatStore((s) => s.setActiveChatId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-full max-w-xs border-r border-slate-700 bg-panel">
|
<aside className="flex h-full w-full max-w-xs flex-col bg-slate-900/60">
|
||||||
<div className="border-b border-slate-700 p-3 text-sm font-semibold">Chats</div>
|
<div className="border-b border-slate-700/50 px-4 py-3">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<p className="text-base font-semibold tracking-wide">Chats</p>
|
||||||
|
<span className="rounded-full bg-slate-700/70 px-2 py-1 text-[11px] text-slate-200">{chats.length}</span>
|
||||||
|
</div>
|
||||||
|
<label className="block">
|
||||||
|
<input
|
||||||
|
className="w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
|
||||||
|
placeholder="Search chats..."
|
||||||
|
disabled
|
||||||
|
value=""
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<NewChatPanel />
|
<NewChatPanel />
|
||||||
<div className="max-h-[calc(100vh-56px)] overflow-auto">
|
|
||||||
|
<div className="tg-scrollbar flex-1 overflow-auto">
|
||||||
{chats.map((chat) => (
|
{chats.map((chat) => (
|
||||||
<button
|
<button
|
||||||
className={`block w-full border-b border-slate-800 px-3 py-3 text-left ${activeChatId === chat.id ? "bg-slate-800" : "hover:bg-slate-800/40"}`}
|
className={`block w-full border-b border-slate-800/60 px-4 py-3 text-left transition ${
|
||||||
|
activeChatId === chat.id ? "bg-sky-500/20" : "hover:bg-slate-800/65"
|
||||||
|
}`}
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
onClick={() => setActiveChatId(chat.id)}
|
onClick={() => setActiveChatId(chat.id)}
|
||||||
>
|
>
|
||||||
<p className="font-medium">{chat.title || `${chat.type} #${chat.id}`}</p>
|
<div className="flex items-start gap-3">
|
||||||
<p className="text-xs text-slate-400">{chat.type}</p>
|
<div className="mt-0.5 flex h-10 w-10 items-center justify-center rounded-full bg-sky-500/30 text-sm font-semibold uppercase text-sky-100">
|
||||||
|
{(chat.title || chat.type).slice(0, 1)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<p className="truncate text-sm font-semibold">{chat.title || `${chat.type} #${chat.id}`}</p>
|
||||||
|
<span className="shrink-0 text-[11px] text-slate-400">
|
||||||
|
{messagesByChat[chat.id]?.length ? "now" : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="truncate text-xs text-slate-400">{chat.type}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -238,11 +238,26 @@ export function MessageComposer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-slate-700 bg-panel p-3">
|
<div className="border-t border-slate-700/50 bg-slate-900/55 p-3">
|
||||||
<div className="mb-2 flex gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<label className="cursor-pointer rounded-full bg-slate-700/80 px-3 py-2 text-xs font-semibold hover:bg-slate-700">
|
||||||
|
+
|
||||||
<input
|
<input
|
||||||
className="flex-1 rounded bg-slate-800 px-3 py-2"
|
className="hidden"
|
||||||
placeholder="Type message..."
|
type="file"
|
||||||
|
disabled={isUploading}
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
selectFile(file);
|
||||||
|
}
|
||||||
|
e.currentTarget.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="flex-1 rounded-full border border-slate-700/80 bg-slate-800/80 px-4 py-2.5 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
|
||||||
|
placeholder="Write a message..."
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setText(e.target.value);
|
setText(e.target.value);
|
||||||
@@ -252,12 +267,12 @@ export function MessageComposer() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button className="rounded bg-accent px-3 py-2 font-semibold text-black" onClick={handleSend}>
|
<button className="rounded-full bg-sky-500 px-4 py-2.5 text-sm font-semibold text-slate-950 hover:bg-sky-400" onClick={handleSend}>
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{selectedFile ? (
|
{selectedFile ? (
|
||||||
<div className="mb-2 rounded border border-slate-700 bg-slate-900 p-3 text-sm">
|
<div className="mb-2 rounded-xl border border-slate-700/80 bg-slate-900/95 p-3 text-sm">
|
||||||
<div className="mb-2 font-semibold">Ready to send</div>
|
<div className="mb-2 font-semibold">Ready to send</div>
|
||||||
<div className="mb-1 break-all text-slate-300">{selectedFile.name}</div>
|
<div className="mb-1 break-all text-slate-300">{selectedFile.name}</div>
|
||||||
<div className="mb-2 text-xs text-slate-400">{formatBytes(selectedFile.size)}</div>
|
<div className="mb-2 text-xs text-slate-400">{formatBytes(selectedFile.size)}</div>
|
||||||
@@ -277,13 +292,13 @@ export function MessageComposer() {
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
className="rounded bg-accent px-3 py-1 font-semibold text-black disabled:opacity-50"
|
className="rounded-lg bg-sky-500 px-3 py-1 font-semibold text-slate-950 disabled:opacity-50"
|
||||||
onClick={() => void sendSelectedFile()}
|
onClick={() => void sendSelectedFile()}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
>
|
>
|
||||||
Send media
|
Send media
|
||||||
</button>
|
</button>
|
||||||
<button className="rounded bg-slate-700 px-3 py-1 disabled:opacity-50" onClick={cancelSelectedFile} disabled={isUploading}>
|
<button className="rounded-lg bg-slate-700 px-3 py-1 disabled:opacity-50" onClick={cancelSelectedFile} disabled={isUploading}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,25 +306,10 @@ export function MessageComposer() {
|
|||||||
) : null}
|
) : null}
|
||||||
{uploadError ? <div className="mb-2 text-sm text-red-400">{uploadError}</div> : null}
|
{uploadError ? <div className="mb-2 text-sm text-red-400">{uploadError}</div> : null}
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<label className="cursor-pointer rounded bg-slate-700 px-3 py-1">
|
<button className="rounded-lg bg-slate-700/90 px-3 py-1.5 disabled:opacity-50" onClick={startRecord} disabled={isUploading || isRecording}>
|
||||||
Upload
|
|
||||||
<input
|
|
||||||
className="hidden"
|
|
||||||
type="file"
|
|
||||||
disabled={isUploading}
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
selectFile(file);
|
|
||||||
}
|
|
||||||
e.currentTarget.value = "";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button className="rounded bg-slate-700 px-3 py-1 disabled:opacity-50" onClick={startRecord} disabled={isUploading || isRecording}>
|
|
||||||
{isRecording ? "Recording..." : "Record Voice"}
|
{isRecording ? "Recording..." : "Record Voice"}
|
||||||
</button>
|
</button>
|
||||||
<button className="rounded bg-slate-700 px-3 py-1 disabled:opacity-50" onClick={stopRecord} disabled={!isRecording}>
|
<button className="rounded-lg bg-slate-700/90 px-3 py-1.5 disabled:opacity-50" onClick={stopRecord} disabled={!isRecording}>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,56 +17,60 @@ export function MessageList() {
|
|||||||
}, [activeChatId, messagesByChat]);
|
}, [activeChatId, messagesByChat]);
|
||||||
|
|
||||||
if (!activeChatId) {
|
if (!activeChatId) {
|
||||||
return <div className="flex h-full items-center justify-center text-slate-400">Select a chat</div>;
|
return <div className="flex h-full items-center justify-center text-slate-300/80">Select a chat</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex-1 overflow-auto p-4">
|
<div className="tg-scrollbar flex-1 overflow-auto px-3 py-4 md:px-6">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => {
|
||||||
<div className={`mb-3 flex ${message.sender_id === me?.id ? "justify-end" : "justify-start"}`} key={message.id}>
|
const own = message.sender_id === me?.id;
|
||||||
<div className="max-w-[80%] rounded-lg bg-slate-800 px-3 py-2">
|
return (
|
||||||
{message.type === "voice" && message.text ? (
|
<div className={`mb-2 flex ${own ? "justify-end" : "justify-start"}`} key={`${message.id}-${message.client_message_id ?? ""}`}>
|
||||||
renderContent(message.type, message.text)
|
<div
|
||||||
) : (
|
className={`max-w-[86%] rounded-2xl px-3 py-2 shadow-sm md:max-w-[72%] ${
|
||||||
renderContent(message.type, message.text)
|
own
|
||||||
)}
|
? "rounded-br-md bg-sky-500/90 text-slate-950"
|
||||||
<p className="mt-1 flex items-center justify-end gap-1 text-right text-[11px] text-slate-400">
|
: "rounded-bl-md border border-slate-700/60 bg-slate-900/80 text-slate-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{renderContent(message.type, message.text)}
|
||||||
|
<p className={`mt-1 flex items-center justify-end gap-1 text-[11px] ${own ? "text-slate-900/85" : "text-slate-400"}`}>
|
||||||
<span>{formatTime(message.created_at)}</span>
|
<span>{formatTime(message.created_at)}</span>
|
||||||
{message.sender_id === me?.id ? <span className={message.delivery_status === "read" ? "text-sky-400" : ""}>{renderStatus(message.delivery_status)}</span> : null}
|
{own ? <span className={message.delivery_status === "read" ? "text-cyan-900" : ""}>{renderStatus(message.delivery_status)}</span> : null}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
</div>
|
})}
|
||||||
<div className="px-4 pb-2 text-xs text-slate-400">
|
|
||||||
{(typingByChat[activeChatId] ?? []).length > 0 ? "Someone is typing..." : ""}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-5 pb-2 text-xs text-slate-300/80">{(typingByChat[activeChatId] ?? []).length > 0 ? "typing..." : ""}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function renderContent(messageType: string, text: string | null) {
|
|
||||||
|
function renderContent(messageType: string, text: string | null) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return <p className="text-slate-300">[empty]</p>;
|
return <p className="opacity-80">[empty]</p>;
|
||||||
}
|
}
|
||||||
if (messageType === "image") {
|
if (messageType === "image") {
|
||||||
return <img alt="attachment" className="max-h-64 rounded" src={text} />;
|
return <img alt="attachment" className="max-h-72 rounded-lg object-cover" src={text} />;
|
||||||
}
|
}
|
||||||
if (messageType === "video" || messageType === "circle_video") {
|
if (messageType === "video" || messageType === "circle_video") {
|
||||||
return <video className="max-h-64 rounded" controls src={text} />;
|
return <video className="max-h-72 rounded-lg" controls src={text} />;
|
||||||
}
|
}
|
||||||
if (messageType === "audio" || messageType === "voice") {
|
if (messageType === "audio" || messageType === "voice") {
|
||||||
return <audio controls src={text} />;
|
return <audio controls src={text} />;
|
||||||
}
|
}
|
||||||
if (messageType === "file") {
|
if (messageType === "file") {
|
||||||
return (
|
return (
|
||||||
<a className="text-accent underline" href={text} rel="noreferrer" target="_blank">
|
<a className="underline" href={text} rel="noreferrer" target="_blank">
|
||||||
Open file
|
Open file
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <p className="whitespace-pre-wrap break-words">{text}</p>;
|
return <p className="whitespace-pre-wrap break-words">{text}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStatus(status: string | undefined): string {
|
function renderStatus(status: string | undefined): string {
|
||||||
if (status === "sending") {
|
if (status === "sending") {
|
||||||
|
|||||||
@@ -75,12 +75,18 @@ export function NewChatPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-slate-700 p-3">
|
<div className="border-b border-slate-700/50 bg-slate-900/45 p-3">
|
||||||
<div className="mb-2 flex gap-2 text-xs">
|
<div className="mb-2 flex gap-2 text-xs">
|
||||||
<button className={`rounded px-2 py-1 ${mode === "group" ? "bg-accent text-black" : "bg-slate-700"}`} onClick={() => setMode("group")}>
|
<button
|
||||||
|
className={`rounded-lg px-2.5 py-1.5 ${mode === "group" ? "bg-sky-500 text-slate-950" : "bg-slate-700/70 hover:bg-slate-700"}`}
|
||||||
|
onClick={() => setMode("group")}
|
||||||
|
>
|
||||||
Group
|
Group
|
||||||
</button>
|
</button>
|
||||||
<button className={`rounded px-2 py-1 ${mode === "channel" ? "bg-accent text-black" : "bg-slate-700"}`} onClick={() => setMode("channel")}>
|
<button
|
||||||
|
className={`rounded-lg px-2.5 py-1.5 ${mode === "channel" ? "bg-sky-500 text-slate-950" : "bg-slate-700/70 hover:bg-slate-700"}`}
|
||||||
|
onClick={() => setMode("channel")}
|
||||||
|
>
|
||||||
Channel
|
Channel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,15 +94,15 @@ export function NewChatPanel() {
|
|||||||
<div className="mb-3 space-y-2">
|
<div className="mb-3 space-y-2">
|
||||||
<p className="text-xs text-slate-400">Новый диалог</p>
|
<p className="text-xs text-slate-400">Новый диалог</p>
|
||||||
<input
|
<input
|
||||||
className="w-full rounded bg-slate-800 px-2 py-1 text-sm"
|
className="w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
|
||||||
placeholder="@username"
|
placeholder="@username"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => void handleSearch(e.target.value)}
|
onChange={(e) => void handleSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="max-h-32 overflow-auto">
|
<div className="tg-scrollbar max-h-32 overflow-auto">
|
||||||
{results.map((user) => (
|
{results.map((user) => (
|
||||||
<button
|
<button
|
||||||
className="mb-1 block w-full rounded bg-slate-800 px-2 py-1 text-left text-sm hover:bg-slate-700"
|
className="mb-1 block w-full rounded-lg bg-slate-800/90 px-2 py-1.5 text-left text-sm hover:bg-slate-700/90"
|
||||||
key={user.id}
|
key={user.id}
|
||||||
onClick={() => void createPrivate(user.id)}
|
onClick={() => void createPrivate(user.id)}
|
||||||
>
|
>
|
||||||
@@ -108,12 +114,12 @@ export function NewChatPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<form className="space-y-2" onSubmit={(e) => void createByType(e)}>
|
<form className="space-y-2" onSubmit={(e) => void createByType(e)}>
|
||||||
<input
|
<input
|
||||||
className="w-full rounded bg-slate-800 px-2 py-1 text-sm"
|
className="w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
|
||||||
placeholder={mode === "group" ? "Group title" : "Channel title"}
|
placeholder={mode === "group" ? "Group title" : "Channel title"}
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button className="w-full rounded bg-slate-700 px-2 py-1 text-sm hover:bg-slate-600" disabled={loading} type="submit">
|
<button className="w-full rounded-lg bg-sky-500 px-2 py-2 text-sm font-semibold text-slate-950 hover:bg-sky-400 disabled:opacity-60" disabled={loading} type="submit">
|
||||||
Create {mode}
|
Create {mode}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap");
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -10,5 +12,36 @@ body,
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
font-family: "Manrope", "Segoe UI", sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 22% 18%, rgba(36, 68, 117, 0.35), transparent 26%),
|
||||||
|
radial-gradient(circle at 82% 12%, rgba(24, 95, 102, 0.25), transparent 30%),
|
||||||
|
linear-gradient(180deg, #101e30 0%, #162233 55%, #19283a 100%);
|
||||||
|
color: #e5edf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-panel {
|
||||||
|
background: rgba(19, 31, 47, 0.9);
|
||||||
|
border: 1px solid rgba(146, 174, 208, 0.14);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-chat-wallpaper {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 14% 18%, rgba(59, 130, 246, 0.09), transparent 30%),
|
||||||
|
radial-gradient(circle at 86% 74%, rgba(34, 197, 94, 0.07), transparent 33%),
|
||||||
|
linear-gradient(160deg, rgba(255, 255, 255, 0.02) 0%, rgba(255, 255, 255, 0.015) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(126, 159, 201, 0.35);
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export function ChatsPage() {
|
|||||||
const logout = useAuthStore((s) => s.logout);
|
const logout = useAuthStore((s) => s.logout);
|
||||||
const loadChats = useChatStore((s) => s.loadChats);
|
const loadChats = useChatStore((s) => s.loadChats);
|
||||||
const activeChatId = useChatStore((s) => s.activeChatId);
|
const activeChatId = useChatStore((s) => s.activeChatId);
|
||||||
|
const chats = useChatStore((s) => s.chats);
|
||||||
|
const setActiveChatId = useChatStore((s) => s.setActiveChatId);
|
||||||
const loadMessages = useChatStore((s) => s.loadMessages);
|
const loadMessages = useChatStore((s) => s.loadMessages);
|
||||||
|
const activeChat = chats.find((chat) => chat.id === activeChatId);
|
||||||
|
|
||||||
useRealtime();
|
useRealtime();
|
||||||
|
|
||||||
@@ -26,12 +29,24 @@ export function ChatsPage() {
|
|||||||
}, [activeChatId, loadMessages]);
|
}, [activeChatId, loadMessages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex h-screen w-full bg-bg text-text">
|
<main className="h-screen w-full p-2 text-text md:p-4">
|
||||||
|
<div className="mx-auto flex h-full max-w-[1500px] gap-2 md:gap-4">
|
||||||
|
<section className={`tg-panel overflow-hidden rounded-2xl ${activeChatId ? "hidden md:block md:w-[360px]" : "w-full md:w-[360px]"}`}>
|
||||||
<ChatList />
|
<ChatList />
|
||||||
<section className="flex flex-1 flex-col">
|
</section>
|
||||||
<div className="flex items-center justify-between border-b border-slate-700 bg-panel px-4 py-3">
|
|
||||||
<p className="text-sm">Signed in as {me?.username}</p>
|
<section className={`tg-panel tg-chat-wallpaper min-w-0 flex-1 overflow-hidden rounded-2xl ${activeChatId ? "flex" : "hidden md:flex"} flex-col`}>
|
||||||
<button className="rounded bg-slate-700 px-3 py-1 text-sm" onClick={logout}>
|
<div className="flex items-center justify-between border-b border-slate-700/50 bg-slate-900/55 px-4 py-3">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<button className="rounded-full bg-slate-700/70 px-2 py-1 text-xs md:hidden" onClick={() => setActiveChatId(null)}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-sm font-semibold">{activeChat?.title || `@${me?.username}`}</p>
|
||||||
|
<p className="truncate text-xs text-slate-300/80">{activeChat ? activeChat.type : "Select a chat"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="rounded-full bg-slate-700/70 px-3 py-1.5 text-xs font-semibold hover:bg-slate-600/80" onClick={logout}>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,11 +56,12 @@ export function ChatsPage() {
|
|||||||
{activeChatId ? (
|
{activeChatId ? (
|
||||||
<MessageComposer />
|
<MessageComposer />
|
||||||
) : (
|
) : (
|
||||||
<div className="border-t border-slate-700 bg-panel p-4 text-center text-sm text-slate-400">
|
<div className="border-t border-slate-700/50 bg-slate-900/40 p-4 text-center text-sm text-slate-300/80">
|
||||||
Выберите чат, чтобы начать переписку
|
Выберите чат, чтобы начать переписку
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user