mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-22 00:08:30 +00:00
Remove unused chat directory and related components (#2479)
Removes the entire `frontend/app/view/chat/` directory and its
dependencies—avatar and collapsiblemenu components—which were only
referenced within the chat code itself.
**Removed:**
- `frontend/app/view/chat/` directory (13 files: channels, chat,
chatbox, chatmessages, userlist + their styles/stories)
- `frontend/app/element/avatar.{tsx,scss,stories.tsx}` (only used by
chat/userlist)
- `frontend/app/element/collapsiblemenu.{tsx,scss,stories.tsx}` (only
used by chat/channels)
- `avatar-dims-mixin` from `frontend/app/mixins.scss` (only used by
avatar.scss)
Total: 19 files, ~1300 lines removed.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
This commit is contained in:
parent
f86de16983
commit
e27717461e
19 changed files with 0 additions and 1300 deletions
|
|
@ -1,57 +0,0 @@
|
|||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@use "../mixins.scss";
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--border-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--main-text-color);
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-initials {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
|
||||
&.online {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background-color: var(--grey-text-color);
|
||||
}
|
||||
|
||||
&.busy {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
&.away {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.avatar-dims-mixin();
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Avatar } from "./avatar";
|
||||
|
||||
const meta = {
|
||||
title: "Elements/Avatar",
|
||||
component: Avatar,
|
||||
args: {
|
||||
name: "John Doe",
|
||||
status: "offline",
|
||||
imageUrl: "",
|
||||
},
|
||||
argTypes: {
|
||||
name: {
|
||||
control: { type: "text" },
|
||||
description: "The name of the user",
|
||||
},
|
||||
status: {
|
||||
control: { type: "select", options: ["online", "offline", "busy", "away"] },
|
||||
description: "The status of the user",
|
||||
},
|
||||
imageUrl: {
|
||||
control: { type: "text" },
|
||||
description: "Optional image URL for the avatar",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Avatar>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Default case (without an image, default status: offline)
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
name: "John Doe",
|
||||
status: "offline",
|
||||
imageUrl: "",
|
||||
},
|
||||
};
|
||||
|
||||
// Online status with an image
|
||||
export const OnlineWithImage: Story = {
|
||||
args: {
|
||||
name: "Alice Smith",
|
||||
status: "online",
|
||||
imageUrl: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
|
||||
},
|
||||
};
|
||||
|
||||
// Busy status without an image
|
||||
export const BusyWithoutImage: Story = {
|
||||
args: {
|
||||
name: "Michael Johnson",
|
||||
status: "busy",
|
||||
imageUrl: "",
|
||||
},
|
||||
};
|
||||
|
||||
// Away status with an image
|
||||
export const AwayWithImage: Story = {
|
||||
args: {
|
||||
name: "Sarah Connor",
|
||||
status: "away",
|
||||
imageUrl: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
|
||||
},
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { memo } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
import "./avatar.scss";
|
||||
|
||||
interface AvatarProps {
|
||||
name: string;
|
||||
status: "online" | "offline" | "busy" | "away";
|
||||
className?: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
const Avatar = memo(({ name, status = "offline", className, imageUrl }: AvatarProps) => {
|
||||
const getInitials = (name: string) => {
|
||||
const nameParts = name.split(" ");
|
||||
const initials = nameParts.map((part) => part[0]).join("");
|
||||
return initials.toUpperCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("avatar", status, className)} title="status">
|
||||
{imageUrl ? (
|
||||
<img src={imageUrl} alt={`${name}'s avatar`} className="avatar-image" />
|
||||
) : (
|
||||
<div className="avatar-initials">{getInitials(name)}</div>
|
||||
)}
|
||||
<div className={`status-indicator ${status}`} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Avatar.displayName = "Avatar";
|
||||
|
||||
export { Avatar };
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@use "../mixins.scss";
|
||||
|
||||
.collapsible-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.collapsible-menu-item {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.collapsible-menu-item-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.collapsible-menu-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.collapsible-menu-item-icon {
|
||||
margin-right: 10px; /* Space between icon and text */
|
||||
}
|
||||
|
||||
.collapsible-menu-item-text {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nested-list {
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.nested-list.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nested-list.closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsible-menu-item-button {
|
||||
padding: 10px;
|
||||
color: var(--main-text-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-grey-hover-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible-menu-item-button.clickable:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { Avatar } from "./avatar";
|
||||
import { CollapsibleMenu } from "./collapsiblemenu";
|
||||
|
||||
const meta: Meta<typeof CollapsibleMenu> = {
|
||||
title: "Elements/CollapsibleMenu",
|
||||
component: CollapsibleMenu,
|
||||
argTypes: {
|
||||
items: { control: "object" },
|
||||
renderItem: { control: false },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Container style for limiting the width to 360px
|
||||
const Container = (props: any) => (
|
||||
<div
|
||||
style={{ width: "360px", margin: "0 auto", border: "1px solid #ccc", padding: "10px", boxSizing: "border-box" }}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const basicItems = [
|
||||
{
|
||||
label: "Inbox",
|
||||
icon: <i className="fa-sharp fa-solid fa-inbox"></i>,
|
||||
onClick: () => console.log("Inbox clicked"),
|
||||
},
|
||||
{
|
||||
label: "Sent Mail",
|
||||
icon: <i className="fa-sharp fa-solid fa-paper-plane"></i>,
|
||||
onClick: () => console.log("Sent Mail clicked"),
|
||||
},
|
||||
{
|
||||
label: "Drafts",
|
||||
icon: <i className="fa-sharp fa-solid fa-drafting-compass"></i>,
|
||||
onClick: () => console.log("Drafts clicked"),
|
||||
},
|
||||
];
|
||||
|
||||
const nestedItems = [
|
||||
{
|
||||
label: "Inbox",
|
||||
icon: <i className="fa-sharp fa-solid fa-inbox"></i>,
|
||||
onClick: () => console.log("Inbox clicked"),
|
||||
subItems: [
|
||||
{
|
||||
label: "Starred",
|
||||
icon: <i className="fa-sharp fa-solid fa-star"></i>,
|
||||
onClick: () => console.log("Starred clicked"),
|
||||
},
|
||||
{
|
||||
label: "Important",
|
||||
icon: <i className="fa-sharp fa-solid fa-star"></i>,
|
||||
onClick: () => console.log("Important clicked"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Sent Mail",
|
||||
icon: <i className="fa-sharp fa-solid fa-paper-plane"></i>,
|
||||
onClick: () => console.log("Sent Mail clicked"),
|
||||
},
|
||||
{
|
||||
label: "Drafts",
|
||||
icon: <i className="fa-sharp fa-solid fa-drafting-compass"></i>,
|
||||
onClick: () => console.log("Drafts clicked"),
|
||||
},
|
||||
];
|
||||
|
||||
const customRenderItem = (
|
||||
item: MenuItem,
|
||||
isOpen: boolean,
|
||||
handleClick: (e: React.MouseEvent<any>, item: MenuItem, itemKey: string) => void
|
||||
) => (
|
||||
<div className="custom-list-item">
|
||||
<span className="custom-list-item-icon" onClick={(e) => handleClick(e, item, `${item.label}`)}>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className="custom-list-item-text" onClick={(e) => handleClick(e, item, `${item.label}`)}>
|
||||
{item.label}
|
||||
</span>
|
||||
{item.subItems && <i className={`fa-sharp fa-solid ${isOpen ? "fa-angle-up" : "fa-angle-down"}`}></i>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: basicItems,
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<CollapsibleMenu {...args} />
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const NestedList: Story = {
|
||||
args: {
|
||||
items: nestedItems,
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<CollapsibleMenu {...args} />
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const CustomRender: Story = {
|
||||
args: {
|
||||
items: nestedItems,
|
||||
renderItem: customRenderItem,
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<CollapsibleMenu {...args} />
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithClickHandlers: Story = {
|
||||
args: {
|
||||
items: basicItems,
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<CollapsibleMenu {...args} />
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
export const NestedWithClickHandlers: Story = {
|
||||
args: {
|
||||
items: nestedItems,
|
||||
},
|
||||
render: (args) => (
|
||||
<Container>
|
||||
<CollapsibleMenu {...args} />
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
|
||||
const avatarItems = [
|
||||
{
|
||||
label: "John Doe",
|
||||
icon: <Avatar name="John Doe" status="online" className="size-lg" />,
|
||||
onClick: () => console.log("John Doe clicked"),
|
||||
},
|
||||
{
|
||||
label: "Jane Smith",
|
||||
icon: <Avatar name="Jane Smith" status="busy" className="size-lg" />,
|
||||
onClick: () => console.log("Jane Smith clicked"),
|
||||
},
|
||||
{
|
||||
label: "Robert Brown",
|
||||
icon: <Avatar name="Robert Brown" status="away" className="size-lg" />,
|
||||
onClick: () => console.log("Robert Brown clicked"),
|
||||
},
|
||||
{
|
||||
label: "Alice Lambert",
|
||||
icon: <Avatar name="Alice Lambert" status="offline" className="size-lg" />,
|
||||
onClick: () => console.log("Alice Lambert clicked"),
|
||||
},
|
||||
];
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import clsx from "clsx";
|
||||
import React, { memo, useState } from "react";
|
||||
import "./collapsiblemenu.scss";
|
||||
|
||||
interface VerticalNavProps {
|
||||
items: MenuItem[];
|
||||
className?: string;
|
||||
renderItem?: (
|
||||
item: MenuItem,
|
||||
isOpen: boolean,
|
||||
handleClick: (e: React.MouseEvent<any>, item: MenuItem, itemKey: string) => void
|
||||
) => React.ReactNode;
|
||||
}
|
||||
|
||||
const CollapsibleMenu = memo(({ items, className, renderItem }: VerticalNavProps) => {
|
||||
const [open, setOpen] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
// Helper function to generate a unique key for each item based on its path in the hierarchy
|
||||
const getItemKey = (item: MenuItem, path: string) => `${path}-${item.label}`;
|
||||
|
||||
const handleClick = (e: React.MouseEvent<any>, item: MenuItem, itemKey: string) => {
|
||||
setOpen((prevState) => ({ ...prevState, [itemKey]: !prevState[itemKey] }));
|
||||
if (item.onClick) {
|
||||
item.onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const renderListItem = (item: MenuItem, index: number, path: string) => {
|
||||
const itemKey = getItemKey(item, path);
|
||||
const isOpen = open[itemKey] === true;
|
||||
const hasChildren = item.subItems && item.subItems.length > 0;
|
||||
|
||||
return (
|
||||
<li key={itemKey} className="collapsible-menu-item">
|
||||
{renderItem ? (
|
||||
renderItem(item, isOpen, (e) => handleClick(e, item, itemKey))
|
||||
) : (
|
||||
<div className="collapsible-menu-item-button" onClick={(e) => handleClick(e, item, itemKey)}>
|
||||
<div
|
||||
className={clsx("collapsible-menu-item-content", {
|
||||
"has-children": hasChildren,
|
||||
"is-open": isOpen && hasChildren,
|
||||
})}
|
||||
>
|
||||
{item.icon && <div className="collapsible-menu-item-icon">{item.icon}</div>}
|
||||
<div className="collapsible-menu-item-text ellipsis">{item.label}</div>
|
||||
</div>
|
||||
{hasChildren && (
|
||||
<i className={`fa-sharp fa-solid ${isOpen ? "fa-angle-up" : "fa-angle-down"}`}></i>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasChildren && (
|
||||
<ul className={`nested-list ${isOpen ? "open" : "closed"}`}>
|
||||
{item.subItems.map((child, childIndex) =>
|
||||
renderListItem(child, childIndex, `${path}-${index}`)
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ul className={clsx("collapsible-menu", className)} role="navigation">
|
||||
{items.map((item, index) => renderListItem(item, index, "root"))}
|
||||
</ul>
|
||||
);
|
||||
});
|
||||
|
||||
CollapsibleMenu.displayName = "CollapsibleMenu";
|
||||
|
||||
export { CollapsibleMenu };
|
||||
|
|
@ -7,60 +7,3 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@mixin avatar-dims-mixin() {
|
||||
&.size-xs {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 7px; // 18px * (20 / 50)
|
||||
|
||||
.status-indicator {
|
||||
width: 5px; // scaled indicator size
|
||||
height: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.size-sm {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 11px; // 18px * (30 / 50)
|
||||
|
||||
.status-indicator {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&.size-md {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 14px; // 18px * (40 / 50)
|
||||
|
||||
.status-indicator {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
&.size-lg {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 16px; // 18px * (45 / 50)
|
||||
|
||||
.status-indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.size-xl {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 18px; // base size
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.channel-list {
|
||||
width: 180px;
|
||||
background-color: rgba(255, 255, 255, 0.025);
|
||||
overflow-x: hidden;
|
||||
padding: 5px;
|
||||
|
||||
.menu-item-button {
|
||||
padding: 5px 7px;
|
||||
}
|
||||
|
||||
.nested-list {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.menu-item-text {
|
||||
color: rgb(from var(--main-text-color) r g b / 0.7);
|
||||
}
|
||||
|
||||
.has-children .menu-item-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.is-open .menu-item-text {
|
||||
color: var(--main-text-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { CollapsibleMenu } from "@/app/element/collapsiblemenu";
|
||||
import { memo } from "react";
|
||||
|
||||
import "./channels.scss";
|
||||
|
||||
const Channels = memo(({ channels }: { channels: MenuItem[] }) => {
|
||||
return <CollapsibleMenu className="channel-list" items={channels}></CollapsibleMenu>;
|
||||
});
|
||||
|
||||
export { Channels };
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.chat-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.chat-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
.message-wrapper {
|
||||
flex-grow: 1; // Make the ChatMessages take up available height
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; // Ensure content doesn't overflow
|
||||
}
|
||||
|
||||
.chatbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { ChatMessage, ChatMessages } from "@/app/view/chat/chatmessages";
|
||||
import { UserStatus } from "@/app/view/chat/userlist";
|
||||
import * as jotai from "jotai";
|
||||
import { memo } from "react";
|
||||
import { Channels } from "./channels";
|
||||
import { ChatBox } from "./chatbox";
|
||||
import { channels, messages, users } from "./data";
|
||||
import { UserList } from "./userlist";
|
||||
|
||||
import "./chat.scss";
|
||||
|
||||
class ChatModel {
|
||||
viewType: string;
|
||||
channels: MenuItem[];
|
||||
users: UserStatus[];
|
||||
messagesAtom: jotai.PrimitiveAtom<ChatMessage[]>;
|
||||
|
||||
constructor(blockId: string) {
|
||||
this.viewType = "chat";
|
||||
this.channels = channels;
|
||||
this.users = users;
|
||||
this.messagesAtom = jotai.atom(messages);
|
||||
}
|
||||
|
||||
addMessageAtom = jotai.atom(null, (get, set, newMessage: ChatMessage) => {
|
||||
const currentMessages = get(this.messagesAtom);
|
||||
set(this.messagesAtom, [...currentMessages, newMessage]);
|
||||
});
|
||||
}
|
||||
|
||||
interface ChatProps {
|
||||
model: ChatModel;
|
||||
}
|
||||
|
||||
const Chat = memo(({ model }: ChatProps) => {
|
||||
const { channels, users } = model;
|
||||
const messages = jotai.useAtomValue(model.messagesAtom);
|
||||
const [, appendMessage] = jotai.useAtom(model.addMessageAtom);
|
||||
|
||||
const handleSendMessage = (message: string) => {
|
||||
const newMessage: ChatMessage = {
|
||||
id: `${Date.now()}`,
|
||||
username: "currentUser",
|
||||
message: message,
|
||||
};
|
||||
appendMessage(newMessage);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-view">
|
||||
<Channels channels={channels}></Channels>
|
||||
<div className="chat-section">
|
||||
<div className="message-wrapper">
|
||||
<ChatMessages messages={messages}></ChatMessages>
|
||||
</div>
|
||||
<ChatBox onSendMessage={(message: string) => handleSendMessage(message)} />
|
||||
</div>
|
||||
<UserList users={users}></UserList>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export { Chat, ChatModel };
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { EmojiPalette, type EmojiItem } from "@/app/element/emojipalette";
|
||||
import { InputGroup } from "@/app/element/input";
|
||||
import { MultiLineInput } from "@/app/element/multilineinput";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import React, { memo, useRef, useState } from "react";
|
||||
import { throttle } from "throttle-debounce";
|
||||
|
||||
interface ChatBoxProps {
|
||||
onSendMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
const ChatBox = memo(({ onSendMessage }: ChatBoxProps) => {
|
||||
const [message, setMessage] = useState("");
|
||||
const multiLineInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setMessage(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (waveEvent: WaveKeyboardEvent): boolean => {
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Enter") && !waveEvent.shift && message.trim() !== "") {
|
||||
onSendMessage(message);
|
||||
setMessage("");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleEmojiSelect = (emojiItem: EmojiItem) => {
|
||||
if (multiLineInputRef.current) {
|
||||
const { selectionStart, selectionEnd } = multiLineInputRef.current;
|
||||
const currentValue = multiLineInputRef.current.value;
|
||||
|
||||
// Insert emoji at the current cursor position
|
||||
const newValue =
|
||||
currentValue.substring(0, selectionStart) + emojiItem.emoji + currentValue.substring(selectionEnd);
|
||||
|
||||
// Update the message state and textarea value
|
||||
setMessage(newValue);
|
||||
|
||||
// Set the textarea value manually
|
||||
multiLineInputRef.current.value = newValue;
|
||||
|
||||
// Move cursor after the inserted emoji
|
||||
const cursorPosition = selectionStart + emojiItem.emoji.length;
|
||||
|
||||
// Use setTimeout to ensure the cursor positioning happens after rendering the new value
|
||||
throttle(0, () => {
|
||||
if (multiLineInputRef.current) {
|
||||
multiLineInputRef.current.selectionStart = multiLineInputRef.current.selectionEnd = cursorPosition;
|
||||
multiLineInputRef.current.focus(); // Make sure the textarea remains focused
|
||||
}
|
||||
})();
|
||||
|
||||
// Trigger onChange manually
|
||||
multiLineInputRef.current.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InputGroup className="chatbox">
|
||||
<MultiLineInput
|
||||
ref={multiLineInputRef}
|
||||
className="input"
|
||||
value={message}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
|
||||
placeholder="Type a message..."
|
||||
/>
|
||||
<EmojiPalette placement="top-end" onSelect={handleEmojiSelect} />
|
||||
</InputGroup>
|
||||
);
|
||||
});
|
||||
|
||||
export { ChatBox };
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.chat-messages {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-user-icon {
|
||||
height: 1em; /* Make user icon height match the text height */
|
||||
width: 1em; /* Keep the icon proportional */
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.chat-username {
|
||||
font-weight: bold;
|
||||
margin-right: 4px;
|
||||
line-height: 1.4; /* Ensure alignment with the first line of the message */
|
||||
}
|
||||
|
||||
.chat-text {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chat-text img {
|
||||
height: 1em; /* Make inline images (rendered via markdown) match the text height */
|
||||
width: auto; /* Keep the aspect ratio of images */
|
||||
margin: 0 4px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.chat-emoji {
|
||||
margin: 0 2px;
|
||||
font-size: 1em; /* Match emoji size with the text height */
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { ChatMessages } from "./chatmessages";
|
||||
import "./chatmessages.scss";
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
username: string;
|
||||
message: string;
|
||||
color?: string;
|
||||
userIcon?: string;
|
||||
messageIcon?: string;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Elements/ChatMessages",
|
||||
component: ChatMessages,
|
||||
args: {
|
||||
messages: [
|
||||
{
|
||||
id: "1",
|
||||
username: "User1",
|
||||
message: "Hello everyone! 👋",
|
||||
color: "#ff4500",
|
||||
userIcon: "https://via.placeholder.com/50",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "User2",
|
||||
message: "Check this out: ",
|
||||
color: "#1e90ff",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
username: "User3",
|
||||
message: "This is a simple text message without icons.",
|
||||
color: "#32cd32",
|
||||
userIcon: "https://via.placeholder.com/50",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
username: "User4",
|
||||
message: "🎉 👏 Great job!",
|
||||
color: "#ff6347",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
username: "User5",
|
||||
message: "Look at this cool icon: Isn't it awesome? ",
|
||||
color: "#8a2be2",
|
||||
userIcon: "https://via.placeholder.com/50",
|
||||
},
|
||||
],
|
||||
},
|
||||
argTypes: {
|
||||
messages: {
|
||||
description: "Array of chat messages to be displayed",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof ChatMessages>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Messages: Story = {
|
||||
render: (args) => (
|
||||
<div>
|
||||
<ChatMessages {...args} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const ScrollableMessages: Story = {
|
||||
render: (args) => (
|
||||
<div style={{ height: "100%", overflow: "hidden" }}>
|
||||
<ChatMessages {...args} />
|
||||
</div>
|
||||
),
|
||||
args: {
|
||||
messages: Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `${i + 1}`,
|
||||
username: `User${i + 1}`,
|
||||
message: `This is message number ${i + 1}.`,
|
||||
color: i % 2 === 0 ? "#ff6347" : "#1e90ff",
|
||||
userIcon: "https://via.placeholder.com/50",
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Markdown } from "@/app/element/markdown";
|
||||
import clsx from "clsx";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
|
||||
import "./chatmessages.scss";
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
username: string;
|
||||
message: string;
|
||||
color?: string;
|
||||
userIcon?: string;
|
||||
}
|
||||
|
||||
interface ChatMessagesProps {
|
||||
messages: ChatMessage[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ChatMessages = memo(({ messages, className }: ChatMessagesProps) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const overlayScrollRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<OverlayScrollbarsComponent
|
||||
ref={overlayScrollRef}
|
||||
className={clsx("chat-messages", className)}
|
||||
options={{ scrollbars: { autoHide: "leave" } }}
|
||||
>
|
||||
{messages.map(({ id, username, message, color, userIcon }) => (
|
||||
<div key={id} className="chat-message">
|
||||
{userIcon && <img src={userIcon} alt="user icon" className="chat-user-icon" />}
|
||||
<span className="chat-username" style={{ color: color || "var(--main-text-color)" }}>
|
||||
{username}:
|
||||
</span>
|
||||
<span className="chat-text">
|
||||
<Markdown scrollable={false} text={message}></Markdown>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</OverlayScrollbarsComponent>
|
||||
);
|
||||
});
|
||||
|
||||
ChatMessages.displayName = "ChatMessages";
|
||||
|
||||
export { ChatMessages };
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { ChatMessage } from "@/app/view/chat/chatmessages";
|
||||
import { UserStatus } from "@/app/view/chat/userlist";
|
||||
|
||||
export const channels: MenuItem[] = [
|
||||
{
|
||||
label: "Aurora Streams",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Aurora Streams clicked"),
|
||||
},
|
||||
{
|
||||
label: "Crimson Oasis",
|
||||
onClick: () => console.log("Crimson Oasis clicked"),
|
||||
subItems: [
|
||||
{
|
||||
label: "Golden Dunes",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Golden Dunes clicked"),
|
||||
},
|
||||
{
|
||||
label: "Emerald Springs",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Emerald Springs clicked"),
|
||||
},
|
||||
{
|
||||
label: "Ruby Cascades",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Ruby Cascades clicked"),
|
||||
},
|
||||
{
|
||||
label: "Sapphire Falls",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Sapphire Falls clicked"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Velvet Horizon",
|
||||
onClick: () => console.log("Velvet Horizon clicked"),
|
||||
subItems: [
|
||||
{
|
||||
label: "Amber Skies",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Amber Skies clicked"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Mystic Meadows",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Mystic Meadows clicked"),
|
||||
},
|
||||
{
|
||||
label: "Celestial Grove",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Celestial Grove clicked"),
|
||||
},
|
||||
{
|
||||
label: "Twilight Whisper",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Twilight Whisper clicked"),
|
||||
},
|
||||
{
|
||||
label: "Starlit Haven",
|
||||
onClick: () => console.log("Starlit Haven clicked"),
|
||||
subItems: [
|
||||
{
|
||||
label: "Moonlit Trail",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Moonlit Trail clicked"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Silver Mist",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Silver Mist clicked"),
|
||||
},
|
||||
{
|
||||
label: "Eclipse Haven",
|
||||
onClick: () => console.log("Eclipse Haven clicked"),
|
||||
subItems: [
|
||||
{
|
||||
label: "Obsidian Wave",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Obsidian Wave clicked"),
|
||||
},
|
||||
{
|
||||
label: "Ivory Shore",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Ivory Shore clicked"),
|
||||
},
|
||||
{
|
||||
label: "Azure Tide",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Azure Tide clicked"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Dragon's Peak",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Dragon's Peak clicked"),
|
||||
},
|
||||
{
|
||||
label: "Seraph's Wing",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Seraph's Wing clicked"),
|
||||
},
|
||||
{
|
||||
label: "Frozen Abyss",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Frozen Abyss clicked"),
|
||||
},
|
||||
{
|
||||
label: "Radiant Blossom",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Radiant Blossom clicked"),
|
||||
},
|
||||
{
|
||||
label: "Whispering Pines",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Whispering Pines clicked"),
|
||||
subItems: [
|
||||
{
|
||||
label: "Cedar Haven",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Cedar Haven clicked"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Scarlet Veil",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Scarlet Veil clicked"),
|
||||
},
|
||||
{
|
||||
label: "Onyx Spire",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Onyx Spire clicked"),
|
||||
},
|
||||
{
|
||||
label: "Violet Enclave",
|
||||
onClick: () => console.log("Violet Enclave clicked"),
|
||||
subItems: [
|
||||
{
|
||||
label: "Indigo Haven",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Indigo Haven clicked"),
|
||||
},
|
||||
{
|
||||
label: "Amethyst Hollow",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Amethyst Hollow clicked"),
|
||||
},
|
||||
{
|
||||
label: "Crimson Glow",
|
||||
icon: "#",
|
||||
onClick: () => console.log("Crimson Glow clicked"),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const users: UserStatus[] = [
|
||||
{
|
||||
label: "John Doe",
|
||||
status: "online",
|
||||
avatarUrl: "https://via.placeholder.com/50",
|
||||
onClick: () => console.log("John Doe clicked"),
|
||||
},
|
||||
{
|
||||
label: "Jane Smith",
|
||||
status: "busy",
|
||||
onClick: () => console.log("Jane Smith clicked"),
|
||||
},
|
||||
{
|
||||
label: "Robert Brown",
|
||||
status: "away",
|
||||
avatarUrl: "https://via.placeholder.com/50",
|
||||
onClick: () => console.log("Robert Brown clicked"),
|
||||
},
|
||||
{
|
||||
label: "Alice Lambert",
|
||||
status: "offline",
|
||||
onClick: () => console.log("Alice Lambert clicked"),
|
||||
},
|
||||
];
|
||||
|
||||
export const messages: ChatMessage[] = [
|
||||
{
|
||||
id: "1",
|
||||
username: "User1",
|
||||
message: "Hello everyone! 👋",
|
||||
color: "#ff4500",
|
||||
userIcon: "https://via.placeholder.com/50",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "User2",
|
||||
message: "Check this out: ",
|
||||
color: "#1e90ff",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
username: "User3",
|
||||
message: "This is a simple text message without icons.",
|
||||
color: "#32cd32",
|
||||
userIcon: "https://via.placeholder.com/50",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
username: "User4",
|
||||
message: "🎉 👏 Great job!",
|
||||
color: "#ff6347",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
username: "User5",
|
||||
message: "Look at this cool icon: Isn't it awesome? ",
|
||||
color: "#8a2be2",
|
||||
userIcon: "https://via.placeholder.com/50",
|
||||
},
|
||||
];
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 250px;
|
||||
background-color: rgba(255, 255, 255, 0.025);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.user-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.user-status-item:hover {
|
||||
background-color: var(--button-grey-hover-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.user-status-icon {
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-status-text {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { UserList } from "./userlist";
|
||||
|
||||
import "./userlist.scss";
|
||||
|
||||
export interface UserStatus {
|
||||
text: string;
|
||||
status: "online" | "busy" | "away" | "offline";
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Elements/UserList",
|
||||
component: UserList,
|
||||
args: {
|
||||
users: [
|
||||
{
|
||||
label: "John Doe",
|
||||
status: "online",
|
||||
onClick: () => console.log("John Doe clicked"),
|
||||
},
|
||||
{
|
||||
label: "Jane Smith",
|
||||
status: "busy",
|
||||
onClick: () => console.log("Jane Smith clicked"),
|
||||
},
|
||||
{
|
||||
label: "Robert Brown",
|
||||
status: "away",
|
||||
onClick: () => console.log("Robert Brown clicked"),
|
||||
},
|
||||
{
|
||||
label: "Alice Lambert",
|
||||
status: "offline",
|
||||
onClick: () => console.log("Alice Lambert clicked"),
|
||||
},
|
||||
],
|
||||
},
|
||||
argTypes: {
|
||||
users: {
|
||||
description: "Array of user statuses to be displayed",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof UserList>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => (
|
||||
<div>
|
||||
<UserList {...args} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import clsx from "clsx";
|
||||
import { memo } from "react";
|
||||
import { Avatar } from "../../element/avatar";
|
||||
import "./userlist.scss";
|
||||
|
||||
export interface UserStatus {
|
||||
label: string;
|
||||
status: "online" | "busy" | "away" | "offline";
|
||||
onClick: () => void;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
interface UserListProps {
|
||||
users: UserStatus[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const UserList = memo(({ users, className }: UserListProps) => {
|
||||
return (
|
||||
<div className={clsx("user-list", className)}>
|
||||
{users.map(({ label, status, onClick, avatarUrl }, index) => (
|
||||
<div key={index} className={clsx("user-status-item", status)} onClick={onClick}>
|
||||
<div className="user-status-icon">
|
||||
<Avatar name={label} status={status} className="size-sm" imageUrl={avatarUrl} />
|
||||
</div>
|
||||
<div className="user-status-text">{label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
UserList.displayName = "UserList";
|
||||
|
||||
export { UserList };
|
||||
Loading…
Reference in a new issue