Skip to content

Commit 21ab6f6

Browse files
Merge branch 'improvements1' without main vfs
2 parents 28165f8 + ef75773 commit 21ab6f6

File tree

17 files changed

+4018
-252
lines changed

17 files changed

+4018
-252
lines changed

Frontend/src/App/CodeEditor/LiveChatPanel.tsx

Lines changed: 206 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,21 @@ import {
1212
type Message,
1313
} from "../../Contexts/EditorContext";
1414
import { useTheme } from "../../Contexts/ThemeProvider";
15-
import { ChevronRight } from "lucide-react";
15+
import { ChevronRight, Trash2, Reply } from "lucide-react";
1616
import Avatar from "../../components/Avatar";
1717

1818
export function ChatSpace() {
1919
const [inputMessage, setInputMessage] = useState("");
2020
const [currentUser, setCurrentUser] = useState<CollaborationUser | null>(
2121
null
2222
);
23+
const [hoveredMessageId, setHoveredMessageId] = useState<string | null>(null);
24+
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(
25+
null
26+
);
27+
const [replyToMessage, setReplyToMessage] = useState<Message | null>(null);
2328
const scrollAreaRef = useRef<HTMLDivElement>(null);
29+
const inputRef = useRef<HTMLTextAreaElement>(null);
2430
const { theme } = useTheme();
2531

2632
const { messages, getAwareness, sendChatMessage } = useEditorCollaboration();
@@ -53,10 +59,29 @@ export function ChatSpace() {
5359
}
5460
}, [messages]);
5561

62+
// Close delete confirmation when clicking outside
63+
useEffect(() => {
64+
const handleClickOutside = (event: MouseEvent) => {
65+
if (
66+
showDeleteConfirm &&
67+
!(event.target as Element).closest(".delete-confirmation")
68+
) {
69+
setShowDeleteConfirm(null);
70+
}
71+
};
72+
73+
if (showDeleteConfirm) {
74+
document.addEventListener("mousedown", handleClickOutside);
75+
return () =>
76+
document.removeEventListener("mousedown", handleClickOutside);
77+
}
78+
}, [showDeleteConfirm]);
79+
5680
const handleSendMessage = useCallback(() => {
5781
if (inputMessage.trim()) {
5882
sendChatMessage(inputMessage);
5983
setInputMessage("");
84+
setReplyToMessage(null); // Clear reply after sending
6085
}
6186
}, [inputMessage, sendChatMessage]);
6287

@@ -75,6 +100,35 @@ export function ChatSpace() {
75100
setInputMessage(e.target.value);
76101
};
77102

