feat: enhance Messages and ThreadChat components with improved textarea handling and styling
This commit is contained in:
parent
49d6b242d4
commit
3ad1cb3c58
|
@ -1,30 +1,71 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
|
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
|
||||||
import { KebabIcon } from "../../icons/ExportCommonIcons";
|
import { KebabIcon } from "../../icons/ExportCommonIcons";
|
||||||
|
import { adjustHeight } from "./function/textAreaHeightAdjust";
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
val: Reply;
|
val: Reply | CommentSchema;
|
||||||
i: number;
|
i: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Messages: React.FC<MessageProps> = ({ val, i }) => {
|
const Messages: React.FC<MessageProps> = ({ val, i }) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [openOptions, setOpenOptions] = useState(false);
|
const [openOptions, setOpenOptions] = useState(false);
|
||||||
|
|
||||||
|
// input
|
||||||
|
const [value, setValue] = useState<string>(
|
||||||
|
"reply" in val ? val.reply : val.comment
|
||||||
|
);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const currentUser = "1";
|
const currentUser = "1";
|
||||||
|
|
||||||
const UserName = "username";
|
const UserName = "username";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) adjustHeight(textareaRef.current);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
function handleCancelAction() {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSaveAction() {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="edit-container">
|
<div className="edit-container">
|
||||||
<div className="input-container">
|
<div className="input-container">
|
||||||
<textarea />
|
<textarea
|
||||||
|
placeholder="type here"
|
||||||
|
ref={textareaRef}
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
style={{ resize: "none" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="actions-container">
|
<div className="actions-container">
|
||||||
|
<div className="options"></div>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<button className="cancel-button">Cancel</button>
|
<button
|
||||||
<button className="save-button">Save</button>
|
className="cancel-button"
|
||||||
|
onClick={() => {
|
||||||
|
handleCancelAction();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="save-button"
|
||||||
|
onClick={() => {
|
||||||
|
handleSaveAction();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,7 +107,9 @@ const Messages: React.FC<MessageProps> = ({ val, i }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="message">{val.reply}</div>
|
<div className="message">
|
||||||
|
{"reply" in val ? val.reply : val.comment}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,37 +1,49 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { CloseIcon, KebabIcon } from "../../icons/ExportCommonIcons";
|
import { CloseIcon, KebabIcon } from "../../icons/ExportCommonIcons";
|
||||||
import Messages from "./Messages";
|
import Messages from "./Messages";
|
||||||
import { ExpandIcon } from "../../icons/SimulationIcons";
|
import { ExpandIcon } from "../../icons/SimulationIcons";
|
||||||
|
import { adjustHeight } from "./function/textAreaHeightAdjust";
|
||||||
|
|
||||||
const ThreadChat: React.FC = () => {
|
const ThreadChat: React.FC = () => {
|
||||||
const [openThreadOptions, setOpenThreadOptions] = useState(false);
|
const [openThreadOptions, setOpenThreadOptions] = useState(false);
|
||||||
|
|
||||||
|
const [inputActive, setInputActive] = useState(false);
|
||||||
|
|
||||||
|
const [value, setValue] = useState<string>("");
|
||||||
|
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{
|
{
|
||||||
replyId: "user 1",
|
replyId: "user 1",
|
||||||
creatorId: "1",
|
creatorId: "1",
|
||||||
createdAt: "hello, thread check",
|
createdAt: "hello, thread check",
|
||||||
lastUpdatedAt: "2 hrs ago",
|
lastUpdatedAt: "2 hrs ago",
|
||||||
reply: "true",
|
reply: "reply 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
replyId: "user 2",
|
replyId: "user 2",
|
||||||
creatorId: "2",
|
creatorId: "2",
|
||||||
createdAt: "hello, thread check",
|
createdAt: "hello, thread check",
|
||||||
lastUpdatedAt: "2 hrs ago",
|
lastUpdatedAt: "2 hrs ago",
|
||||||
reply: "true",
|
reply: "reply 2",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) adjustHeight(textareaRef.current);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="thread-char-wrapper">
|
<div className="thread-chat-wrapper">
|
||||||
<div className="thread-char-container">
|
<div className="thread-chat-container">
|
||||||
<div className="header-wrapper">
|
<div className="header-wrapper">
|
||||||
<div className="header">Comment</div>
|
<div className="header">Comment</div>
|
||||||
<div className="header-options">
|
<div className="header-options">
|
||||||
<button
|
<button
|
||||||
className="options-button"
|
className="options-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenThreadOptions(true);
|
setOpenThreadOptions(!openThreadOptions);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KebabIcon />
|
<KebabIcon />
|
||||||
|
@ -40,7 +52,7 @@ const ThreadChat: React.FC = () => {
|
||||||
<div className="options-list">
|
<div className="options-list">
|
||||||
<div className="options">Mark as Unread</div>
|
<div className="options">Mark as Unread</div>
|
||||||
<div className="options">Mark as Resolved</div>
|
<div className="options">Mark as Resolved</div>
|
||||||
<div className="options">Delete Thread</div>
|
<div className="options delete">Delete Thread</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button className="close-button">
|
<button className="close-button">
|
||||||
|
@ -54,9 +66,21 @@ const ThreadChat: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="send-message-wrapper">
|
<div className="send-message-wrapper">
|
||||||
<div className="input-container">
|
<div className={`input-container ${inputActive ? "active" : ""}`}>
|
||||||
<input type="text" />
|
<textarea
|
||||||
<div className="sent-button">
|
placeholder="type something"
|
||||||
|
ref={textareaRef}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onFocus={() => setInputActive(true)}
|
||||||
|
onBlur={() => setInputActive(false)}
|
||||||
|
style={{ resize: "none" }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`sent-button ${
|
||||||
|
value === "" ? "disable-send-btn" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<ExpandIcon />
|
<ExpandIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
export const adjustHeight = (textareaRef: HTMLTextAreaElement) => {
|
||||||
|
const el = textareaRef;
|
||||||
|
if (el) {
|
||||||
|
el.style.height = "auto";
|
||||||
|
el.style.height = "38px";
|
||||||
|
|
||||||
|
// Clamp to max height for 6 lines
|
||||||
|
const lineHeight = 18; // px, adjust if needed
|
||||||
|
const maxHeight = lineHeight * 6;
|
||||||
|
if (el.scrollHeight > maxHeight) {
|
||||||
|
el.style.overflowY = "auto";
|
||||||
|
el.style.height = `${maxHeight}px`;
|
||||||
|
} else {
|
||||||
|
el.style.overflowY = "hidden";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -5,7 +5,6 @@ import Controls from '../controls/controls';
|
||||||
import { Environment } from '@react-three/drei'
|
import { Environment } from '@react-three/drei'
|
||||||
|
|
||||||
import background from "../../../assets/textures/hdr/mudroadpuresky2k.hdr";
|
import background from "../../../assets/textures/hdr/mudroadpuresky2k.hdr";
|
||||||
import { MovingClouds } from '../clouds/clouds';
|
|
||||||
|
|
||||||
function Setup() {
|
function Setup() {
|
||||||
return (
|
return (
|
||||||
|
@ -18,7 +17,7 @@ function Setup() {
|
||||||
|
|
||||||
<PostProcessing />
|
<PostProcessing />
|
||||||
|
|
||||||
<MovingClouds />
|
{/* <MovingClouds /> */}
|
||||||
|
|
||||||
<Environment files={background} environmentIntensity={1.5} />
|
<Environment files={background} environmentIntensity={1.5} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -183,7 +183,7 @@ const Project: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<VersionSaved />
|
<VersionSaved />
|
||||||
{/* <ThreadChat /> */}
|
<ThreadChat />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
|
|
||||||
// global input style
|
// global input style
|
||||||
|
|
||||||
input {
|
input,
|
||||||
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: #{$border-radius-large};
|
border-radius: #{$border-radius-large};
|
||||||
|
|
|
@ -93,36 +93,86 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-char-wrapper {
|
.thread-chat-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
// remove later
|
// remove later
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
// ----
|
// ----
|
||||||
z-index: 10000;
|
z-index: #{$z-index-ui-highest};
|
||||||
.thread-char-container {
|
.thread-chat-container {
|
||||||
|
background: var(--background-color);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
border-radius: #{$border-radius-extra-large};
|
||||||
|
width: 20rem;
|
||||||
.header-wrapper {
|
.header-wrapper {
|
||||||
.header {
|
padding: 12px;
|
||||||
}
|
@include flex-space-between;
|
||||||
.header-options {
|
.header-options {
|
||||||
.options-button {
|
@include flex-center;
|
||||||
}
|
position: relative;
|
||||||
.options-list {
|
.options-list {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
transform: translate(-24px, 100%);
|
||||||
|
background: var(--background-color);
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-radius: #{$border-radius-medium};
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
.options {
|
.options {
|
||||||
|
text-wrap: nowrap;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: #{$border-radius-medium};
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: var(--background-color-accent);
|
||||||
|
}
|
||||||
|
&.delete {
|
||||||
|
&:hover {
|
||||||
|
color: var(--log-error-text-color);
|
||||||
|
background: var(--log-error-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.options-button,
|
||||||
|
.close-button {
|
||||||
|
@include flex-center;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
border-radius: #{$border-radius-medium};
|
||||||
|
&:hover {
|
||||||
|
background: var(--background-color-solid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.close-button {
|
.close-button {
|
||||||
|
svg {
|
||||||
|
scale: 1.4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.messages-wrapper {
|
.messages-wrapper {
|
||||||
|
padding: 12px;
|
||||||
|
padding-top: 0;
|
||||||
.edit-container {
|
.edit-container {
|
||||||
.input-container {
|
.input-container {
|
||||||
}
|
}
|
||||||
.actions-container {
|
.actions-container {
|
||||||
|
@include flex-space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin: 8px 0;
|
||||||
.actions {
|
.actions {
|
||||||
.cancel-button {
|
@include flex-center;
|
||||||
|
gap: 4px;
|
||||||
|
.cancel-button,
|
||||||
|
.save-button {
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: #{$border-radius-large};
|
||||||
|
background: var(--background-color-solid);
|
||||||
|
outline: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
.save-button {
|
.save-button {
|
||||||
}
|
}
|
||||||
|
@ -151,8 +201,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.send-message-wrapper {
|
.send-message-wrapper {
|
||||||
|
padding: 12px;
|
||||||
|
padding-top: 8px;
|
||||||
.input-container {
|
.input-container {
|
||||||
|
position: relative;
|
||||||
|
@include flex-space-between;
|
||||||
|
background: var(--background-color);
|
||||||
|
border-radius: #{$border-radius-extra-large};
|
||||||
|
outline: 1px solid var(--border-color);
|
||||||
|
textarea {
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
width: calc(100% - 36px);
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 28px;
|
||||||
|
max-height: 108px;
|
||||||
|
}
|
||||||
.sent-button {
|
.sent-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
@include flex-center;
|
||||||
|
padding: 2px;
|
||||||
|
svg {
|
||||||
|
rotate: 45deg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disable-send-btn {
|
||||||
|
filter: saturate(0);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background: var(--background-color-solid);
|
||||||
|
padding-top: 4px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: end;
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.sent-button {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue