feat: improve chat realtime and media composer UX
All checks were successful
CI / test (push) Successful in 27s

- add media preview and upload confirmation for image/video

- add upload progress tracking for presigned uploads

- keep voice recording/upload flow with better UI states

- include related realtime/chat updates currently in working tree
This commit is contained in:
2026-03-07 22:46:04 +03:00
parent 9ef9366aca
commit f95a0e9727
10 changed files with 279 additions and 83 deletions

View File

@@ -4,10 +4,10 @@ import { searchUsers } from "../api/users";
import type { ChatType, UserSearchItem } from "../chat/types";
import { useChatStore } from "../store/chatStore";
type CreateMode = "private" | "group" | "channel";
type CreateMode = "group" | "channel";
export function NewChatPanel() {
const [mode, setMode] = useState<CreateMode>("private");
const [mode, setMode] = useState<CreateMode>("group");
const [query, setQuery] = useState("");
const [title, setTitle] = useState("");
const [results, setResults] = useState<UserSearchItem[]>([]);
@@ -57,9 +57,6 @@ export function NewChatPanel() {
async function createByType(event: FormEvent) {
event.preventDefault();
if (mode === "private") {
return;
}
if (!title.trim()) {
setError("Title is required");
return;
@@ -80,9 +77,6 @@ export function NewChatPanel() {
return (
<div className="border-b border-slate-700 p-3">
<div className="mb-2 flex gap-2 text-xs">
<button className={`rounded px-2 py-1 ${mode === "private" ? "bg-accent text-black" : "bg-slate-700"}`} onClick={() => setMode("private")}>
Private
</button>
<button className={`rounded px-2 py-1 ${mode === "group" ? "bg-accent text-black" : "bg-slate-700"}`} onClick={() => setMode("group")}>
Group
</button>
@@ -91,40 +85,38 @@ export function NewChatPanel() {
</button>
</div>
{mode === "private" ? (
<div className="space-y-2">
<input
className="w-full rounded bg-slate-800 px-2 py-1 text-sm"
placeholder="@username"
value={query}
onChange={(e) => void handleSearch(e.target.value)}
/>
<div className="max-h-32 overflow-auto">
{results.map((user) => (
<button
className="mb-1 block w-full rounded bg-slate-800 px-2 py-1 text-left text-sm hover:bg-slate-700"
key={user.id}
onClick={() => void createPrivate(user.id)}
>
@{user.username}
</button>
))}
{normalizedQuery && results.length === 0 ? <p className="text-xs text-slate-400">No users</p> : null}
</div>
<div className="mb-3 space-y-2">
<p className="text-xs text-slate-400">Новый диалог</p>
<input
className="w-full rounded bg-slate-800 px-2 py-1 text-sm"
placeholder="@username"
value={query}
onChange={(e) => void handleSearch(e.target.value)}
/>
<div className="max-h-32 overflow-auto">
{results.map((user) => (
<button
className="mb-1 block w-full rounded bg-slate-800 px-2 py-1 text-left text-sm hover:bg-slate-700"
key={user.id}
onClick={() => void createPrivate(user.id)}
>
@{user.username}
</button>
))}
{normalizedQuery && results.length === 0 ? <p className="text-xs text-slate-400">No users</p> : null}
</div>
) : (
<form className="space-y-2" onSubmit={(e) => void createByType(e)}>
<input
className="w-full rounded bg-slate-800 px-2 py-1 text-sm"
placeholder={mode === "group" ? "Group title" : "Channel title"}
value={title}
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">
Create {mode}
</button>
</form>
)}
</div>
<form className="space-y-2" onSubmit={(e) => void createByType(e)}>
<input
className="w-full rounded bg-slate-800 px-2 py-1 text-sm"
placeholder={mode === "group" ? "Group title" : "Channel title"}
value={title}
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">
Create {mode}
</button>
</form>
{error ? <p className="mt-2 text-xs text-red-400">{error}</p> : null}
</div>
);