103+
const handleDeleteMessage = useCallback(
104+
(messageId: string) => {
105+
const success = collaboration.deleteChatMessage(messageId);
106+
if (success) {
107+
setShowDeleteConfirm(null);
108+
}
109+
},
110+
[collaboration]
111+
);
112+
113+
const confirmDelete = (messageId: string) => {
114+
setShowDeleteConfirm(messageId);
115+
};
116+
117+
const cancelDelete = () => {
118+
setShowDeleteConfirm(null);
119+
};
120+
121+
const handleReplyToMessage = useCallback((message: Message) => {
122+
setReplyToMessage(message);
123+
setShowDeleteConfirm(null); // Close any open delete confirmations
124+
// Focus input after setting reply
125+
setTimeout(() => inputRef.current?.focus(), 100);
126+
}, []);
127+
128+
const cancelReply = () => {
129+
setReplyToMessage(null);
130+
};
131+
78132
return (
79133
<div className={`flex flex-col ${theme.surface}`}>
80134
{/* Header */}
@@ -94,6 +148,7 @@ export function ChatSpace() {
94148
<div className="space-y-3">
95149
{messages.map((msg: Message) => {
96150
const isCurrentUser = msg.user === currentUser?.name;
151+
const isDeleteConfirmOpen = showDeleteConfirm === msg.id;
97152

98153
return (
99154
<div
@@ -103,19 +158,21 @@ export function ChatSpace() {
103158
? "justify-start flex-row-reverse"
104159
: "justify-start"
105160
}`}
161+
onMouseEnter={() => setHoveredMessageId(msg.id)}
162+
onMouseLeave={() => setHoveredMessageId(null)}
106163
>
107164
<Avatar
108165
name={msg.user}
109166
src={msg.avatar}
110-
color={undefined} // Blue for current user
167+
color={undefined}
111168
size="medium"
112169
/>
113170

114171
{/* Message bubble */}
115172
<div
116173
className={`flex flex-col max-w-[75%] ${
117174
isCurrentUser ? "items-end" : "items-start"
118-
}`}
175+
} relative group`}
119176
>
120177
{/* Username (only for other users) */}
121178
{!isCurrentUser && (
@@ -126,17 +183,92 @@ export function ChatSpace() {
126183
</span>
127184
)}
128185

129-
{/* Message content */}
130-
<div
131-
className={`px-4 py-2 text-sm shadow-sm ${
132-
isCurrentUser
133-
? "bg-blue-500 text-white rounded-br-md"
134-
: `${theme.surfaceSecondary} ${theme.text} rounded-bl-md`
135-
}`}
136-
>
137-
<p className="break-words">{msg.text}</p>
186+
{/* Message content with reply and delete buttons */}
187+
<div className="relative">
188+
<div
189+
className={`px-4 py-2 text-sm shadow-sm ${
190+
isCurrentUser
191+
? "bg-blue-500 text-white rounded-br-md"
192+
: `${theme.surfaceSecondary} ${theme.text} rounded-bl-md`
193+
}`}
194+
>
195+
{/* Reply indicator */}
196+
{msg.replyTo && (
197+
<div
198+
className={`mb-2 pb-2 border-l-2 pl-2 text-xs opacity-75 ${
199+
isCurrentUser
200+
? "border-blue-200"
201+
: `border-gray-300 ${theme.textSecondary}`
202+
}`}
203+
>
204+
<div className="font-medium">
205+
Replying to {msg.replyTo.user}
206+
</div>
207+
{/* <div className="truncate">{msg.replyTo.text}</div> */}
208+
</div>
209+
)}
210+
<p className="break-words">{msg.text}</p>
211+
</div>
212+
213+
{hoveredMessageId === msg.id && (
214+
<div
215+
className={`absolute -top-2 ${
216+
isCurrentUser ? "-left-16" : "-right-16"
217+
} flex gap-1`}
218+
>
219+
{/* Reply button (for all messages) */}
220+
<button
221+
onClick={() => handleReplyToMessage(msg)}
222+
className="p-1 bg-gray-500 text-white rounded-full opacity-0 group-hover:opacity-100 hover:bg-gray-600 transition-all duration-200 shadow-lg"
223+
title="Reply to message"
224+
>
225+
<Reply className="w-3 h-3" />
226+
</button>
227+
228+
{/* Delete button */}
229+
{isCurrentUser && (
230+
<button
231+
onClick={() => confirmDelete(msg.id)}
232+
className="p-1 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 hover:bg-red-600 transition-all duration-200 shadow-lg"
233+
title="Delete message"
234+
>
235+
<Trash2 className="w-3 h-3" />
236+
</button>
237+
)}
238+
</div>
239+
)}
138240
</div>
139241

242+
{/* Delete confirmation dialog */}
243+
{isDeleteConfirmOpen && (
244+
<div
245+
className={`delete-confirmation absolute z-10 mt-1 p-3 ${
246+
theme.surface
247+
} ${theme.border} border rounded-lg shadow-lg ${
248+
isCurrentUser ? "right-0" : "left-0"
249+
}`}
250+
style={{ top: "100%" }}
251+
>
252+
<p className={`text-xs ${theme.text} mb-2`}>
253+
Delete this message?
254+
</p>
255+
<div className="flex gap-2">
256+
<button
257+
onClick={() => handleDeleteMessage(msg.id)}
258+
className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
259+
>
260+
Delete
261+
</button>
262+
<button
263+
onClick={cancelDelete}
264+
className={`px-2 py-1 text-xs ${theme.surfaceSecondary} ${theme.text} rounded hover:${theme.hover} transition-colors`}
265+
>
266+
Cancel
267+
</button>
268+
</div>
269+
</div>
270+
)}
271+
140272
{/* Timestamp */}
141273
<span className={`text-xs ${theme.textMuted} mt-1 px-1`}>
142274
{new Date(msg.timestamp).toLocaleTimeString([], {
@@ -153,35 +285,68 @@ export function ChatSpace() {
153285
</div>
154286

155287
{/* Input Area */}
156-
<div
157-
className={`flex-shrink-0 min-h-20 flex items-end gap-2 pt-4 p-4 ${theme.border} border-t`}
158-
>
159-
<textarea
160-
placeholder="Type a message... "
161-
value={inputMessage}
162-
onChange={handleInputChange}
163-
onKeyDown={handleKeyDown}
164-
rows={1}
165-
className={`flex-1 px-4 py-2 ${theme.border} border ${theme.surface} focus:outline-none focus:ring-2 focus:ring-blue-500 ${theme.text} transition-all resize-none min-h-[40px] max-h-[120px]`}
166-
style={{
167-
height: "auto",
168-
minHeight: "40px",
169-
maxHeight: "120px",
170-
overflowY: inputMessage.split("\n").length > 3 ? "auto" : "hidden",
171-
}}
172-
onInput={(e) => {
173-
const target = e.target as HTMLTextAreaElement;
174-
target.style.height = "auto";
175-
target.style.height = Math.min(target.scrollHeight, 120) + "px";
176-
}}
177-
/>
178-
<button
179-
onClick={handleSendMessage}
180-
disabled={!inputMessage.trim()}
181-
className={`p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${theme.hover}`}
182-
>
183-
<ChevronRight className="w-5 h-5" />
184-
</button>
288+
<div className={`flex-shrink-0 ${theme.border} border-t`}>
289+
{/* Reply Preview */}
290+
{replyToMessage && (
291+
<div
292+
className={`p-3 ${theme.surfaceSecondary} border-l-4 border-blue-500 mx-4 mt-3 rounded`}
293+
>
294+
<div className="flex items-center justify-between">
295+
<div className="flex-1">
296+
<div
297+
className={`text-xs font-medium ${theme.textSecondary} mb-1`}
298+
>
299+
Replying to {replyToMessage.user}
300+
</div>
301+
<div className={`text-sm ${theme.text} truncate`}>
302+
{replyToMessage.text}
303+
</div>
304+
</div>
305+
<button
306+
onClick={cancelReply}
307+
className={`ml-2 p-1 hover:${theme.hover} rounded`}
308+
title="Cancel reply"
309+
>
310+
<span className={`text-lg ${theme.textSecondary}`}>×</span>
311+
</button>
312+
</div>
313+
</div>
314+
)}
315+
316+
<div className="min-h-20 flex items-end gap-2 pt-4 p-4">
317+
<textarea
318+
ref={inputRef}
319+
placeholder={
320+
replyToMessage
321+
? `Replying to ${replyToMessage.user}...`
322+
: "Type a message... "
323+
}
324+
value={inputMessage}
325+
onChange={handleInputChange}
326+
onKeyDown={handleKeyDown}
327+
rows={1}
328+
className={`flex-1 px-4 py-2 ${theme.border} border ${theme.surface} focus:outline-none focus:ring-2 focus:ring-blue-500 ${theme.text} transition-all resize-none min-h-[40px] max-h-[120px]`}
329+
style={{
330+
height: "auto",
331+
minHeight: "40px",
332+
maxHeight: "120px",
333+
overflowY:
334+
inputMessage.split("\n").length > 3 ? "auto" : "hidden",
335+
}}
336+
onInput={(e) => {
337+
const target = e.target as HTMLTextAreaElement;
338+
target.style.height = "auto";
339+
target.style.height = Math.min(target.scrollHeight, 120) + "px";
340+
}}
341+
/>
342+
<button
343+
onClick={handleSendMessage}
344+
disabled={!inputMessage.trim()}
345+
className={`p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${theme.hover}`}
346+
>
347+
<ChevronRight className="w-5 h-5" />
348+
</button>
349+
</div>
185350
</div>
186351
</div>
187352
);

0 commit comments

Comments
 (0)