feat(web): sprint1 ui core with global toasts and improved chat layout
Some checks failed
CI / test (push) Failing after 19s
Some checks failed
CI / test (push) Failing after 19s
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user