Improve design

This commit is contained in:
2025-09-18 20:11:53 +03:00
Unverified
parent e77ba2421c
commit c5f976b4d6
8 changed files with 169 additions and 44 deletions
@@ -1,14 +1,18 @@
import { useState } from "react";
import { RichTextArea } from "../core/RichTextArea";
import type { Message } from "../../../core/types";
import Quote from "../core/Quote";
import AnimatedHeight from "../core/animations/AnimatedHeight";
interface ChatInputWrapperProps {
onSendMessage: (message: string) => void | ((message: string) => void);
onSendMessage: (message: string) => void;
replyTo?: Message | null;
replyToVisible: boolean;
onClearReply?: () => void;
onCloseReply?: () => void;
}
export function ChatInputWrapper({ onSendMessage, replyTo, onClearReply }: ChatInputWrapperProps) {
export function ChatInputWrapper({ onSendMessage, replyTo, replyToVisible, onClearReply, onCloseReply }: ChatInputWrapperProps) {
const [message, setMessage] = useState("");
const handleSubmit = async (e: React.FormEvent | Event) => {
@@ -23,15 +27,17 @@ export function ChatInputWrapper({ onSendMessage, replyTo, onClearReply }: ChatI
return (
<div className="chat-input-wrapper">
<form className="input-group" id="message-form" onSubmit={handleSubmit}>
{replyTo && (
<div className="reply-preview">
<div className="reply-content">
<span className="reply-username">{replyTo.username}</span>
<span className="reply-text">{replyTo.content}</span>
<AnimatedHeight visible={replyToVisible} onFinish={onCloseReply}>
{replyTo && (
<div className="reply-preview">
<Quote className="reply-content" background="surfaceContainer">
<span className="reply-username">{replyTo!.username}</span>
<span className="reply-text">{replyTo!.content}</span>
</Quote>
<mdui-button-icon icon="close" className="reply-cancel" onClick={onClearReply}></mdui-button-icon>
</div>
<mdui-button-icon icon="close" className="reply-cancel" onClick={onClearReply}></mdui-button-icon>
</div>
)}
)}
</AnimatedHeight>
<div className="chat-input">
<RichTextArea
className="message-input"
+5 -6
View File
@@ -1,6 +1,7 @@
import { formatTime } from "../../../utils/utils";
import type { Message as MessageType } from "../../../core/types";
import defaultAvatar from "../../../resources/images/default-avatar.png";
import Quote from "../core/Quote";
interface MessageProps {
message: MessageType;
@@ -53,12 +54,10 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLo
{/* Add reply preview if this is a reply */}
{message.reply_to && (
<div className="message-reply">
<div className="reply-content">
<span className="reply-username">{message.reply_to.username}</span>
<span className="reply-text">{message.reply_to.content}</span>
</div>
</div>
<Quote className="message-reply">
<span className="reply-username">{message.reply_to.username}</span>
<span className="reply-text">{message.reply_to.content}</span>
</Quote>
)}
<div className="message-content">
@@ -17,6 +17,13 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
const [switchOut, setSwitchOut] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const [replyTo, setReplyTo] = useState<Message | null>(null);
const [replyToVisible, setReplyToVisible] = useState(Boolean(replyTo));
useEffect(() => {
if (replyTo) {
setReplyToVisible(true);
}
}, [replyTo]);
// Handle panel state changes
useEffect(() => {
@@ -144,8 +151,10 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
panel.handleSendMessage(text, replyTo?.id);
setReplyTo(null);
}}
replyTo={replyTo}
onClearReply={() => setReplyTo(null)}
replyTo={replyTo}
replyToVisible={replyToVisible}
onClearReply={() => setReplyToVisible(false)}
onCloseReply={() => setReplyTo(null)}
/>
</div>
</div>
+17
View File
@@ -0,0 +1,17 @@
import type { ReactNode } from "react";
export interface QuoteProps {
className?: string;
children?: ReactNode;
background?: "surfaceContainer" | "primaryContainer"
}
export default function Quote({ className, children, background = "primaryContainer" }: QuoteProps) {
return (
<div className={`quote bg-${background} ${className}`}>
<div className="quote-inner">
{children}
</div>
</div>
)
}
@@ -0,0 +1,74 @@
import { useEffect, useState, useRef, type ReactNode } from "react";
export interface AnimatedHeightProps {
visible: any;
duration?: number;
onFinish?: () => void
children?: ReactNode;
}
export default function AnimatedHeight({ visible, duration = 0.25, onFinish, children }: AnimatedHeightProps) {
const [height, setHeight] = useState("0px");
const [shouldRender, setShouldRender] = useState(visible);
const [isAnimating, setIsAnimating] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const measureRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (visible) {
setShouldRender(true);
setIsAnimating(true);
// Wait for content to render, then measure
setTimeout(() => {
if (measureRef.current) {
const contentHeight = measureRef.current.scrollHeight;
setHeight(`${contentHeight}px`);
}
// Animation complete
setTimeout(() => {
setIsAnimating(false);
}, duration * 1000);
}, 0);
} else {
if (shouldRender) {
setIsAnimating(true);
if (measureRef.current) {
const contentHeight = measureRef.current.scrollHeight;
setHeight(`${contentHeight}px`);
// Force a reflow before animating to 0
requestAnimationFrame(() => {
setHeight("0px");
});
}
// Hide content after animation completes
setTimeout(() => {
setShouldRender(false);
setIsAnimating(false);
if (onFinish) {
onFinish();
}
}, duration * 1000);
}
}
}, [visible, duration, shouldRender]);
// Don't render if not visible and not animating
if (!visible && !shouldRender && !isAnimating) {
return null;
}
return (
<div
ref={contentRef}
style={{
height,
transition: `height ${duration}s ease`,
overflow: "hidden"
}}
>
<div ref={measureRef} style={{ height: "auto" }}>
{shouldRender && children}
</div>
</div>
);
}