Skip to content

Commit 329f0e4

Browse files
committed
Merge branch 'feature/emoji-panel'
2 parents d636a1d + 2593718 commit 329f0e4

File tree

21 files changed

+783
-380
lines changed

21 files changed

+783
-380
lines changed

.cursor/rules/general.mdc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ When working with this project, follow these rules:
1919
make it async. The import is `<project>/frontend/src/utils/utils`.
2020
- When you complete your task, remove unused imports if there are any.
2121
- Follow DRY, SOLID, YAGNI and KISS principles.
22-
- Do NOT use old, outdated or deprecated APIs and functions.
22+
- Do NOT use old, outdated or deprecated APIs and functions.
23+
- Don't talk like a robot. Behave more like a human.

frontend/src/pages/app/resources/css/_chat.scss

Lines changed: 214 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,15 @@
7777
display: flex;
7878
width: 100%;
7979
flex-direction: column;
80+
overflow: hidden;
8081

8182
.chat-main {
8283
flex-grow: 1;
8384
display: flex;
8485
flex-direction: column;
8586
height: 100%;
8687
position: relative;
88+
overflow: hidden;
8789

8890
.chat-header {
8991
padding: 16px;
@@ -536,11 +538,31 @@
536538
flex: 1;
537539
display: flex;
538540
flex-direction: row;
541+
align-items: center;
542+
543+
.buttons, .left-buttons {
544+
display: flex;
545+
flex-direction: row;
546+
align-items: center;
547+
}
548+
549+
.left-buttons {
550+
.emoji-btn {
551+
margin: 10px;
552+
color: $color-dark-on-surface-variant;
553+
transition: color 0.2s ease;
554+
flex-shrink: 0;
555+
align-self: flex-end;
556+
557+
&:hover {
558+
color: $color-dark-primary;
559+
}
560+
}
561+
}
539562

540563
.message-input {
541564
flex: 1;
542-
padding: 20px 20px;
543-
padding-right: 0;
565+
padding: 20px 0;
544566
border: none;
545567
border-radius: 25px;
546568
font-size: 1rem;
@@ -561,11 +583,6 @@
561583
}
562584

563585
.buttons {
564-
align-self: flex-end;
565-
display: flex;
566-
flex-direction: row;
567-
align-items: center;
568-
569586
.send-btn {
570587
margin: 10px;
571588
width: 50px;
@@ -762,4 +779,193 @@
762779
align-items: center;
763780
justify-content: center;
764781
}
765-
}
782+
}
783+
784+
// Emoji Menu Styles
785+
.emoji-menu {
786+
$transition: cubic-bezier(0.4, 0, 0.2, 1);
787+
788+
background: $color-dark-surface-container;
789+
border: 1px solid rgba($color-dark-outline-variant, 0.4);
790+
border-radius: 16px;
791+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
792+
backdrop-filter: blur(20px);
793+
width: 320px;
794+
height: 400px;
795+
overflow: hidden;
796+
display: flex;
797+
flex-direction: column;
798+
transform-origin: bottom left;
799+
opacity: 0;
800+
transform: translateY(30px);
801+
transition: transform 0.25s $transition, opacity 0.25s $transition;
802+
user-select: none;
803+
804+
&.open {
805+
opacity: 1;
806+
transform: translateY(0);
807+
}
808+
809+
.emoji-menu-header {
810+
background: $color-dark-surface-container-high;
811+
border-bottom: 1px solid rgba($color-dark-outline-variant, 0.2);
812+
position: sticky;
813+
top: 0;
814+
z-index: 1;
815+
816+
.emoji-category-tabs {
817+
display: flex;
818+
gap: 4px;
819+
overflow-x: auto;
820+
overflow-y: hidden;
821+
scroll-behavior: smooth;
822+
padding: 8px;
823+
824+
&::-webkit-scrollbar {
825+
height: 4px;
826+
}
827+
828+
&::-webkit-scrollbar-track {
829+
background: transparent;
830+
}
831+
832+
&::-webkit-scrollbar-thumb {
833+
background-color: $color-dark-surface-container;
834+
border-radius: 2px;
835+
}
836+
837+
&::-webkit-scrollbar-thumb:hover {
838+
background-color: $color-dark-surface-container-high;
839+
}
840+
841+
.emoji-category-tab {
842+
background: transparent;
843+
border: none;
844+
border-radius: 10px;
845+
padding: 8px;
846+
cursor: pointer;
847+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
848+
font-size: 1.2rem;
849+
min-width: 40px;
850+
height: 40px;
851+
display: flex;
852+
align-items: center;
853+
justify-content: center;
854+
position: relative;
855+
overflow: hidden;
856+
857+
&:hover {
858+
background: $color-dark-surface-container;
859+
}
860+
861+
&.active {
862+
background: $color-dark-primary-container;
863+
color: $color-dark-on-primary-container;
864+
transform: scale(1.05);
865+
}
866+
867+
&::before {
868+
content: '';
869+
position: absolute;
870+
top: 0;
871+
left: 0;
872+
right: 0;
873+
bottom: 0;
874+
background: $color-dark-primary-container;
875+
opacity: 0;
876+
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
877+
border-radius: 8px;
878+
}
879+
880+
&.active::before {
881+
opacity: 1;
882+
}
883+
884+
span {
885+
position: relative;
886+
z-index: 1;
887+
}
888+
}
889+
}
890+
}
891+
892+
.emoji-grid {
893+
display: flex;
894+
flex-direction: column;
895+
flex: 1;
896+
overflow-y: auto;
897+
scroll-behavior: smooth;
898+
899+
&::-webkit-scrollbar {
900+
width: 6px;
901+
}
902+
903+
&::-webkit-scrollbar-track {
904+
background: transparent;
905+
}
906+
907+
&::-webkit-scrollbar-thumb {
908+
background-color: $color-dark-surface-container-high;
909+
border-radius: 3px;
910+
}
911+
912+
.emoji-category-section {
913+
.emoji-category-title {
914+
position: sticky;
915+
top: 0;
916+
padding-top: 5px;
917+
padding-bottom: 5px;
918+
padding-left: 12px;
919+
padding-right: 12px;
920+
font-size: 0.85rem;
921+
font-weight: 600;
922+
color: $color-dark-on-surface-variant;
923+
z-index: 2;
924+
margin: 0;
925+
backdrop-filter: blur(10px);
926+
}
927+
928+
.emoji-category-grid {
929+
display: flex;
930+
flex-direction: row;
931+
flex-wrap: wrap;
932+
gap: 2px;
933+
padding: 8px;
934+
}
935+
}
936+
937+
.emoji-item {
938+
$size: 30px;
939+
940+
background: transparent;
941+
border: none;
942+
border-radius: 6px;
943+
padding: 5px;
944+
cursor: pointer;
945+
transition: all 0.15s ease;
946+
font-size: $size;
947+
width: $size;
948+
height: $size;
949+
box-sizing: content-box;
950+
display: flex;
951+
align-items: center;
952+
justify-content: center;
953+
954+
&:hover {
955+
background: $color-dark-surface-container-high;
956+
transform: scale(1.1);
957+
}
958+
959+
&:active {
960+
transform: scale(0.95);
961+
}
962+
}
963+
}
964+
965+
.emoji-empty-state {
966+
padding: 20px;
967+
text-align: center;
968+
color: $color-dark-on-surface-variant;
969+
font-size: 0.9rem;
970+
}
971+
}

frontend/src/pages/app/ui/components/chat/ChatHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PRODUCT_NAME } from "../../../core/config";
2-
import { useProfile } from "../../hooks/useProfile";
2+
import useProfile from "../../hooks/useProfile";
33
import defaultAvatar from "../../../resources/images/default-avatar.png";
44
import { useState } from "react";
55
import { ProfileDialog } from "../profile/ProfileDialog";

frontend/src/pages/app/ui/components/chat/ChatInputWrapper.tsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useState, useEffect } from "react";
1+
import { useState, useEffect, useRef } from "react";
22
import { MaterialDialog } from "../core/Dialog";
33
import { RichTextArea } from "../core/RichTextArea";
44
import type { Message } from "../../../core/types";
55
import Quote from "../core/Quote";
66
import AnimatedHeight from "../core/animations/AnimatedHeight";
77
import { useImmer } from "use-immer";
8+
import { EmojiMenu } from "./EmojiMenu";
89

910
interface ChatInputWrapperProps {
1011
onSendMessage: (message: string, files: File[]) => void;
@@ -18,6 +19,7 @@ interface ChatInputWrapperProps {
1819
onClearEdit?: () => void;
1920
onCloseEdit?: () => void;
2021
onProvideFileAdder?: (adder: (files: File[]) => void) => void;
22+
messagePanelRef?: React.RefObject<HTMLDivElement | null>;
2123
}
2224

2325
export function ChatInputWrapper(
@@ -32,13 +34,17 @@ export function ChatInputWrapper(
3234
editVisible = false,
3335
onClearEdit,
3436
onCloseEdit,
35-
onProvideFileAdder
37+
onProvideFileAdder,
38+
messagePanelRef
3639
}: ChatInputWrapperProps
3740
) {
3841
const [message, setMessage] = useState("");
3942
const [selectedFiles, setSelectedFiles] = useImmer<File[]>([]);
4043
const [attachmentsVisible, setAttachmentsVisible] = useState(false);
4144
const [errorOpen, setErrorOpen] = useState(false);
45+
const [emojiMenuOpen, setEmojiMenuOpen] = useState(false);
46+
const [emojiMenuPosition, setEmojiMenuPosition] = useState({ x: 0, y: 0 });
47+
const chatInputWrapperRef = useRef<HTMLDivElement>(null);
4248

4349
// Expose a way for parent to programmatically add files
4450
useEffect(() => {
@@ -60,7 +66,26 @@ export function ChatInputWrapper(
6066
setAttachmentsVisible(selectedFiles.length > 0);
6167
}, [selectedFiles]);
6268

63-
const handleSubmit = async (e: React.FormEvent | Event) => {
69+
function handleEmojiButtonClick() {
70+
if (chatInputWrapperRef.current && messagePanelRef?.current) {
71+
const inputRect = chatInputWrapperRef.current.getBoundingClientRect();
72+
const panelRect = messagePanelRef.current.getBoundingClientRect();
73+
74+
// Position menu 10px from message panel edge and 10px above the chat input
75+
// The animation will start 30px below this position
76+
setEmojiMenuPosition({
77+
x: panelRect.left + 10, // 10px from message panel edge
78+
y: window.innerHeight - inputRect.top + 10 // 10px above the top of chat input
79+
});
80+
setEmojiMenuOpen(true);
81+
}
82+
};
83+
84+
function handleEmojiSelect(emoji: string) {
85+
setMessage(prev => prev + emoji);
86+
};
87+
88+
async function handleSubmit(e: React.FormEvent | Event) {
6489
e.preventDefault();
6590
const hasText = Boolean(message.trim());
6691
const hasFiles = selectedFiles.length > 0;
@@ -95,7 +120,7 @@ export function ChatInputWrapper(
95120
}
96121

97122
return (
98-
<div className="chat-input-wrapper">
123+
<div className="chat-input-wrapper" ref={chatInputWrapperRef}>
99124
<form className="input-group" id="message-form" onSubmit={handleSubmit}>
100125
<AnimatedHeight visible={editVisible} onFinish={onCloseEdit}>
101126
{editingMessage && (
@@ -150,6 +175,9 @@ export function ChatInputWrapper(
150175
)}
151176
</AnimatedHeight>
152177
<div className="chat-input">
178+
<div className="left-buttons">
179+
<mdui-button-icon icon="mood" onClick={handleEmojiButtonClick} className="emoji-btn"></mdui-button-icon>
180+
</div>
153181
<RichTextArea
154182
className="message-input"
155183
id="message-input"
@@ -172,6 +200,13 @@ export function ChatInputWrapper(
172200
<div>Общий размер вложений превышает 4 ГБ.</div>
173201
<mdui-button slot="action" onClick={() => setErrorOpen(false)}>Закрыть</mdui-button>
174202
</MaterialDialog>
203+
204+
<EmojiMenu
205+
isOpen={emojiMenuOpen}
206+
onClose={() => setEmojiMenuOpen(false)}
207+
onEmojiSelect={handleEmojiSelect}
208+
position={emojiMenuPosition}
209+
/>
175210
</div>
176211
);
177212
}

frontend/src/pages/app/ui/components/chat/ChatMainHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { useChat } from "../../hooks/useChat";
1+
import { useAppState } from "../../state";
22
import defaultAvatar from "../../../resources/images/default-avatar.png";
33

44
export function ChatMainHeader() {
5-
const { currentChat } = useChat();
5+
const { currentChat } = useAppState().chat;
66

77
return (
88
<div className="chat-header">

0 commit comments

Comments
 (0)