mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Improve design
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user