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
+7 -10
View File
@@ -192,15 +192,11 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.message-reply { .quote.message-reply {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 0.5rem;
margin-bottom: 0.5rem;
border-left: 3px solid $color-dark-primary;
user-select: none; user-select: none;
margin-bottom: 10px;
.reply-content { .quote-inner {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.2rem; gap: 0.2rem;
@@ -289,10 +285,11 @@
.input-group { .input-group {
display: flex; display: flex;
.chat-input {
background-color: $color-dark-surface-container; background-color: $color-dark-surface-container;
border-radius: 30px; border-radius: 30px;
flex-direction: column;
.chat-input {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -313,7 +310,6 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
}
.send-btn { .send-btn {
margin: 10px; margin: 10px;
@@ -336,6 +332,7 @@
} }
} }
} }
}
} }
.message-profile-pic { .message-profile-pic {
@@ -1,4 +1,5 @@
@use "material" as *; @use "material" as *;
@use "sass:color";
.text-center { .text-center {
text-align: center; text-align: center;
@@ -124,3 +125,22 @@ button, input {
background-color: transparent; background-color: transparent;
display: block; display: block;
} }
.quote {
background-color: $color-dark-surface-primary-container-lightened;
border-radius: 8px;
overflow: hidden;
&.bg-surfaceContainer {
background-color: $color-dark-secondary-container;
.quote-inner {
border-left: 3px solid $color-dark-secondary;
}
}
.quote-inner {
border-left: 3px solid $color-dark-primary;
padding: 0.5rem;
}
}
@@ -50,6 +50,9 @@ $color-dark-surface-container-low: rgb(24 28 31);
$color-dark-surface-container: rgb(28 32 36); $color-dark-surface-container: rgb(28 32 36);
$color-dark-surface-container-high: rgb(38 43 46); $color-dark-surface-container-high: rgb(38 43 46);
$color-dark-surface-container-highest: rgb(49 53 57); $color-dark-surface-container-highest: rgb(49 53 57);
$color-dark-surface-primary-container-lightened: color.adjust($color-dark-primary-container, $lightness: 5%);
$color-dark-surface-container-lightened: color.adjust($color-dark-surface-container, $lightness: 5%);
// custom colors // custom colors
$color-1: rgb(82, 109, 246); $color-1: rgb(82, 109, 246);
$color-2: rgb(65, 11, 113); $color-2: rgb(65, 11, 113);
@@ -1,14 +1,18 @@
import { useState } from "react"; import { useState } from "react";
import { RichTextArea } from "../core/RichTextArea"; import { RichTextArea } from "../core/RichTextArea";
import type { Message } from "../../../core/types"; import type { Message } from "../../../core/types";
import Quote from "../core/Quote";
import AnimatedHeight from "../core/animations/AnimatedHeight";
interface ChatInputWrapperProps { interface ChatInputWrapperProps {
onSendMessage: (message: string) => void | ((message: string) => void); onSendMessage: (message: string) => void;
replyTo?: Message | null; replyTo?: Message | null;
replyToVisible: boolean;
onClearReply?: () => void; 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 [message, setMessage] = useState("");
const handleSubmit = async (e: React.FormEvent | Event) => { const handleSubmit = async (e: React.FormEvent | Event) => {
@@ -23,15 +27,17 @@ export function ChatInputWrapper({ onSendMessage, replyTo, onClearReply }: ChatI
return ( return (
<div className="chat-input-wrapper"> <div className="chat-input-wrapper">
<form className="input-group" id="message-form" onSubmit={handleSubmit}> <form className="input-group" id="message-form" onSubmit={handleSubmit}>
<AnimatedHeight visible={replyToVisible} onFinish={onCloseReply}>
{replyTo && ( {replyTo && (
<div className="reply-preview"> <div className="reply-preview">
<div className="reply-content"> <Quote className="reply-content" background="surfaceContainer">
<span className="reply-username">{replyTo.username}</span> <span className="reply-username">{replyTo!.username}</span>
<span className="reply-text">{replyTo.content}</span> <span className="reply-text">{replyTo!.content}</span>
</div> </Quote>
<mdui-button-icon icon="close" className="reply-cancel" onClick={onClearReply}></mdui-button-icon> <mdui-button-icon icon="close" className="reply-cancel" onClick={onClearReply}></mdui-button-icon>
</div> </div>
)} )}
</AnimatedHeight>
<div className="chat-input"> <div className="chat-input">
<RichTextArea <RichTextArea
className="message-input" className="message-input"
+3 -4
View File
@@ -1,6 +1,7 @@
import { formatTime } from "../../../utils/utils"; import { formatTime } from "../../../utils/utils";
import type { Message as MessageType } from "../../../core/types"; import type { Message as MessageType } from "../../../core/types";
import defaultAvatar from "../../../resources/images/default-avatar.png"; import defaultAvatar from "../../../resources/images/default-avatar.png";
import Quote from "../core/Quote";
interface MessageProps { interface MessageProps {
message: MessageType; message: MessageType;
@@ -53,12 +54,10 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLo
{/* Add reply preview if this is a reply */} {/* Add reply preview if this is a reply */}
{message.reply_to && ( {message.reply_to && (
<div className="message-reply"> <Quote className="message-reply">
<div className="reply-content">
<span className="reply-username">{message.reply_to.username}</span> <span className="reply-username">{message.reply_to.username}</span>
<span className="reply-text">{message.reply_to.content}</span> <span className="reply-text">{message.reply_to.content}</span>
</div> </Quote>
</div>
)} )}
<div className="message-content"> <div className="message-content">
@@ -17,6 +17,13 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
const [switchOut, setSwitchOut] = useState(false); const [switchOut, setSwitchOut] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const [replyTo, setReplyTo] = useState<Message | null>(null); const [replyTo, setReplyTo] = useState<Message | null>(null);
const [replyToVisible, setReplyToVisible] = useState(Boolean(replyTo));
useEffect(() => {
if (replyTo) {
setReplyToVisible(true);
}
}, [replyTo]);
// Handle panel state changes // Handle panel state changes
useEffect(() => { useEffect(() => {
@@ -145,7 +152,9 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
setReplyTo(null); setReplyTo(null);
}} }}
replyTo={replyTo} replyTo={replyTo}
onClearReply={() => setReplyTo(null)} replyToVisible={replyToVisible}
onClearReply={() => setReplyToVisible(false)}
onCloseReply={() => setReplyTo(null)}
/> />
</div> </div>
</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>
);
}