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:
@@ -192,15 +192,11 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.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;
|
||||
.quote.message-reply {
|
||||
user-select: none;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.reply-content {
|
||||
.quote-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
@@ -289,10 +285,11 @@
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
background-color: $color-dark-surface-container;
|
||||
border-radius: 30px;
|
||||
flex-direction: column;
|
||||
|
||||
.chat-input {
|
||||
background-color: $color-dark-surface-container;
|
||||
border-radius: 30px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -313,24 +310,24 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
margin: 10px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: $color-dark-primary;
|
||||
color: $color-dark-on-primary;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.25s ease;
|
||||
align-self: flex-end;
|
||||
.send-btn {
|
||||
margin: 10px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: $color-dark-primary;
|
||||
color: $color-dark-on-primary;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.25s ease;
|
||||
align-self: flex-end;
|
||||
|
||||
@include hoverStateLayer($background: $color-dark-primary);
|
||||
@include hoverStateLayer($background: $color-dark-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use "material" as *;
|
||||
@use "sass:color";
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
@@ -124,3 +125,22 @@ button, input {
|
||||
background-color: transparent;
|
||||
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-high: rgb(38 43 46);
|
||||
$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
|
||||
$color-1: rgb(82, 109, 246);
|
||||
$color-2: rgb(65, 11, 113);
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -145,7 +152,9 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
||||
setReplyTo(null);
|
||||
}}
|
||||
replyTo={replyTo}
|
||||
onClearReply={() => setReplyTo(null)}
|
||||
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