feat(web): sprint1 ui core with global toasts and improved chat layout
Some checks failed
CI / test (push) Failing after 19s

This commit is contained in:
2026-03-08 10:35:21 +03:00
parent 1119cc65b8
commit a77516cfea
6 changed files with 81 additions and 32 deletions

View File

@@ -10,6 +10,7 @@ import {
import type { Message, MessageReaction } from "../chat/types";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
import { useUiStore } from "../store/uiStore";
import { formatTime } from "../utils/format";
import { formatMessageHtml } from "../utils/formatMessage";
@@ -45,6 +46,7 @@ export function MessageList() {
const updateChatPinnedMessage = useChatStore((s) => s.updateChatPinnedMessage);
const removeMessage = useChatStore((s) => s.removeMessage);
const restoreMessages = useChatStore((s) => s.restoreMessages);
const showToast = useUiStore((s) => s.showToast);
const [ctx, setCtx] = useState<ContextMenuState>(null);
const [forwardMessageId, setForwardMessageId] = useState<number | null>(null);
@@ -58,7 +60,6 @@ export function MessageList() {
const [pendingDelete, setPendingDelete] = useState<PendingDeleteState>(null);
const [undoTick, setUndoTick] = useState(0);
const [reactionsByMessage, setReactionsByMessage] = useState<Record<number, MessageReaction[]>>({});
const [toast, setToast] = useState<string | null>(null);
const messages = useMemo(() => {
if (!activeChatId) {
@@ -113,14 +114,6 @@ export function MessageList() {
setReactionsByMessage({});
}, [activeChatId]);
useEffect(() => {
if (!toast) {
return;
}
const timer = window.setTimeout(() => setToast(null), 2200);
return () => window.clearTimeout(timer);
}, [toast]);
useEffect(() => {
if (!pendingDelete) {
return;
@@ -339,6 +332,12 @@ export function MessageList() {
{messages.map((message, messageIndex) => {
const own = message.sender_id === me?.id;
const prev = messageIndex > 0 ? messages[messageIndex - 1] : null;
const groupedWithPrev = Boolean(
prev &&
prev.sender_id === message.sender_id &&
Math.abs(new Date(message.created_at).getTime() - new Date(prev.created_at).getTime()) < 4 * 60 * 1000
);
const replySource = message.reply_to_message_id ? messagesMap.get(message.reply_to_message_id) : null;
const isSelected = selectedIds.has(message.id);
const messageReactions = reactionsByMessage[message.id] ?? [];
@@ -355,13 +354,13 @@ export function MessageList() {
</div>
) : null}
<div className={`mb-2 flex ${own ? "justify-end" : "justify-start"}`}>
<div className={`${groupedWithPrev ? "mb-1" : "mb-2"} flex ${own ? "justify-end" : "justify-start"}`}>
<div
id={`message-${message.id}`}
className={`max-w-[90%] rounded-2xl px-3 py-2.5 shadow-sm md:max-w-[70%] ${
className={`max-w-[90%] px-3 py-2.5 shadow-sm md:max-w-[70%] ${
own
? "rounded-br-md bg-gradient-to-b from-sky-500/95 to-sky-600/90 text-slate-950"
: "rounded-bl-md border border-slate-700/60 bg-slate-900/80 text-slate-100"
? `${groupedWithPrev ? "rounded-2xl rounded-tr-md" : "rounded-2xl rounded-br-md"} bg-gradient-to-b from-sky-500/95 to-sky-600/90 text-slate-950`
: `${groupedWithPrev ? "rounded-2xl rounded-tl-md" : "rounded-2xl rounded-bl-md"} border border-slate-700/60 bg-slate-900/80 text-slate-100`
} ${isSelected ? "ring-2 ring-sky-400/80" : ""} ${focusedMessageId === message.id ? "ring-2 ring-amber-300" : ""}`}
onClick={() => {
if (selectedIds.size > 0) {
@@ -534,9 +533,9 @@ export function MessageList() {
}
try {
await downloadFileFromUrl(url);
setToast("File downloaded");
showToast("File downloaded");
} catch {
setToast("Download failed");
showToast("Download failed");
} finally {
setCtx(null);
}
@@ -554,9 +553,9 @@ export function MessageList() {
}
try {
await navigator.clipboard.writeText(url);
setToast("Link copied");
showToast("Link copied");
} catch {
setToast("Copy failed");
showToast("Copy failed");
} finally {
setCtx(null);
}
@@ -657,11 +656,6 @@ export function MessageList() {
</div>
</div>
) : null}
{toast ? (
<div className="pointer-events-none absolute bottom-3 left-0 right-0 z-[120] flex justify-center px-3">
<div className="rounded-lg border border-slate-700/80 bg-slate-900/95 px-3 py-2 text-xs text-slate-100 shadow-xl">{toast}</div>
</div>
) : null}
</div>
);
}